feat(posture): add EU AI Act, NIST AI RMF, ISO 42001 compliance categories (14-16)

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 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-10 13:17:25 +02:00
commit 51b5371d6f
2 changed files with 253 additions and 4 deletions

View file

@ -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);

View file

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