fix(llm-security): v8 Phase 1 — Berry lockfile, nested-v1 recursion, per-occurrence strip attribution
Three TDD-first fixes surviving the B8 roadmap bucket (v8.0.0-plan.local.md Phase 1, items 1-3; item 4 JAR hardening scoped out at review): - supply-chain-recheck.mjs parseYarnLock: ported the hook's per-entry parser (pre-install-supply-chain.mjs) so Berry's `version: x` format (unquoted) is recognized alongside Classic's `version "x"` — Berry lockfiles previously yielded zero deps, silently missing pinned compromised packages. - supply-chain-recheck.mjs parsePackageLock: lockfileVersion-1 fallback now recurses nested `dependencies`, mirroring the hook's walk() — a transitive, non-hoisted compromised copy below the top level was previously invisible. - content-extractor.mjs stripInjection: attribution moved from a global `Set<label>` to `Set<label::lineIndex>`. The old check silenced the unstripped flag for ANY occurrence of a label once ANY occurrence had been line-redacted, so a second, cross-line-only encoded occurrence of the same label survived into sanitized output without being flagged. Full suite 2019/2019 (one known-flaky timing test confirmed green in isolation).
This commit is contained in:
parent
b929ddc2bb
commit
ff4d8e8a31
5 changed files with 149 additions and 33 deletions
|
|
@ -117,6 +117,39 @@ describe('supply-chain-recheck: npm blocklist', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('detects compromised event-stream@3.3.6 nested under lockfileVersion 1 dependencies', async () => {
|
||||
// lockfileVersion 1's `dependencies` is a nested tree — a transitive,
|
||||
// non-hoisted copy sits under a parent's own `dependencies` object.
|
||||
// parsePackageLock must recurse, mirroring the hook's walk()
|
||||
// (pre-install-supply-chain.mjs #48).
|
||||
if (existsSync(TEMP)) rmSync(TEMP, { recursive: true });
|
||||
mkdirSync(TEMP, { recursive: true });
|
||||
writeFileSync(join(TEMP, 'package-lock.json'), JSON.stringify({
|
||||
name: 'sample',
|
||||
lockfileVersion: 1,
|
||||
dependencies: {
|
||||
a: {
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
'event-stream': { version: '3.3.6' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
try {
|
||||
const result = await scan(TEMP, { files: [] });
|
||||
const compromised = result.findings.filter(
|
||||
f => f.title.includes('Compromised') && f.title.includes('event-stream')
|
||||
);
|
||||
assert.ok(
|
||||
compromised.length >= 1,
|
||||
`Expected compromised finding for nested event-stream, got ${result.findings.map(f => f.title).join('; ')}`
|
||||
);
|
||||
} finally {
|
||||
cleanupTemp();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not flag clean packages in package-lock.json', async () => {
|
||||
setupTemp({ 'package-lock.json': 'package-lock-clean.json' });
|
||||
try {
|
||||
|
|
@ -140,6 +173,26 @@ describe('supply-chain-recheck: npm blocklist', () => {
|
|||
cleanupTemp();
|
||||
}
|
||||
});
|
||||
|
||||
it('detects compromised event-stream@3.3.6 in a Berry yarn.lock (version: x format)', async () => {
|
||||
// Berry (yarn v2+) writes `version: x` with no quotes, unlike Classic's
|
||||
// `version "x"`. parseYarnLock must recognize both, mirroring the hook's
|
||||
// parser (pre-install-supply-chain.mjs #17).
|
||||
setupTemp({ 'yarn.lock': 'yarn-berry-compromised.lock' });
|
||||
try {
|
||||
const result = await scan(TEMP, { files: [] });
|
||||
const compromised = result.findings.filter(
|
||||
f => f.title.includes('Compromised') && f.title.includes('event-stream')
|
||||
);
|
||||
assert.ok(
|
||||
compromised.length >= 1,
|
||||
`Expected compromised finding for event-stream, got ${result.findings.map(f => f.title).join('; ')}`
|
||||
);
|
||||
assert.equal(compromised[0].severity, 'critical');
|
||||
} finally {
|
||||
cleanupTemp();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue