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:
parent
0765a5595e
commit
51b5371d6f
2 changed files with 253 additions and 4 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue