mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 04:55:12 +02:00
fix(hooks): preserve shell boundaries and detect literal hook bypasses
Preserve contributor history while applying the exact reviewed current-main repair.
Source-PR: https://github.com/affaan-m/ECC/pull/2965
Source-Parent: de480c65fd
Review-Manifest-SHA256: 53ead3eb859a4c6c13d6751b1fe4f9a94f1a5d35a37430f6b8524c50e903490b
This commit is contained in:
@@ -159,28 +159,68 @@ function isNoVerifyLongFlag(value) {
|
||||
}
|
||||
|
||||
const PROTECTED_GIT_COMMANDS = new Set(['commit', 'push', 'merge', 'cherry-pick', 'rebase', 'am']);
|
||||
const GIT_GLOBAL_VALUES = new Set(['-c', '-C', '--work-tree', '--git-dir', '--namespace', '--super-prefix']);
|
||||
const GIT_GLOBAL_VALUES = new Set(['-c', '-C', '--config-env', '--work-tree', '--git-dir', '--namespace', '--super-prefix']);
|
||||
const SHELLS = new Set(['sh', 'bash', 'dash', 'zsh', 'ksh']);
|
||||
const DATA_COMMANDS = new Set(['echo', 'printf', 'cat', 'grep', 'head', 'tail', 'wc', 'sort', 'uniq', ':', 'true', 'false']);
|
||||
const DATA_COMMANDS = new Set(['echo', 'printf', 'cat', 'tee', 'grep', 'head', 'tail', 'wc', 'sort', 'uniq', ':', 'true', 'false']);
|
||||
const CONTROL_WORDS = new Set(['!', 'if', 'then', 'elif', 'while', 'until', 'do', 'else']);
|
||||
|
||||
function basename(value) {
|
||||
return value.replace(/\\/g, '/').split('/').pop();
|
||||
}
|
||||
|
||||
function checkGitWords(words, budget, start = 0) {
|
||||
function isGitExecutable(value) {
|
||||
const name = basename(value).toLowerCase();
|
||||
return name === 'git' || name === 'git.exe';
|
||||
}
|
||||
|
||||
// Only explicit command-scoped assignments are tracked. No host environment,
|
||||
// exported shell state, arbitrary expansion or external configuration is read.
|
||||
function gitEnvironmentOverride(environment, budget) {
|
||||
const count = environment.get('GIT_CONFIG_COUNT') || '';
|
||||
budget.spend(count.length + environment.size + 1);
|
||||
// Git uses strtoul: leading ASCII whitespace/+ are accepted, trailing bytes
|
||||
// and counts above INT_MAX are rejected. Bound work by assignments we own.
|
||||
const configured = /^[ \t\r\n\v\f]*\+?[0-9]+(?![\s\S])/.test(count) ? Number(count) : 0;
|
||||
if (configured > 0 && configured <= 0x7fffffff && configured <= environment.size / 2) {
|
||||
let override = false;
|
||||
let complete = true;
|
||||
for (let i = 0; i < configured; i++) {
|
||||
budget.spend();
|
||||
const key = environment.get(`GIT_CONFIG_KEY_${i}`);
|
||||
if (key === undefined || !environment.has(`GIT_CONFIG_VALUE_${i}`)) { complete = false; break; }
|
||||
budget.spend(key.length + 1);
|
||||
override ||= key.toLowerCase() === 'core.hookspath';
|
||||
}
|
||||
if (complete && override) return true;
|
||||
}
|
||||
const parameters = environment.get('GIT_CONFIG_PARAMETERS');
|
||||
if (parameters) {
|
||||
budget.spend(parameters.length + 1);
|
||||
// Git's old 'key=value' and new 'key'='value' forms both use quote removal.
|
||||
// This inspects literal keys only; nested regions are never executed.
|
||||
for (const command of scanShell(parameters, budget).commands) {
|
||||
for (const word of command.words) {
|
||||
budget.spend(word.value.length + 1);
|
||||
if (word.value.toLowerCase().startsWith(GIT_CONFIG_KEY_PREFIX)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function checkGitWords(words, budget, start = 0, environmentOverride = false) {
|
||||
let index = start + 1;
|
||||
let override = false;
|
||||
let override = environmentOverride;
|
||||
for (; index < words.length; index++) {
|
||||
const value = words[index].value;
|
||||
budget.spend(value.length + 1);
|
||||
if (!value.startsWith('-')) break;
|
||||
if (value === '--') { index++; break; }
|
||||
if (value === '-c') {
|
||||
if (value === '-c' || value === '--config-env') {
|
||||
const setting = words[index + 1]?.value || '';
|
||||
budget.spend(setting.length + 1);
|
||||
override ||= setting.toLowerCase().startsWith(GIT_CONFIG_KEY_PREFIX);
|
||||
} else if (value.toLowerCase().startsWith(`-c${GIT_CONFIG_KEY_PREFIX}`)) override = true;
|
||||
} else if (value.toLowerCase().startsWith(`-c${GIT_CONFIG_KEY_PREFIX}`) || value.toLowerCase().startsWith(`--config-env=${GIT_CONFIG_KEY_PREFIX}`)) override = true;
|
||||
if (GIT_GLOBAL_VALUES.has(value)) index++;
|
||||
}
|
||||
const command = words[index]?.value;
|
||||
@@ -206,10 +246,17 @@ function checkGitWords(words, budget, start = 0) {
|
||||
|
||||
// Only explicit option grammars remove wrapper operands. Unknown launchers are
|
||||
// opaque/conservative, never guessed from a name found among data arguments.
|
||||
function executableWords(words, budget) {
|
||||
function executableWords(words, budget, inherited = new Map()) {
|
||||
budget.spend(inherited.size + 1);
|
||||
const environment = new Map(inherited);
|
||||
function suffix(start) {
|
||||
budget.spend(words.length - start);
|
||||
return words.slice(start);
|
||||
return { words: words.slice(start), environment };
|
||||
}
|
||||
function assignment(value) {
|
||||
const equals = value.indexOf('=');
|
||||
const key = value.slice(0, equals);
|
||||
if (/^GIT_CONFIG_(?:COUNT|PARAMETERS|(?:KEY|VALUE)_[0-9]+)$/.test(key)) environment.set(key, value.slice(equals + 1));
|
||||
}
|
||||
let i = 0;
|
||||
let assignments = true;
|
||||
@@ -217,7 +264,7 @@ function executableWords(words, budget) {
|
||||
while (i < words.length) {
|
||||
const token = words[i];
|
||||
budget.spend(token.value.length + token.raw.length + 1);
|
||||
if (assignments && /^[A-Za-z_][A-Za-z0-9_]*=/.test(environmentAssignments ? token.value : token.raw)) { i++; continue; }
|
||||
if (assignments && /^[A-Za-z_][A-Za-z0-9_]*=/.test(environmentAssignments ? token.value : token.raw)) { assignment(token.value); i++; continue; }
|
||||
if (!token.quoted && CONTROL_WORDS.has(token.value)) { i++; continue; }
|
||||
const name = basename(token.value);
|
||||
if (name === 'command') {
|
||||
@@ -226,7 +273,7 @@ function executableWords(words, budget) {
|
||||
const flag = words[i++].value;
|
||||
budget.spend(flag.length + 1);
|
||||
if (flag === '--') break;
|
||||
if (/^-[pvV]+$/.test(flag) && /[vV]/.test(flag)) return [];
|
||||
if (/^-[pvV]+$/.test(flag) && /[vV]/.test(flag)) return { words: [], environment };
|
||||
if (!/^-p+$/.test(flag)) return suffix(i - 1);
|
||||
}
|
||||
assignments = false; continue;
|
||||
@@ -237,8 +284,9 @@ function executableWords(words, budget) {
|
||||
const flag = words[i++].value;
|
||||
budget.spend(flag.length + 1);
|
||||
if (flag === '--') break;
|
||||
if (flag === '-a') i++;
|
||||
if (/^-[cl]*a$/.test(flag)) i++;
|
||||
else if (!/^-([cl]*a.+|[cl]+)$/.test(flag)) return suffix(i - 1);
|
||||
if (flag.slice(1).split('a', 1)[0].includes('c')) environment.clear();
|
||||
}
|
||||
assignments = false; continue;
|
||||
}
|
||||
@@ -253,6 +301,10 @@ function executableWords(words, budget) {
|
||||
const flag = words[i].value;
|
||||
budget.spend(flag.length + 1);
|
||||
if (flag === '--') { i++; break; }
|
||||
if (env && (flag === '-i' || flag === '--ignore-environment')) environment.clear();
|
||||
if (env && (flag === '-u' || flag === '--unset')) environment.delete(words[i + 1]?.value);
|
||||
else if (env && flag.startsWith('--unset=')) environment.delete(flag.slice('--unset='.length));
|
||||
else if (env && flag.startsWith('-u')) environment.delete(flag.slice(2));
|
||||
if (values.has(flag)) i += 2;
|
||||
else if (flags.has(flag) || [...values].some(value => value.startsWith('--') ? flag.startsWith(`${value}=`) : flag.startsWith(value) && flag.length > value.length)) i++;
|
||||
else return suffix(i - 1); // Includes opaque env -S / sudo shell modes.
|
||||
@@ -261,7 +313,7 @@ function executableWords(words, budget) {
|
||||
}
|
||||
return suffix(i);
|
||||
}
|
||||
return [];
|
||||
return { words: [], environment };
|
||||
}
|
||||
|
||||
function shellRole(words, budget, shell) {
|
||||
@@ -312,7 +364,7 @@ function commandRole(words, budget) {
|
||||
if (!words.length) return { kind: 'data' };
|
||||
budget.spend(words[0].value.length + 1);
|
||||
const name = basename(words[0].value);
|
||||
if (name === 'git' || name === 'git.exe') return { kind: 'git' };
|
||||
if (isGitExecutable(words[0].value)) return { kind: 'git' };
|
||||
if (SHELLS.has(name)) return shellRole(words, budget, name);
|
||||
if (name === 'eval') {
|
||||
for (const word of words) budget.spend(word.value.length + 3);
|
||||
@@ -328,17 +380,26 @@ function pipelineSources(command, budget) {
|
||||
const sources = [];
|
||||
for (let current = command; current; current = current.pipeFrom) {
|
||||
budget.spend(current.words.length + 1);
|
||||
const words = executableWords(current.words, budget);
|
||||
const { words } = executableWords(current.words, budget);
|
||||
for (const word of words) budget.spend(word.value.length + 3);
|
||||
const name = basename(words[0]?.value || '');
|
||||
if (name === 'echo') sources.push(words.slice(1).filter(word => !/^-[neE]+$/.test(word.value)).map(word => word.value).join(' '));
|
||||
if (name === 'echo') sources.push({ text: words.slice(1).filter(word => !/^-[neE]+$/.test(word.value)).map(word => word.value).join(' ') });
|
||||
if (name === 'printf') {
|
||||
const format = words[1]?.value || '';
|
||||
if (format !== '-v') sources.push((format === '%s' || format === '%s\\n') ? words.slice(2).map(word => word.value).join('\n') : words.slice(1).map(word => word.value).join(' '));
|
||||
if (format !== '-v') sources.push({ text: (format === '%s' || format === '%s\\n') ? words.slice(2).map(word => word.value).join('\n') : words.slice(1).map(word => word.value).join(' ') });
|
||||
}
|
||||
if (!DATA_COMMANDS.has(name)) {
|
||||
// Foreign transformations can introduce literal bypasses into executable
|
||||
// stdin. Treat their punctuation as delimiters, not as proved shell syntax.
|
||||
// This deliberately may refuse a transformation that removes a bypass; it
|
||||
// does not evaluate sed/interpreters or detect arbitrary generated source.
|
||||
const text = words.map(word => word.value).join(' ');
|
||||
budget.spend(2 * text.length + 1);
|
||||
sources.push({ text: text.replace(/[^\w$=.+-]/g, ' '), opaque: true });
|
||||
}
|
||||
for (const redirect of current.redirects) {
|
||||
if (redirect.operator === '<<<') sources.push(redirect.word.value);
|
||||
else if (redirect.operator === '<<' || redirect.operator === '<<-') sources.push(redirect.body);
|
||||
if (redirect.operator === '<<<') sources.push({ text: redirect.word.value });
|
||||
else if (redirect.operator === '<<' || redirect.operator === '<<-') sources.push({ text: redirect.body });
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
@@ -346,21 +407,21 @@ function pipelineSources(command, budget) {
|
||||
|
||||
function checkCommand(input) {
|
||||
const budget = createBudget(input.length);
|
||||
const pending = [{ text: input, opaque: false }];
|
||||
function enqueue(text, opaque = false) {
|
||||
const pending = [{ text: input, opaque: false, environment: new Map() }];
|
||||
function enqueue(text, opaque = false, environment = new Map()) {
|
||||
if (!text) return;
|
||||
budget.spend(text.length + 1);
|
||||
pending.push({ text, opaque });
|
||||
pending.push({ text, opaque, environment });
|
||||
}
|
||||
function inspectOpaque(words, text) {
|
||||
function inspectOpaque(words, text, environment) {
|
||||
for (let index = 0; index < words.length; index++) {
|
||||
const word = words[index];
|
||||
budget.spend(word.value.length + 1);
|
||||
if (['git', 'git.exe'].includes(basename(word.value))) {
|
||||
const reason = checkGitWords(words, budget, index);
|
||||
if (isGitExecutable(word.value)) {
|
||||
const reason = checkGitWords(words, budget, index, gitEnvironmentOverride(environment, budget));
|
||||
if (reason) return reason;
|
||||
}
|
||||
if (word.value !== text && /git/.test(word.value) && /[\s'"()]/.test(word.value)) enqueue(word.value, true);
|
||||
if (word.value !== text && /git/i.test(word.value) && /[\s'"()]/.test(word.value)) enqueue(word.value, true, environment);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -368,22 +429,22 @@ function checkCommand(input) {
|
||||
while (pending.length) {
|
||||
const task = pending.pop();
|
||||
const scan = scanShell(task.text, budget);
|
||||
for (const text of scan.nested) enqueue(text);
|
||||
for (const text of scan.nested) enqueue(text, false, task.environment);
|
||||
for (const command of scan.commands) {
|
||||
const words = executableWords(command.words, budget);
|
||||
const { words, environment } = executableWords(command.words, budget, task.environment);
|
||||
const role = commandRole(words, budget);
|
||||
const reason = task.opaque || role.kind === 'opaque'
|
||||
? inspectOpaque(command.words, task.text)
|
||||
: role.kind === 'git' ? checkGitWords(words, budget) : null;
|
||||
? inspectOpaque(command.words, task.text, environment)
|
||||
: role.kind === 'git' ? checkGitWords(words, budget, 0, gitEnvironmentOverride(environment, budget)) : null;
|
||||
if (reason) return { blocked: true, reason };
|
||||
if (role.code) enqueue(role.code);
|
||||
if (role.code) enqueue(role.code, false, environment);
|
||||
if (role.stdin) {
|
||||
for (const redirect of command.redirects) {
|
||||
if (redirect.operator === '<<<') enqueue(redirect.word.value, role.kind === 'opaque');
|
||||
else if (redirect.operator === '<<' || redirect.operator === '<<-') enqueue(redirect.body, role.kind === 'opaque');
|
||||
if (redirect.operator === '<<<') enqueue(redirect.word.value, role.kind === 'opaque', environment);
|
||||
else if (redirect.operator === '<<' || redirect.operator === '<<-') enqueue(redirect.body, role.kind === 'opaque', environment);
|
||||
}
|
||||
if (command.pipeFrom) {
|
||||
for (const source of pipelineSources(command.pipeFrom, budget)) enqueue(source, role.kind === 'opaque');
|
||||
for (const source of pipelineSources(command.pipeFrom, budget)) enqueue(source.text, source.opaque || role.kind === 'opaque', environment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,8 @@ function executionRegion(input, start, budget) {
|
||||
budget.spend();
|
||||
const state = stack[stack.length - 1];
|
||||
const c = input[i];
|
||||
if (state.quote === "'") {
|
||||
if (state.quote === "'" || state.quote === "$'") {
|
||||
if (state.quote === "$'" && c === '\\' && i + 1 < input.length) { i++; continue; }
|
||||
if (c === "'") state.quote = null;
|
||||
continue;
|
||||
}
|
||||
@@ -167,6 +168,9 @@ function executionRegion(input, start, budget) {
|
||||
if (c === '"') state.quote = null;
|
||||
continue;
|
||||
}
|
||||
if (c === '$' && input[i + 1] === "'") {
|
||||
state.quote = "$'"; state.word += "$'"; state.quotedWord = true; i++; continue;
|
||||
}
|
||||
if (c === '"' || c === "'") { state.quote = c; state.word += c; state.quotedWord = true; continue; }
|
||||
if (c === '#' && /[\s;|&()]/.test(input[i - 1] || ' ')) {
|
||||
while (i < input.length && input[i] !== '\n') { budget.spend(); i++; }
|
||||
@@ -282,7 +286,13 @@ function scanShell(input, budget) {
|
||||
while (i < input.length) {
|
||||
budget.spend();
|
||||
const c = input[i];
|
||||
if (quote === "'") {
|
||||
if (quote === "'" || quote === "$'") {
|
||||
if (quote === "$'" && c === '\\' && i + 1 < input.length) {
|
||||
const next = input[i + 1];
|
||||
// Preserve boundaries without claiming general ANSI-C escape expansion.
|
||||
word.value += next === "'" || next === '\\' ? next : c + next;
|
||||
i += 2; continue;
|
||||
}
|
||||
if (c === "'") quote = null;
|
||||
else word.value += c;
|
||||
i++; continue;
|
||||
@@ -307,9 +317,9 @@ function scanShell(input, budget) {
|
||||
i++; continue;
|
||||
}
|
||||
if (c === '$' && input[i + 1] === "'") {
|
||||
// Literal ANSI-C words without escape interpretation; escaped/generated
|
||||
// names are not claimed to be a complete expansion implementation.
|
||||
begin(); word.quoted = true; quote = "'"; i += 2; continue;
|
||||
// ANSI-C escaped quotes do not close the word; its contents never expand.
|
||||
// Numeric/control escapes and generated names remain outside this grammar.
|
||||
begin(); word.quoted = true; quote = "$'"; i += 2; continue;
|
||||
}
|
||||
if (c === '"' || c === "'") { begin(); word.quoted = true; quote = c; i++; continue; }
|
||||
if (c === '#' && !word) {
|
||||
|
||||
@@ -701,6 +701,520 @@ for (const shell of ['sh', 'dash', 'ksh']) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Review-followup witnesses remain inert strings passed only to the classifier.
|
||||
const reviewFollowupCases = [
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
2,
|
||||
"printf '%s' x | sed 's/x/git push --no-verify/' | bash"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
2,
|
||||
"printf '%s' x | sed 's/x/git commit -n/' | sh"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
2,
|
||||
"printf '%s' x | sed 's|x|GIT push --no-verify|' | env bash -s"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
2,
|
||||
"printf '%s' x | sed 's/x/git push --no-verify/' | tee file | bash"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
2,
|
||||
"echo \"$(printf '%s' x | sed 's/x/git push --no-verify/' | bash)\""
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
0,
|
||||
"printf '%s' x | sed 's/x/git push --no-verify/'"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
0,
|
||||
"printf '%s' x | sed 's/x/git push --no-verify/' | tee file"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
0,
|
||||
"printf '%s' x | sed 's/x/echo safe/' | bash"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
0,
|
||||
"printf '%s' x | sed 's/x/git status/' | bash"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
0,
|
||||
"printf '%s' x | sed 's/x/git push --no-verify/' | bash script.sh"
|
||||
],
|
||||
[
|
||||
"transformed executable pipeline",
|
||||
0,
|
||||
"printf '%s' x | sed 's/x/git push --no-verify/' | bash -c 'echo safe'"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
0,
|
||||
"tee file <<'EOF'\ngit push --no-verify\nEOF"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
0,
|
||||
"tee -a file <<EOF\ngit push --no-verify\nEOF"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
0,
|
||||
"tee file <<'EOF'\n$(git push --no-verify)\nEOF"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
2,
|
||||
"tee file <<EOF\n$(git push --no-verify)\nEOF"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
2,
|
||||
"tee file <<'EOF' | bash\ngit push --no-verify\nEOF"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
2,
|
||||
"tee file <<'EOF' | cat | sh -s\ngit push --no-verify\nEOF"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
0,
|
||||
"tee file <<'EOF' | cat\ngit push --no-verify\nEOF"
|
||||
],
|
||||
[
|
||||
"tee data versus executable sink",
|
||||
0,
|
||||
"tee file <<'EOF' | bash -c 'echo safe'\ngit push --no-verify\nEOF"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"GIT commit -n -m x"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"GIT push --no-verify"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"GIT status"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"GIT commit -m \"git push --no-verify\""
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"Git commit -n -m x"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"Git push --no-verify"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"Git status"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"Git commit -m \"git push --no-verify\""
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"git.EXE commit -n -m x"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"git.EXE push --no-verify"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"git.EXE status"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"git.EXE commit -m \"git push --no-verify\""
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"/opt/bin/GiT commit -n -m x"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"/opt/bin/GiT push --no-verify"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"/opt/bin/GiT status"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"/opt/bin/GiT commit -m \"git push --no-verify\""
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"\"C:\\\\tools\\\\git.EXE\" commit -n -m x"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
2,
|
||||
"\"C:\\\\tools\\\\git.EXE\" push --no-verify"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"\"C:\\\\tools\\\\git.EXE\" status"
|
||||
],
|
||||
[
|
||||
"Git basename case",
|
||||
0,
|
||||
"\"C:\\\\tools\\\\git.EXE\" commit -m \"git push --no-verify\""
|
||||
],
|
||||
[
|
||||
"opaque Git basename case",
|
||||
2,
|
||||
"unknown 'GIT push --no-verify'"
|
||||
],
|
||||
[
|
||||
"passive Git basename case",
|
||||
0,
|
||||
"echo 'GIT push --no-verify'"
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
2,
|
||||
"HP=/dev/null git --config-env=core.hooksPath=HP commit -m x"
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
2,
|
||||
"git --config-env=CORE.HOOKSPATH=HP push origin main"
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
2,
|
||||
"git --config-env core.hooksPath=HP commit -m x"
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
0,
|
||||
"git --config-env=color.ui=COLOR commit -m x"
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
0,
|
||||
"git --config-env=core.hooksPath=HP status"
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
0,
|
||||
"git commit -m \"--config-env=core.hooksPath=HP\""
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
0,
|
||||
"echo 'HP=/dev/null git --config-env=core.hooksPath=HP commit'"
|
||||
],
|
||||
[
|
||||
"Git config-env",
|
||||
2,
|
||||
"env HP=/dev/null git --config-env=core.hooksPath=HP merge branch"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit -m x"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
2,
|
||||
"env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git push"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
2,
|
||||
"env -i GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null command git rebase main"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null bash -c 'git commit -m x'"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git -c core.hooksPath=safe-hooks commit -m x"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git status"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null echo 'git push --no-verify'"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT= GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=color.ui GIT_CONFIG_VALUE_0=core.hooksPath=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null env -i git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null env -u GIT_CONFIG_COUNT git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null env --unset=GIT_CONFIG_KEY_0 git commit"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null bash -c 'env -i git commit'"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=color.ui GIT_CONFIG_VALUE_0=auto GIT_CONFIG_KEY_1=CORE.HOOKSPATH GIT_CONFIG_VALUE_1= git am patches"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0= git merge branch"
|
||||
],
|
||||
[
|
||||
"explicit Git environment",
|
||||
0,
|
||||
"git commit -m \"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath\""
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
2,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\" git commit -m x"
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\" git status"
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\" echo safe"
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
2,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.hooksPath'='/dev/null'\" git commit -m x"
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.hooksPath'='/dev/null'\" git status"
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.hooksPath'='/dev/null'\" echo safe"
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"'color.ui=core.hooksPath=/dev/null'\" git commit"
|
||||
],
|
||||
[
|
||||
"Git parameter environment",
|
||||
0,
|
||||
"GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\" env -i git commit"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
2,
|
||||
"echo $'x\\'' ; git push --no-verify #'"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
2,
|
||||
"echo \"$(echo $'x\\'' ; git push --no-verify #')\n)\""
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
2,
|
||||
"echo \"$(echo $'x\\' )'; git push --no-verify)\""
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
0,
|
||||
"echo $'x\\'; git push --no-verify'"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
0,
|
||||
"echo $'$(git push --no-verify)'"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
0,
|
||||
"echo \"$(echo $'x\\'; git push --no-verify')\""
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
2,
|
||||
"echo $'x\\\\'; git push --no-verify"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
0,
|
||||
"echo $'x\\\\; git push --no-verify'"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
2,
|
||||
"$'git' push --no-verify"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
0,
|
||||
"$'git' commit -m $'document git push --no-verify'"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
2,
|
||||
"echo $'x\\''; git commit -n # ignored'"
|
||||
],
|
||||
[
|
||||
"ANSI-C quote boundary",
|
||||
0,
|
||||
"printf '%s' $'x\\'; git commit -n'"
|
||||
]
|
||||
];
|
||||
reviewFollowupCases.push(...[
|
||||
[
|
||||
"exec environment reset",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null exec -c git commit"
|
||||
],
|
||||
[
|
||||
"exec environment reset",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null exec -cl git commit"
|
||||
],
|
||||
[
|
||||
"exec environment reset",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null exec -acustom git commit"
|
||||
],
|
||||
[
|
||||
"exec environment reset",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null exec -a c git commit"
|
||||
],
|
||||
[
|
||||
"exec environment reset",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null exec -ca custom git commit"
|
||||
],
|
||||
[
|
||||
"Git count grammar",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT='+1' GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"Git count grammar",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT=' 1' GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"Git count grammar",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT='\t+1' GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"Git count grammar",
|
||||
2,
|
||||
"GIT_CONFIG_COUNT='0001' GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"Git count grammar",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT='1 ' GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"Git count grammar",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT='-1' GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
],
|
||||
[
|
||||
"Git count grammar",
|
||||
0,
|
||||
"GIT_CONFIG_COUNT='2147483648' GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null git commit"
|
||||
]
|
||||
]);
|
||||
for (const [family, expected, command] of reviewFollowupCases) {
|
||||
if (test(`${family} ${expected}: ${JSON.stringify(command)}`, () => {
|
||||
const result = runHook(command);
|
||||
assert.strictEqual(result.code, expected, result.stderr);
|
||||
if (expected === 2) assert.match(result.stderr, /BLOCKED/);
|
||||
})) passed++; else failed++;
|
||||
}
|
||||
|
||||
const pureOnly = process.argv.includes('--pure-only');
|
||||
if (pureOnly) console.log('Pure classifier mode: 3 bounded Node routing checks omitted.');
|
||||
else {
|
||||
|
||||
Reference in New Issue
Block a user