Files
msd-core/tests/fix-1627-asvs-level-scaling.test.cjs
Tom Boucher f9d9dfb4bc fix(#1627): scale security rigor by ASVS level (planner disposition + auditor depth) (#1636)
workflow.security_asvs_level was display-only — the planner hardcoded
'mitigate if ASVS L1 requires it' and the auditor only echoed the level,
so L2/L3 behaved identically to L1.

- New reference gsd-core/references/security-asvs-levels.md defines L1
  (opportunistic), L2 (standard), L3 (comprehensive) for both planner
  threat disposition and auditor verification depth (higher = superset).
- planner: disposition now scales with the configured ASVS level (no
  hardcoded L1) + @-pointer to the reference.
- auditor: verification depth scales with asvs_level (L1 grep-presence,
  L2 boundary/vector check, L3 end-to-end trace + bypass check).
- planning-config.md + INVENTORY updated; planner kept under its 48K cap
  by extracting the goal-backward worked example to planner-guidance.md.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:48:58 -04:00

234 lines
9.8 KiB
JavaScript

// allow-test-rule: source-text-is-the-product #1627
// Agent .md / reference .md files — their text IS what the runtime loads.
// Testing text content tests the deployed contract.
// Per CONTRIBUTING.md exception matrix.
/**
* Fix #1627 — ASVS level scaling
*
* Asserts that `workflow.security_asvs_level` now scales both planner
* threat-disposition rigor and auditor verification depth rather than
* being display-only.
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const AGENTS_DIR = path.join(ROOT, 'agents');
const REFS_DIR = path.join(ROOT, 'gsd-core', 'references');
const MANIFEST_PATH = path.join(ROOT, 'docs', 'INVENTORY-MANIFEST.json');
describe('SECURE: ASVS level scaling (#1627)', () => {
// ── 1. New reference file ────────────────────────────────────────────────
describe('security-asvs-levels.md reference', () => {
const refPath = path.join(REFS_DIR, 'security-asvs-levels.md');
test('file exists', () => {
assert.ok(fs.existsSync(refPath), 'gsd-core/references/security-asvs-levels.md must exist');
});
test('defines all three levels', () => {
const content = fs.readFileSync(refPath, 'utf-8');
assert.ok(content.includes('L1'), 'must define L1');
assert.ok(content.includes('L2'), 'must define L2');
assert.ok(content.includes('L3'), 'must define L3');
});
test('L1 describes opportunistic scope and planner disposition', () => {
const content = fs.readFileSync(refPath, 'utf-8');
assert.ok(
content.toLowerCase().includes('opportunistic'),
'L1 must be described as opportunistic'
);
assert.ok(
content.includes('mitigate') && content.includes('accept'),
'must describe mitigate/accept dispositions'
);
});
test('L2 requires explicit rationale for accepted threats', () => {
const content = fs.readFileSync(refPath, 'utf-8');
// L2 must require documented rationale for accepted risks
assert.ok(
content.includes('rationale') || content.includes('documented'),
'L2 must require documented rationale for accepted threats'
);
});
test('L3 describes deep/comprehensive verification', () => {
const content = fs.readFileSync(refPath, 'utf-8');
const lower = content.toLowerCase();
assert.ok(
lower.includes('deep') || lower.includes('comprehensive') || lower.includes('exhaustive'),
'L3 must describe deep/comprehensive verification'
);
});
test('mentions that higher levels are supersets of lower', () => {
const content = fs.readFileSync(refPath, 'utf-8');
const lower = content.toLowerCase();
assert.ok(
lower.includes('superset') || lower.includes('higher level') || lower.includes('includes all'),
'must note that higher levels are supersets of lower'
);
});
test('describes distinct auditor verification depth for each level', () => {
const content = fs.readFileSync(refPath, 'utf-8');
// All three audit depth keywords should appear
assert.ok(content.includes('grep') || content.includes('PRESENT'), 'L1 audit depth must mention grep/presence check');
assert.ok(content.includes('boundary') || content.includes('addresses'), 'L2 audit depth must mention boundary/addresses');
assert.ok(content.includes('end-to-end') || content.includes('bypass'), 'L3 audit depth must mention end-to-end or bypass check');
});
});
// ── 2. gsd-planner.md — no hardcoded L1 in disposition ──────────────────
describe('gsd-planner.md security disposition', () => {
const plannerPath = path.join(AGENTS_DIR, 'gsd-planner.md');
test('planner security instruction does not hardcode "ASVS L1"', () => {
const content = fs.readFileSync(plannerPath, 'utf-8');
// The old bug: "mitigate if ASVS L1 requires it" — must be gone
assert.ok(
!content.includes('ASVS L1 requires it'),
'planner must not hardcode "ASVS L1 requires it"; it must reference the configured level'
);
});
test('planner references the configured OWASP ASVS level', () => {
const content = fs.readFileSync(plannerPath, 'utf-8');
assert.ok(
content.includes('OWASP ASVS level') || content.includes('configured OWASP'),
'planner must reference the configured OWASP ASVS level'
);
});
test('planner @-references security-asvs-levels.md', () => {
const content = fs.readFileSync(plannerPath, 'utf-8');
assert.ok(
content.includes('security-asvs-levels.md'),
'planner must @-reference security-asvs-levels.md'
);
});
test('planner is under the 49152-char cap', () => {
const content = fs.readFileSync(plannerPath, 'utf-8').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
assert.ok(
content.length < 49152,
`gsd-planner.md must be < 49152 chars (LF-normalized); got ${content.length}`
);
});
});
// ── 3. gsd-security-auditor.md — scaled verification depth ──────────────
describe('gsd-security-auditor.md verification depth', () => {
const auditorPath = path.join(AGENTS_DIR, 'gsd-security-auditor.md');
test('auditor scales verification depth by asvs_level', () => {
const content = fs.readFileSync(auditorPath, 'utf-8');
assert.ok(
content.includes('asvs_level') || content.includes('ASVS level'),
'auditor must reference asvs_level to scale verification'
);
});
test('auditor describes L1/L2/L3 depth differences', () => {
const content = fs.readFileSync(auditorPath, 'utf-8');
// All three levels must appear in context of depth scaling
assert.ok(content.includes('L1'), 'auditor must mention L1 depth');
assert.ok(content.includes('L2'), 'auditor must mention L2 depth');
assert.ok(content.includes('L3'), 'auditor must mention L3 depth');
});
test('auditor @-references security-asvs-levels.md', () => {
const content = fs.readFileSync(auditorPath, 'utf-8');
assert.ok(
content.includes('security-asvs-levels.md'),
'auditor must @-reference security-asvs-levels.md'
);
});
test('auditor still echoes ASVS Level in structured output', () => {
const content = fs.readFileSync(auditorPath, 'utf-8');
assert.ok(
content.includes('ASVS Level:') && content.includes('{1/2/3}'),
'auditor must still emit ASVS Level in SECURED/OPEN_THREATS output'
);
});
});
// ── 4. secure-phase.md — ASVS-aware short-circuit ──────────────────────
describe('secure-phase.md short-circuit conditioned on asvs_level', () => {
const wfPath = path.join(ROOT, 'gsd-core', 'workflows', 'secure-phase.md');
test('short-circuit to Step 6 is gated on asvs_level == 1', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
// The condition must reference asvs_level so that L2/L3 don't skip the auditor
assert.ok(
content.includes('asvs_level == 1'),
'secure-phase.md must gate the skip-to-Step-6 short-circuit on asvs_level == 1'
);
});
test('auditor runs at L2/L3 even when threats_open is 0 (asvs_level >= 2 branch present)', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
// The >= 2 branch must explicitly say the auditor is spawned for L2/L3 deep verification
assert.ok(
content.includes('asvs_level >= 2'),
'secure-phase.md must include asvs_level >= 2 branch that does NOT skip the auditor'
);
// The >= 2 branch must make clear the auditor is spawned (not skipped)
assert.ok(
content.includes('L2/L3 deep verification') || content.includes('L2 boundary') || content.includes('L3 end-to-end'),
'secure-phase.md asvs_level >= 2 branch must reference L2/L3 deep verification'
);
});
});
// ── 5. security-asvs-levels.md — L1 medium-severity gap closed ──────────
describe('security-asvs-levels.md L1 medium-severity is specified', () => {
const refPath = path.join(REFS_DIR, 'security-asvs-levels.md');
test('L1 explicitly handles medium-severity threats (no gap)', () => {
const content = fs.readFileSync(refPath, 'utf-8');
// L1 section must say something about medium-severity
assert.ok(
content.includes('medium-severity') || content.includes('medium severity'),
'L1 must explicitly specify disposition for medium-severity threats (no ambiguity gap)'
);
});
test('L1 medium-severity disposition is conditional (trust-boundary-aware)', () => {
const content = fs.readFileSync(refPath, 'utf-8');
// L1 must distinguish between medium on primary trust boundary vs not
assert.ok(
content.includes('trust boundary') || content.includes('primary trust'),
'L1 medium-severity rule must reference trust boundary to disambiguate disposition'
);
});
});
// ── 6. Inventory manifest ─────────────────────────────────────────────────
describe('inventory manifest', () => {
test('security-asvs-levels.md is registered in INVENTORY-MANIFEST.json', () => {
const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf-8'));
const refs = (manifest.families || {}).references || [];
assert.ok(
refs.includes('security-asvs-levels.md'),
'security-asvs-levels.md must appear in families.references of INVENTORY-MANIFEST.json'
);
});
});
});