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

@@ -18,9 +18,9 @@
"reductionPct": 16.51
},
"execute-phase": {
"offTokens": 26297,
"onTokens": 24007,
"reductionPct": 8.71
"offTokens": 26184,
"onTokens": 23894,
"reductionPct": 8.75
},
"new-project": {
"offTokens": 14308,
@@ -28,9 +28,9 @@
"reductionPct": 13.59
},
"plan-phase": {
"offTokens": 27641,
"onTokens": 24351,
"reductionPct": 11.9
"offTokens": 27878,
"onTokens": 24588,
"reductionPct": 11.8
},
"verify-work": {
"offTokens": 13384,
@@ -39,8 +39,8 @@
}
},
"aggregate": {
"offTokens": 109060,
"onTokens": 92373,
"reductionPct": 15.3
"offTokens": 109184,
"onTokens": 92497,
"reductionPct": 15.28
}
}

View File

@@ -455,7 +455,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -527,7 +527,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -391,7 +391,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -455,7 +455,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -457,7 +457,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -527,7 +527,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -491,7 +491,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -456,7 +456,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -455,7 +455,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -455,7 +455,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -527,7 +527,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -456,7 +456,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -463,7 +463,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -527,7 +527,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -257,7 +257,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -455,7 +455,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -455,7 +455,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -383,7 +383,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

View File

@@ -527,7 +527,9 @@
"gsd-core/workflows/execute-phase/steps/sequential-root-pin.md",
"gsd-core/workflows/execute-phase/steps/stale-reverification.md",
"gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md",
"gsd-core/workflows/execute-phase/steps/threat-id-gate.md",
"gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md",
"gsd-core/workflows/execute-phase/steps/worktree-base-check.md",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md",
"gsd-core/workflows/execute-plan.md",
"gsd-core/workflows/explore.md",

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');
});
});

View File

@@ -199,3 +199,102 @@ Some body text.
assert.equal(t.trackerId, 'beads:GSD-7');
});
});
describe('plan-document: extractThreatRegisterIds (#4683)', () => {
const { extractThreatRegisterIds } = require('../gsd-core/bin/lib/plan-document.cjs');
const register = (rows) => [
'<threat_model>',
'| Threat ID | Category | Component | Severity | Disposition | Mitigation |',
'|-----------|----------|-----------|----------|-------------|------------|',
...rows,
'</threat_model>',
].join('\n');
test('extracts first-cell IDs in document order', () => {
const ids = extractThreatRegisterIds(register([
'| T-47-01 | Tampering | c | high | mitigate | fix |',
'| T-47-02 | Repudiation | c | low | accept | rationale |',
'| T-47-19 | DoS | c | medium | mitigate | fix |',
]));
assert.deepEqual(ids, ['T-47-01', 'T-47-02', 'T-47-19']);
});
test('the reserved -SC supply-chain row never matches', () => {
const ids = extractThreatRegisterIds(register([
'| T-47-SC | Tampering | npm installs | high | mitigate | gate |',
'| T-47-01 | Tampering | c | high | mitigate | fix |',
]));
assert.deepEqual(ids, ['T-47-01']);
});
test('decimal phases match (T-4.1-05)', () => {
const ids = extractThreatRegisterIds(register(['| T-4.1-05 | Tampering | c | low | accept | r |']));
assert.deepEqual(ids, ['T-4.1-05']);
});
test('rows outside a threat_model block never count', () => {
const ids = extractThreatRegisterIds([
'# Plan',
'',
'See T-47-01 in SECURITY.md. | T-47-02 | not a register |',
'',
register(['| T-47-03 | Tampering | c | high | mitigate | fix |']),
].join('\n'));
assert.deepEqual(ids, ['T-47-03']);
});
test('a register quoted inside a backtick fence is prose, not a claim', () => {
const quoted = ['```markdown', register(['| T-47-01 | Tampering | c | high | mitigate | fix |']), '```'].join('\n');
const live = register(['| T-47-02 | Repudiation | c | low | accept | r |']);
assert.deepEqual(extractThreatRegisterIds(`${quoted}\n\n${live}`), ['T-47-02']);
});
test('a tilde fence is stripped the same way', () => {
const quoted = ['~~~', register(['| T-47-01 | Tampering | c | high | mitigate | fix |']), '~~~'].join('\n');
const live = register(['| T-47-02 | Repudiation | c | low | accept | r |']);
assert.deepEqual(extractThreatRegisterIds(`${quoted}\n\n${live}`), ['T-47-02']);
});
test('an unclosed fence suppresses everything after it', () => {
const doc = [register(['| T-47-01 | Tampering | c | high | mitigate | fix |']), '```', register(['| T-47-02 | DoS | c | low | accept | r |'])].join('\n');
assert.deepEqual(extractThreatRegisterIds(doc), ['T-47-01']);
});
test('block tags are case-insensitive', () => {
const ids = extractThreatRegisterIds([
'<THREAT_MODEL>', '| T-47-05 | Tampering | c | high | mitigate | fix |', '</THREAT_MODEL>',
].join('\n'));
assert.deepEqual(ids, ['T-47-05']);
});
test('multiple blocks yield IDs across both, in order', () => {
const doc = [
register(['| T-47-01 | Tampering | c | high | mitigate | fix |']),
'',
register(['| T-47-02 | Repudiation | c | low | accept | r |']),
].join('\n');
assert.deepEqual(extractThreatRegisterIds(doc), ['T-47-01', 'T-47-02']);
});
test('CRLF rows are read the same as LF', () => {
const ids = extractThreatRegisterIds(register(['| T-47-07 | Tampering | c | high | mitigate | fix |']).replace(/\n/g, '\r\n'));
assert.deepEqual(ids, ['T-47-07']);
});
test('indented rows still match on the first cell', () => {
const ids = extractThreatRegisterIds([
'<threat_model>', ' | T-47-09 | Tampering | c | high | mitigate | fix |', '</threat_model>',
].join('\n'));
assert.deepEqual(ids, ['T-47-09']);
});
test('annotated first cells are knowingly unmatched (accepted residual)', () => {
const ids = extractThreatRegisterIds(register(['| T-47-06 (revised) | Tampering | c | high | mitigate | fix |']));
assert.deepEqual(ids, []);
});
test('content without any threat_model block yields an empty list', () => {
assert.deepEqual(extractThreatRegisterIds('# Plan\n\nDo things.\n'), []);
});
});