fix(#4683): detect cross-plan threat-ID duplicates before execution (#4828)

admin_reason: missing-secondary-reviewer — self-authored overnight sweep; two orthogonal agent reviews ran (isolated adversarial REQUEST-CHANGES with all six findings dispositioned, plus a bypass/consumer-lens APPROVE) and the sha-pinned bench passed 46362/0 on the merged head.
This commit is contained in:
Tom Boucher
2026-09-17 19:06:55 -04:00
committed by GitHub
parent bff99a8bb5
commit c9a5cc3e12
33 changed files with 519 additions and 14 deletions

View File

@@ -5340,3 +5340,197 @@ describe('init plan-phase — wrapped Goal/Requirements fields (#4731)', () => {
);
});
});
// ── #4683 — gap-closure plans reused threat IDs that earlier plans in the ────
// same phase had already assigned to different threats. Nothing detected it:
// SECURITY.md rows and VALIDATION.md's Threat Ref column key on the ID, so a
// reused ID makes every downstream consumer ambiguous. The init payloads now
// carry the cross-plan duplicate list (T-{phase}-NN shapes; the reserved
// T-{phase}-SC supply-chain row is deliberately shared and never flagged), and
// execute-phase.md hard-stops on a non-empty list before any dispatch.
describe('#4683 — cross-plan threat-ID duplicate detection', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.realpathSync(createFixture());
});
afterEach(() => cleanup(tmpDir));
function threatPlan({ ids, gapClosure = false, superseded = false, withSc = true }) {
const rows = ids.map((id) => `| ${id} | Tampering | component | medium | mitigate | fix it |`);
return [
...(superseded ? ['---', 'status: superseded', '---', ''] : []),
'# Plan',
'',
...(gapClosure ? ['gap_closure: true', ''] : []),
'<threat_model>',
'| Threat ID | Category | Component | Severity | Disposition | Mitigation |',
'|-----------|----------|-----------|----------|-------------|------------|',
...(withSc ? ['| T-47-SC | Tampering | npm installs | high | mitigate | legitimacy gate |'] : []),
...rows,
'</threat_model>',
'',
].join('\n');
}
test('init execute-phase reports threat IDs reused across plans (#4683)', () => {
seedPhase(tmpDir, '47-security', {
// Earlier plans own T-47-01..09 / 10..14 / 15..19.
'47-03-PLAN.md': threatPlan({ ids: ['T-47-01', 'T-47-02', 'T-47-03'] }),
'47-04-PLAN.md': threatPlan({ ids: ['T-47-10', 'T-47-11', 'T-47-15'] }),
'47-05-PLAN.md': threatPlan({ ids: ['T-47-19'] }),
// Gap-closure plans renumber from 01 again — the bug: every ID below is
// already claimed by an earlier plan for a DIFFERENT threat.
'47-06-PLAN.md': threatPlan({ ids: ['T-47-10', 'T-47-11', 'T-47-19'], gapClosure: true }),
'47-07-PLAN.md': threatPlan({ ids: ['T-47-15'], gapClosure: true }),
});
writePlanningDocs(tmpDir);
const result = runGsdTools('init execute-phase 47', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
const duplicates = output.threat_id_duplicates;
assert.ok(Array.isArray(duplicates), 'threat_id_duplicates must be an array');
const byId = Object.fromEntries(duplicates.map((d) => [d.id, d.plans]));
for (const id of ['T-47-10', 'T-47-11', 'T-47-15', 'T-47-19']) {
assert.ok(byId[id], `reused ID ${id} must be reported, got: ${JSON.stringify(duplicates)}`);
assert.ok(byId[id].length >= 2, `${id} must name at least the two plans claiming it`);
}
assert.strictEqual(byId['T-47-15'][0], '47-04-PLAN.md');
assert.strictEqual(byId['T-47-15'][1], '47-07-PLAN.md');
// Only genuinely reused IDs — the unique ones stay out.
assert.ok(!byId['T-47-01'] && !byId['T-47-02'] && !byId['T-47-03'], 'uniquely-claimed IDs must not be reported');
assert.strictEqual(output.threat_id_duplicate_count, 4,
`count must match the duplicate list, got ${output.threat_id_duplicate_count} for ${JSON.stringify(duplicates)}`);
// The reserved supply-chain ID is shared BY DESIGN (every plan keeps it).
assert.ok(!byId['T-47-SC'], 'T-47-SC is reserved and deliberately shared — never a duplicate');
});
test('init execute-phase reports no duplicates for unique registers (#4683)', () => {
seedPhase(tmpDir, '47-security', {
'47-03-PLAN.md': threatPlan({ ids: ['T-47-01', 'T-47-02'] }),
'47-04-PLAN.md': threatPlan({ ids: ['T-47-03', 'T-47-04'] }),
});
writePlanningDocs(tmpDir);
const result = runGsdTools('init execute-phase 47', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepEqual(output.threat_id_duplicates, []);
assert.strictEqual(output.threat_id_duplicate_count, 0);
});
test('superseded plans do not hold threat IDs against their replacements (#4683)', () => {
seedPhase(tmpDir, '47-security', {
// Deliberately retired: its IDs moved to the replacing plan.
'47-03-PLAN.md': threatPlan({ ids: ['T-47-01'], superseded: true }),
'47-05-PLAN.md': threatPlan({ ids: ['T-47-01'] }),
});
writePlanningDocs(tmpDir);
const result = runGsdTools('init execute-phase 47', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepEqual(output.threat_id_duplicates, [],
'a superseded plan\'s IDs were deliberately reassigned — not a collision');
assert.strictEqual(output.threat_id_duplicate_count, 0);
});
test('init plan-phase surfaces the same duplicate list (#4683)', () => {
seedPhase(tmpDir, '47-security', {
'47-03-PLAN.md': threatPlan({ ids: ['T-47-01'] }),
'47-04-PLAN.md': threatPlan({ ids: ['T-47-01'] }),
});
writePlanningDocs(tmpDir);
const result = runGsdTools('init plan-phase 47', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.threat_id_duplicate_count, 1);
assert.deepEqual(output.threat_id_duplicates, [{ id: 'T-47-01', plans: ['47-03-PLAN.md', '47-04-PLAN.md'] }]);
});
test('IDs outside a <threat_model> block never count (#4683)', () => {
seedPhase(tmpDir, '47-security', {
'47-03-PLAN.md': '# Plan\n\nSee T-47-01 in SECURITY.md. | T-47-02 | not a register |\n',
'47-04-PLAN.md': '# Plan\n\nSee T-47-01 again.\n',
});
writePlanningDocs(tmpDir);
const result = runGsdTools('init execute-phase 47', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepEqual(output.threat_id_duplicates, [],
'prose mentions of an ID are not register rows — only <threat_model> tables count');
assert.strictEqual(output.threat_id_duplicate_count, 0);
});
});
// ── #4683 review repairs ─────────────────────────────────────────────────────
describe('#4683 review repairs — fence blindness and deterministic ordering', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.realpathSync(createFixture());
});
afterEach(() => cleanup(tmpDir));
test('a register QUOTED inside a code fence is not a claim (#4683 review MAJOR)', () => {
seedPhase(tmpDir, '47-security', {
'47-03-PLAN.md': [
'# Plan', '',
'<threat_model>',
'| T-47-01 | Tampering | component | high | mitigate | fix |',
'</threat_model>', '',
'The register we are extending (quoted verbatim):', '',
'```markdown',
'<threat_model>',
'| Threat ID | Category | Component | Severity | Disposition | Mitigation |',
'|-----------|----------|-----------|----------|-------------|------------|',
'| T-47-01 | Tampering | component | high | mitigate | fix |',
'</threat_model>',
'```', '',
].join('\n'),
'47-04-PLAN.md': [
'# Plan', '',
'<threat_model>',
'| T-47-02 | Repudiation | component | low | accept | rationale |',
'</threat_model>', '',
'Reference copy of phase 47-03\'s register:', '',
'~~~',
'<threat_model>',
'| T-47-01 | Tampering | component | high | mitigate | fix |',
'</threat_model>',
'~~~', '',
].join('\n'),
});
writePlanningDocs(tmpDir);
const result = runGsdTools('init execute-phase 47', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepEqual(output.threat_id_duplicates, [],
'quoted registers live in fenced code blocks — prose, not claims; only live blocks count');
assert.strictEqual(output.threat_id_duplicate_count, 0);
});
test('duplicate entries list claiming plans in deterministic sorted order (#4683 review MINOR)', () => {
seedPhase(tmpDir, '47-security', {
'47-07-PLAN.md': [
'# Plan', '', '<threat_model>', '| T-47-15 | DoS | component | medium | mitigate | fix |', '</threat_model>', '',
].join('\n'),
'47-04-PLAN.md': [
'# Plan', '', '<threat_model>', '| T-47-15 | Repudiation | component | high | mitigate | fix |', '</threat_model>', '',
].join('\n'),
});
writePlanningDocs(tmpDir);
const result = runGsdTools('init execute-phase 47', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepEqual(output.threat_id_duplicates, [
{ id: 'T-47-15', plans: ['47-04-PLAN.md', '47-07-PLAN.md'] },
], 'claiming-plan lists must be sorted, never readdir order');
});
});