fix(llm-security): misc scanner correctness — reflog FP, diff exact-pass, hex dedupe, SARIF version (#20,#22,#50,#54,#56)
#20 git-forensics' reflog force-push detector had a redundant bare 'reset' term subsuming 'reset:', so any commit subject containing 'reset' tripped it; removed. #22 diff-engine's per-current moved-fallback greedily consumed a baseline candidate a later byte-exact match needed (mislabeling unchanged as new/moved on duplicate fingerprints); a global exact pass now runs before the moved-fallback. #54 memory-poisoning double-reported a 64+ char hex token as both base64 and hex; the base64 check now skips pure-hex tokens. #56 SARIF output hardcoded driver.version 6.0.0 because the orchestrator called toSARIF() without a version; it now passes the real plugin version from package.json. #50 the VS Code known-malicious blocklist was empty with no explanation (unlike the JetBrains file's 'empty by design' note); added a matching blocklist_note. Suite 2004/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
parent
8a59d616fb
commit
66e8ce2740
10 changed files with 340 additions and 9 deletions
83
tests/scanners/git-reflog-reset.test.mjs
Normal file
83
tests/scanners/git-reflog-reset.test.mjs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// git-reflog-reset.test.mjs — Regression for #20 (LOW, v7.8.3).
|
||||
//
|
||||
// The force-push detector filtered reflog lines with
|
||||
// `l.includes('reset:') || l.includes('reset')` — the second term subsumes the
|
||||
// first, so any commit whose SUBJECT merely contains the word "reset" tripped
|
||||
// the detector (the reflog %gs for a commit is `commit: <subject>`). Only the
|
||||
// reflog ACTION `reset:` (from `git reset`) is force-push evidence.
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resetCounter } from '../../scanners/lib/output.mjs';
|
||||
import { scan } from '../../scanners/git-forensics.mjs';
|
||||
|
||||
const gitAvailable = spawnSync('git', ['--version'], { encoding: 'utf-8' }).status === 0;
|
||||
|
||||
const RESET_TITLE = 'Force push signal: reflog contains reset entries';
|
||||
|
||||
/** Initialise a hermetic git repo (no global hooks/signing) at `dir`. */
|
||||
function gitInit(dir) {
|
||||
const run = (...args) =>
|
||||
spawnSync('git', args, { cwd: dir, encoding: 'utf-8', env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1' } });
|
||||
run('init', '-q');
|
||||
run('config', 'user.email', 'test@example.com');
|
||||
run('config', 'user.name', 'Test');
|
||||
run('config', 'commit.gpgsign', 'false');
|
||||
run('config', 'core.hooksPath', '/dev/null');
|
||||
return run;
|
||||
}
|
||||
|
||||
describe('git-forensics: reflog reset detection (#20)', () => {
|
||||
it('a commit subject containing the word "reset" does not trip the detector', { skip: !gitAvailable }, async () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), 'git-reflog-reset-'));
|
||||
try {
|
||||
const run = gitInit(repo);
|
||||
writeFileSync(join(repo, 'README.md'), '# fixture\n');
|
||||
run('add', '-A');
|
||||
run('commit', '-q', '-m', 'reset onboarding flow');
|
||||
|
||||
resetCounter();
|
||||
const result = await scan(repo, {});
|
||||
assert.equal(result.status, 'ok', `expected ok, got '${result.status}'`);
|
||||
const resetFindings = result.findings.filter(f => f.title === RESET_TITLE);
|
||||
assert.equal(
|
||||
resetFindings.length, 0,
|
||||
`commit subject "reset onboarding flow" must not trip the force-push detector, ` +
|
||||
`got: ${resetFindings.map(f => f.evidence).join('; ')}`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('a real `reset:` reflog action still trips the detector', { skip: !gitAvailable }, async () => {
|
||||
const repo = mkdtempSync(join(tmpdir(), 'git-reflog-reset-'));
|
||||
try {
|
||||
const run = gitInit(repo);
|
||||
writeFileSync(join(repo, 'README.md'), '# fixture\n');
|
||||
run('add', '-A');
|
||||
run('commit', '-q', '-m', 'initial');
|
||||
writeFileSync(join(repo, 'README.md'), '# fixture v2\n');
|
||||
run('add', '-A');
|
||||
run('commit', '-q', '-m', 'second');
|
||||
// Rewrites history: reflog gains a `reset: moving to HEAD~1` action entry.
|
||||
run('reset', '-q', '--hard', 'HEAD~1');
|
||||
|
||||
resetCounter();
|
||||
const result = await scan(repo, {});
|
||||
assert.equal(result.status, 'ok', `expected ok, got '${result.status}'`);
|
||||
const resetFindings = result.findings.filter(f => f.title === RESET_TITLE);
|
||||
assert.ok(
|
||||
resetFindings.length >= 1,
|
||||
`expected the reset: reflog action to trip the detector, ` +
|
||||
`got findings: ${result.findings.map(f => f.title).join('; ')}`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue