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
|
|
@ -115,7 +115,10 @@ function detectForcePushes(targetPath) {
|
|||
try {
|
||||
const reflog = git(['reflog', '--format=%H %gD %gs', '-n', '500'], targetPath);
|
||||
const lines = reflog.split('\n').filter(Boolean);
|
||||
const resetLines = lines.filter(l => l.includes('reset:') || l.includes('reset'));
|
||||
// v7.8.3 (#20): match only the reflog ACTION `reset:` — a bare `reset`
|
||||
// also matched commit subjects (reflog %gs for a commit is
|
||||
// `commit: <subject>`), so "reset onboarding flow" tripped the detector.
|
||||
const resetLines = lines.filter(l => l.includes('reset:'));
|
||||
|
||||
if (resetLines.length > 0) {
|
||||
const examples = resetLines.slice(0, 3).map(l => l.slice(0, 80)).join(' | ');
|
||||
|
|
|
|||
|
|
@ -175,7 +175,13 @@ export function diffFindings(baselineFindings, currentFindings) {
|
|||
moved: [],
|
||||
};
|
||||
|
||||
// Pass 1: Match current findings against baseline
|
||||
// Pass 1a: Global EXACT-match pass (same file, line within threshold).
|
||||
// v7.8.3 (#22): the moved-fallback used to run per-current inside this loop,
|
||||
// so an earlier unmatched current finding greedily consumed a baseline
|
||||
// candidate that a LATER current finding needed for a byte-exact match
|
||||
// (duplicate fingerprints), mislabeling unchanged findings as new/moved.
|
||||
// Exact matches are now claimed globally before any moved-fallback runs.
|
||||
const unmatchedCurrent = [];
|
||||
for (const current of currentFindings) {
|
||||
const candidates = baselineByFp.get(current.fingerprint);
|
||||
if (!candidates) {
|
||||
|
|
@ -183,7 +189,6 @@ export function diffFindings(baselineFindings, currentFindings) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Try exact match first (same file, line within threshold)
|
||||
let matched = false;
|
||||
for (const baseline of candidates) {
|
||||
if (baseline.matched) continue;
|
||||
|
|
@ -197,9 +202,13 @@ export function diffFindings(baselineFindings, currentFindings) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (matched) continue;
|
||||
if (!matched) unmatchedCurrent.push(current);
|
||||
}
|
||||
|
||||
// Try moved match (fingerprint matches, location differs)
|
||||
// Pass 1b: Moved-fallback on the remainder (fingerprint matches, location differs)
|
||||
for (const current of unmatchedCurrent) {
|
||||
const candidates = baselineByFp.get(current.fingerprint);
|
||||
let matched = false;
|
||||
for (const baseline of candidates) {
|
||||
if (baseline.matched) continue;
|
||||
baseline.matched = true;
|
||||
|
|
@ -211,10 +220,9 @@ export function diffFindings(baselineFindings, currentFindings) {
|
|||
matched = true;
|
||||
break;
|
||||
}
|
||||
if (matched) continue;
|
||||
|
||||
// All candidates consumed — this is new
|
||||
results.new.push(current);
|
||||
if (!matched) results.new.push(current);
|
||||
}
|
||||
|
||||
// Pass 2: Unmatched baseline findings are resolved
|
||||
|
|
|
|||
|
|
@ -326,6 +326,10 @@ function detectEncodedPayloads(content, relPath) {
|
|||
BASE64_TOKEN_RE.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = BASE64_TOKEN_RE.exec(line)) !== null) {
|
||||
// v7.8.3 (#54): a 64+ char pure-hex token (e.g. a sha256) is also
|
||||
// reported by the hex-blob check below — skip the base64 report so one
|
||||
// token yields one finding, labelled hex.
|
||||
if (/^(?:0x)?[0-9a-fA-F]{64,}$/.test(match[0])) continue;
|
||||
if (isBase64Like(match[0])) {
|
||||
results.push(finding({
|
||||
scanner: 'MEM',
|
||||
|
|
|
|||
|
|
@ -283,7 +283,13 @@ async function main() {
|
|||
}
|
||||
|
||||
// Output: SARIF or JSON, to file (--output-file) or stdout
|
||||
const finalOutput = args.format === 'sarif' ? toSARIF(output) : output;
|
||||
// v7.8.3 (#56): pass the real plugin version from package.json — calling
|
||||
// toSARIF(output) bare hardcoded its stale '6.0.0' default as driver.version.
|
||||
let pluginVersion = '0.0.0';
|
||||
try {
|
||||
pluginVersion = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8')).version || pluginVersion;
|
||||
} catch { /* unreadable package.json — fall back to placeholder */ }
|
||||
const finalOutput = args.format === 'sarif' ? toSARIF(output, pluginVersion) : output;
|
||||
const jsonStr = JSON.stringify(finalOutput, null, 2) + '\n';
|
||||
if (args.outputFile) {
|
||||
writeFileSync(args.outputFile, jsonStr);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue