From 51b5371d6f0afef99a9ab9461193363688fd9100 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 10 Apr 2026 13:17:25 +0200 Subject: [PATCH] feat(posture): add EU AI Act, NIST AI RMF, ISO 42001 compliance categories (14-16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends posture scanner from 13 to 16 categories with three governance/compliance checks. New categories are advisory (not in CRITICAL_CATEGORIES) — existing Grade A projects remain Grade A. VERSION bumped to 6.0.0. Co-Authored-By: Claude Opus 4.6 --- .../llm-security/scanners/posture-scanner.mjs | 198 +++++++++++++++++- .../tests/scanners/posture.test.mjs | 59 +++++- 2 files changed, 253 insertions(+), 4 deletions(-) diff --git a/plugins/llm-security/scanners/posture-scanner.mjs b/plugins/llm-security/scanners/posture-scanner.mjs index ff43ddc..9e6c2c8 100644 --- a/plugins/llm-security/scanners/posture-scanner.mjs +++ b/plugins/llm-security/scanners/posture-scanner.mjs @@ -20,7 +20,7 @@ import { finding, scannerResult, resetCounter } from './lib/output.mjs'; // Constants // --------------------------------------------------------------------------- -const VERSION = '5.1.0'; +const VERSION = '6.0.0'; /** Minimum lines for a hook script to be considered non-stub */ const NON_STUB_THRESHOLD = 5; @@ -43,6 +43,9 @@ const CATEGORIES = [ { id: 11, name: 'Prompt Injection Hardening', owasp: 'LLM01, ASI01' }, { id: 12, name: 'Rule of Two', owasp: 'ASI02, ASI05' }, { id: 13, name: 'Long-Horizon Monitoring', owasp: 'ASI06, ASI08' }, + { id: 14, name: 'EU AI Act Compliance', owasp: 'Governance' }, + { id: 15, name: 'NIST AI RMF Alignment', owasp: 'Governance' }, + { id: 16, name: 'ISO 42001 Readiness', owasp: 'Governance' }, ]; // Critical categories: FAIL in these prevents Grade A @@ -1234,6 +1237,194 @@ async function checkLongHorizonMonitoring(projectRoot, hooksJson) { return { status: STATUS.FAIL, findings, evidence }; } +// --------------------------------------------------------------------------- +// Category 14: EU AI Act Compliance (Governance) +// Checks for evidence that supports EU AI Act requirements: +// Art. 9 — risk management system +// Art. 14 — human oversight +// Art. 15 — accuracy, robustness, cybersecurity +// Art. 17 — quality management system +// --------------------------------------------------------------------------- + +async function checkEUAIActCompliance(projectRoot, hooksJson) { + const evidence = []; + const findings = []; + let score = 0; + const maxScore = 4; + + // Art. 9: Risk management — look for structured risk/security documentation + const claudeMd = await readText(join(projectRoot, 'CLAUDE.md')); + const hasRiskDoc = claudeMd && claudeMd.length > 100 && + /security.*boundar|risk.*manage|threat.*model|security.*polic/i.test(claudeMd); + if (hasRiskDoc) { + score++; + evidence.push('Art. 9: risk management documentation found in CLAUDE.md'); + } + + // Art. 14: Human oversight — hooks or CLAUDE.md mention human-in-the-loop + const hasHitl = claudeMd && /human[- ](?:in[- ]the[- ]loop|oversight|review|confirm)|AskUserQuestion/i.test(claudeMd); + const hasHumanHooks = hooksJson && JSON.stringify(hooksJson).includes('UserPromptSubmit'); + if (hasHitl || hasHumanHooks) { + score++; + evidence.push('Art. 14: human oversight mechanism present'); + } + + // Art. 15: Robustness/cybersecurity — security hooks registered + const hookCount = hooksJson ? Object.keys(hooksJson.hooks || {}).length : 0; + if (hookCount >= 2) { + score++; + evidence.push(`Art. 15: ${hookCount} hook event types registered for robustness`); + } + + // Art. 17: Quality management — test suite or scan reports exist + const hasTests = await fileExists(join(projectRoot, 'tests')) || await fileExists(join(projectRoot, 'test')); + const hasReports = await fileExists(join(projectRoot, 'reports')); + if (hasTests || hasReports) { + score++; + evidence.push('Art. 17: quality management evidence (tests or reports)'); + } + + if (score === 0) { + findings.push(finding({ + scanner: 'PST', + severity: SEVERITY.INFO, + title: 'No EU AI Act compliance evidence', + description: 'No risk management, human oversight, robustness hooks, or quality management evidence found.', + owasp: 'Governance', + recommendation: 'Add security documentation to CLAUDE.md, register security hooks, and maintain test suites.', + })); + return { status: STATUS.FAIL, findings, evidence }; + } + + if (score >= maxScore) return { status: STATUS.PASS, findings, evidence }; + return { status: STATUS.PARTIAL, findings, evidence }; +} + +// --------------------------------------------------------------------------- +// Category 15: NIST AI RMF Alignment (Governance) +// Maps to four NIST AI RMF functions: +// Govern — governance controls (deny-first config, policies) +// Map — risk mapping documentation (threat models, risk assessments) +// Measure — measurement tooling (scanners, posture assessment) +// Manage — risk management actions (hooks, remediation capabilities) +// --------------------------------------------------------------------------- + +async function checkNISTAlignment(projectRoot, hooksJson, projectSettings) { + const evidence = []; + const findings = []; + let functionsPresent = 0; + const totalFunctions = 4; + + // Govern: deny-first configuration or policy documentation + const settingsJson = projectSettings || await readJson(join(projectRoot, '.claude', 'settings.json')); + const hasDenyFirst = settingsJson?.permissions?.defaultPermissionLevel === 'deny'; + const hasPolicyFile = await fileExists(join(projectRoot, '.llm-security', 'policy.json')); + if (hasDenyFirst || hasPolicyFile) { + functionsPresent++; + evidence.push('Govern: deny-first config or policy file present'); + } + + // Map: risk documentation (threat-model reports, CLAUDE.md with threat/risk mentions) + const claudeMd = await readText(join(projectRoot, 'CLAUDE.md')); + const hasRiskDoc = claudeMd && /threat|risk.*assess|security.*boundar/i.test(claudeMd); + const hasReports = await fileExists(join(projectRoot, 'reports')); + if (hasRiskDoc || hasReports) { + functionsPresent++; + evidence.push('Map: risk documentation or reports present'); + } + + // Measure: measurement tooling (scanners, tests) + const hasScanners = await fileExists(join(projectRoot, 'scanners')); + const hasTests = await fileExists(join(projectRoot, 'tests')) || await fileExists(join(projectRoot, 'test')); + if (hasScanners || hasTests) { + functionsPresent++; + evidence.push('Measure: measurement tooling present (scanners or tests)'); + } + + // Manage: risk management actions (hooks registered) + const hookCount = hooksJson ? Object.keys(hooksJson.hooks || {}).length : 0; + if (hookCount >= 2) { + functionsPresent++; + evidence.push(`Manage: ${hookCount} hook event types for active risk management`); + } + + if (functionsPresent === 0) { + findings.push(finding({ + scanner: 'PST', + severity: SEVERITY.INFO, + title: 'No NIST AI RMF alignment evidence', + description: 'No evidence for any of the four NIST AI RMF functions: Govern, Map, Measure, Manage.', + owasp: 'Governance', + recommendation: 'Implement deny-first permissions (Govern), add risk documentation (Map), enable scanners (Measure), register hooks (Manage).', + })); + return { status: STATUS.FAIL, findings, evidence }; + } + + if (functionsPresent >= totalFunctions) return { status: STATUS.PASS, findings, evidence }; + return { status: STATUS.PARTIAL, findings, evidence }; +} + +// --------------------------------------------------------------------------- +// Category 16: ISO 42001 Readiness (Governance) +// ISO/IEC 42001:2023 AI Management System indicators: +// Cl. 6 — planning and risk assessment +// Cl. 8 — operational controls +// Cl. 9 — performance evaluation and monitoring +// Cl. 10 — continual improvement +// --------------------------------------------------------------------------- + +async function checkISO42001Readiness(projectRoot, hooksJson) { + const evidence = []; + const findings = []; + let indicators = 0; + const totalIndicators = 4; + + // Cl. 6: Planning and risk — documented processes (CLAUDE.md with structure) + const claudeMd = await readText(join(projectRoot, 'CLAUDE.md')); + if (claudeMd && claudeMd.length > 100) { + indicators++; + evidence.push('Cl. 6: documented AI management processes in CLAUDE.md'); + } + + // Cl. 8: Operational controls — hooks and settings providing runtime controls + const hookCount = hooksJson ? Object.keys(hooksJson.hooks || {}).length : 0; + if (hookCount >= 2) { + indicators++; + evidence.push(`Cl. 8: ${hookCount} operational control hook event types`); + } + + // Cl. 9: Performance evaluation — monitoring and measurement capabilities + const hasReports = await fileExists(join(projectRoot, 'reports')); + const hasTests = await fileExists(join(projectRoot, 'tests')) || await fileExists(join(projectRoot, 'test')); + if (hasReports || hasTests) { + indicators++; + evidence.push('Cl. 9: performance evaluation evidence (reports or tests)'); + } + + // Cl. 10: Continual improvement — baseline diff capability, scan history + const hasBaselines = await fileExists(join(projectRoot, 'reports', 'baselines')); + const hasChangelog = await fileExists(join(projectRoot, 'CHANGELOG.md')); + if (hasBaselines || hasChangelog) { + indicators++; + evidence.push('Cl. 10: continual improvement evidence (baselines or changelog)'); + } + + if (indicators === 0) { + findings.push(finding({ + scanner: 'PST', + severity: SEVERITY.INFO, + title: 'No ISO 42001 readiness evidence', + description: 'No evidence for ISO/IEC 42001 AI management system requirements.', + owasp: 'Governance', + recommendation: 'Document AI processes in CLAUDE.md, register operational hooks, maintain reports, track improvement via baselines.', + })); + return { status: STATUS.FAIL, findings, evidence }; + } + + if (indicators >= totalIndicators) return { status: STATUS.PASS, findings, evidence }; + return { status: STATUS.PARTIAL, findings, evidence }; +} + // --------------------------------------------------------------------------- // Main scan function // --------------------------------------------------------------------------- @@ -1258,7 +1449,7 @@ export async function scan(targetPath) { const projectSettings = await readJson(projectSettingsPath); const hooksJson = await readJson(hooksJsonPath); - // Run all 13 category checks + // Run all 16 category checks (13 security + 3 compliance) const results = []; results.push({ ...CATEGORIES[0], ...(await checkDenyFirst(projectRoot, globalSettings, projectSettings)) }); results.push({ ...CATEGORIES[1], ...(await checkSecretsProtection(projectRoot, hooksJson)) }); @@ -1273,6 +1464,9 @@ export async function scan(targetPath) { results.push({ ...CATEGORIES[10], ...(await checkPromptInjectionHardening(projectRoot, hooksJson)) }); results.push({ ...CATEGORIES[11], ...(await checkRuleOfTwo(projectRoot, hooksJson)) }); results.push({ ...CATEGORIES[12], ...(await checkLongHorizonMonitoring(projectRoot, hooksJson)) }); + results.push({ ...CATEGORIES[13], ...(await checkEUAIActCompliance(projectRoot, hooksJson)) }); + results.push({ ...CATEGORIES[14], ...(await checkNISTAlignment(projectRoot, hooksJson, projectSettings)) }); + results.push({ ...CATEGORIES[15], ...(await checkISO42001Readiness(projectRoot, hooksJson)) }); // Compute grade const applicable = results.filter(r => r.status !== STATUS.NA); diff --git a/plugins/llm-security/tests/scanners/posture.test.mjs b/plugins/llm-security/tests/scanners/posture.test.mjs index 75b4ec6..f06d72b 100644 --- a/plugins/llm-security/tests/scanners/posture.test.mjs +++ b/plugins/llm-security/tests/scanners/posture.test.mjs @@ -42,8 +42,8 @@ describe('posture-scanner: grade-a-project', () => { assert.equal(result.scoring.grade, 'A'); }); - it('has 13 categories assessed', () => { - assert.equal(result.categories.length, 13); + it('has 16 categories assessed', () => { + assert.equal(result.categories.length, 16); }); it('has low risk score', () => { @@ -153,6 +153,42 @@ describe('posture-scanner: grade-a-project', () => { assert.ok(cat12.owasp.includes('ASI02'), 'Cat 12 should map to ASI02'); assert.ok(cat13.owasp.includes('ASI06'), 'Cat 13 should map to ASI06'); }); + + // v6.0 compliance categories + it('EU AI Act Compliance category exists', () => { + const cat = result.categories.find(c => c.id === 14); + assert.ok(cat, 'Category 14 should exist'); + assert.equal(cat.name, 'EU AI Act Compliance'); + }); + + it('NIST AI RMF Alignment category exists', () => { + const cat = result.categories.find(c => c.id === 15); + assert.ok(cat, 'Category 15 should exist'); + assert.equal(cat.name, 'NIST AI RMF Alignment'); + }); + + it('ISO 42001 Readiness category exists', () => { + const cat = result.categories.find(c => c.id === 16); + assert.ok(cat, 'Category 16 should exist'); + assert.equal(cat.name, 'ISO 42001 Readiness'); + }); + + it('compliance categories are PARTIAL for grade-a (has hooks+config but no reports/tests)', () => { + for (const id of [14, 15, 16]) { + const cat = result.categories.find(c => c.id === id); + assert.ok( + cat.status === 'PASS' || cat.status === 'PARTIAL', + `Category ${id} (${cat.name}) should be PASS or PARTIAL, got ${cat.status}`, + ); + } + }); + + it('compliance categories have Governance OWASP mapping', () => { + for (const id of [14, 15, 16]) { + const cat = result.categories.find(c => c.id === id); + assert.ok(cat.owasp, `Category ${id} should have owasp mapping`); + } + }); }); // --------------------------------------------------------------------------- @@ -272,6 +308,25 @@ describe('posture-scanner: grade-f-project', () => { const cat = result.categories.find(c => c.id === 13); assert.equal(cat.status, 'FAIL'); }); + + // v6.0 compliance categories — grade-f has no security config → FAIL + it('EU AI Act Compliance is FAIL', () => { + const cat = result.categories.find(c => c.id === 14); + assert.ok(cat, 'Category 14 should exist'); + assert.equal(cat.status, 'FAIL'); + }); + + it('NIST AI RMF Alignment is FAIL', () => { + const cat = result.categories.find(c => c.id === 15); + assert.ok(cat, 'Category 15 should exist'); + assert.equal(cat.status, 'FAIL'); + }); + + it('ISO 42001 Readiness is FAIL', () => { + const cat = result.categories.find(c => c.id === 16); + assert.ok(cat, 'Category 16 should exist'); + assert.equal(cat.status, 'FAIL'); + }); }); // ---------------------------------------------------------------------------