Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Every file answers one question: **which controls from framework X address vulne
| **<!-- stats:frameworks-mapped -->26<!-- /stats -->** frameworks | Compliance · Governance · Threat modeling · Testing · OT/ICS · Identity · Secure SDLC · Financial |
| **<!-- stats:mapping-files -->77<!-- /stats -->** mapping files | Every source list entry × every applicable framework |
| **Mapping review state** | <!-- stats:frameworks-draft -->2 of 26 carry candidate DRAFT rows only — CoSAI · EU AI Act Code of Practice<!-- /stats --> — see [docs/SCHEMA_V2_MIGRATION.md](docs/SCHEMA_V2_MIGRATION.md) |
| **Framework freshness** | <!-- stats:freshness -->4 current · 2 behind upstream · 20 unchecked<!-- /stats --> — see [docs/FRESHNESS_SLA.md](docs/FRESHNESS_SLA.md) |
| **Framework freshness** | <!-- stats:freshness -->5 current · 2 behind upstream · 20 unchecked<!-- /stats --> — see [docs/FRESHNESS_SLA.md](docs/FRESHNESS_SLA.md) |
| **21** implementation recipes | Production-ready Python patterns |
| **70+** open-source tools | Catalogued and organised by function |
| **25** eval profiles | Runnable Garak (13) + PyRIT (6) + LAAF (6) tests mapped to OWASP entries |
Expand Down
8 changes: 8 additions & 0 deletions data/framework-sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@
"source_url": "https://www.nist.gov/artificial-intelligence/executive-order-safe-secure-and-trustworthy-artificial-intelligence",
"notes": ""
},
"nist-ai-600-1": {
"name": "NIST AI 600-1",
"mapped_version": "2024-07",
"current_version": "2024-07",
"checked": "2026-10-03",
"source_url": "https://doi.org/10.6028/NIST.AI.600-1",
"notes": "NIST AI 600-1 carries no revision number; version is the publication month (July 2024). No later revision published as of the checked date."
},
"nist-csf": {
"name": "NIST CSF 2.0",
"mapped_version": "2.0",
Expand Down
2,568 changes: 2,568 additions & 0 deletions data/frameworks/nist-ai-600-1.json

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions data/stats.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
}
},
"frameworks": {
"registries": 26,
"registries": 27,
"mapped": 26,
"unmapped_registries": [],
"unmapped_registries": [
"NIST AI 600-1"
],
"draft_only": [
"CoSAI",
"EU AI Act Code of Practice"
Expand Down Expand Up @@ -73,8 +75,8 @@
"orphan_failures": 5
},
"freshness": {
"checked": 6,
"current": 4,
"checked": 7,
"current": 5,
"diverged": 2,
"unchecked": 20,
"diverged_frameworks": [
Expand All @@ -93,10 +95,10 @@
]
},
"controls": {
"total": 973,
"registry_items": 1107,
"total": 1184,
"registry_items": 1318,
"by_kind": {
"control": 973,
"control": 1184,
"layer": 10,
"technique": 57,
"threat-category": 6,
Expand Down
2,570 changes: 2,569 additions & 1 deletion docs/frameworks-registry.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions scripts/control-ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ const GRAMMARS = {
parentRe: /\b(V\d{1,2})\b(?!\.)/,
parent: (m) => m[1],
},
'NIST AI 600-1': {
// Suggested-action ids from the GenAI Profile: function, AI RMF subcategory,
// then a three-digit sequence — GV-1.1-001. GV/MP/MS/MG are Govern, Map,
// Measure, Manage, per the document's own key.
re: /\b((?:GV|MP|MS|MG)-\d{1,2}\.\d{1,2}-\d{3})\b/,
},
'ISO/IEC 42001:2023': {
// Annex A controls, Annex B guidance, and management-system clauses.
re: /\b([AB]\.\d{1,2}(?:\.\d{1,2}){0,2})\b|\bCl(?:ause)?\.?\s*(\d{1,2}(?:\.\d{1,2}){0,2})\b/,
Expand Down Expand Up @@ -163,6 +169,7 @@ const ID_SHAPES = {
'NIST SP 800-82 Rev 3': /^(?:§\d{1,2}(?:\.\d{1,2}){0,2}|[A-Z]{2}-\d{1,2})$/,
'CWE/CVE': /^(?:CWE-\d{1,4}|CVE-\d{4}-\d{4,7})$/,
'OWASP AI Testing Guide': /^[A-Z]{3}$/,
'NIST AI 600-1': /^(?:GV|MP|MS|MG)-\d{1,2}\.\d{1,2}-\d{3}$/,
'ISO/IEC 42001:2023': /^(?:[AB]\.\d{1,2}(?:\.\d{1,2}){0,2}|\d{1,2}(?:\.\d{1,2}){0,2})$/,
'OWASP ASVS 5.0.0': /^V\d{1,2}\.\d{1,2}\.\d{1,2}$/,
};
Expand Down
67 changes: 66 additions & 1 deletion scripts/control-ids.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import assert from 'node:assert/strict';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const { resolveControlId } = require('./control-ids.js');
const { resolveControlId, isValidControlId, isValidRegistryId } = require('./control-ids.js');

const cases = [
// framework, row cells, expected id, expected name, expected parent, table headers
Expand Down Expand Up @@ -66,3 +66,68 @@ test('prose is never returned as a control name', () => {
assert.notEqual(got.name, prose);
assert.ok(got.name.length <= 120);
});

test('NIST AI 600-1 suggested-action ids are recognised wherever they sit in the row', () => {
const cases = [
[['GV-1.1-001', 'Align GAI development and use with applicable laws'], 'GV-1.1-001'],
[['Fairness assessments', 'MS-2.11-002', 'prose about the measure'], 'MS-2.11-002'],
[['MG-4.1-003'], 'MG-4.1-003'],
];
for (const [cells, id] of cases) {
assert.equal(resolveControlId('NIST AI 600-1', cells).id, id);
}
});

test('NIST AI 600-1 rejects subcategory ids and malformed sequences', () => {
// GV-1.1 is an AI RMF subcategory, not a 600-1 suggested action.
assert.equal(resolveControlId('NIST AI 600-1', ['GV-1.1', 'Legal and regulatory']), null);
for (const bad of ['GV-1.1', 'GV-1.1-1', 'GV-1.1-0001', 'XX-1.1-001']) {
assert.equal(isValidControlId('NIST AI 600-1', bad), false, bad);
}
for (const good of ['GV-1.1-001', 'MP-5.1-001', 'MS-2.11-002', 'MG-4.1-003']) {
assert.equal(isValidControlId('NIST AI 600-1', good), true, good);
}
});

test('every id in the NIST AI 600-1 registry satisfies its own grammar', async () => {
const { readFileSync } = await import('node:fs');
const fw = JSON.parse(readFileSync(new URL('../data/frameworks/nist-ai-600-1.json', import.meta.url)));
assert.equal(fw.controls.length, 211);
for (const c of fw.controls) {
assert.ok(isValidRegistryId('NIST AI 600-1', c.control_id), `${c.control_id} fails the id shape`);
}
});

test('every NIST AI 600-1 action carries GAI risk tags drawn from the document\'s twelve', async () => {
// The twelve risks enumerated in section 2 of NIST AI 600-1. The suggested-action
// tables spell four of them differently; the registry normalises to this list.
const TWELVE = new Set([
'CBRN Information or Capabilities',
'Confabulation',
'Dangerous, Violent, or Hateful Content',
'Data Privacy',
'Environmental Impacts',
'Harmful Bias or Homogenization',
'Human-AI Configuration',
'Information Integrity',
'Information Security',
'Intellectual Property',
'Obscene, Degrading, and/or Abusive Content',
'Value Chain and Component Integration',
]);
const { readFileSync } = await import('node:fs');
const fw = JSON.parse(readFileSync(new URL('../data/frameworks/nist-ai-600-1.json', import.meta.url)));
const seen = new Set();
for (const c of fw.controls) {
assert.ok(Array.isArray(c.gai_risks) && c.gai_risks.length > 0,
`${c.control_id} has no gai_risks`);
assert.equal(new Set(c.gai_risks).size, c.gai_risks.length,
`${c.control_id} repeats a risk`);
for (const r of c.gai_risks) {
assert.ok(TWELVE.has(r), `${c.control_id} cites "${r}", which is not one of the twelve`);
seen.add(r);
}
}
// All twelve are exercised, so a typo in the list cannot pass unnoticed.
assert.equal(seen.size, 12);
});
13 changes: 13 additions & 0 deletions scripts/exports.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,21 @@ test('prose-shaped control ids do not spread beyond the known set', () => {
const registries = fs.readdirSync(fwDir).filter((f) => f.endsWith('.json'))
.map((f) => JSON.parse(fs.readFileSync(path.join(fwDir, f), 'utf8')));

// compliance-report.js resolves --framework from the names the mappings cite,
// so a registry no mapping references has no catalog to export and errors out.
// Such a registry emits no OSCAL, so it cannot carry a prose id; skip it
// rather than exempting any framework that does produce one.
const entryDir = path.join(ROOT, 'data', 'entries');
const mapped = new Set();
for (const f of fs.readdirSync(entryDir).filter((n) => n.endsWith('.json'))) {
for (const m of JSON.parse(fs.readFileSync(path.join(entryDir, f), 'utf8')).mappings || []) {
mapped.add(m.framework);
}
}

const counted = {};
for (const reg of registries) {
if (!mapped.has(reg.name)) continue;
// `--framework` is a partial match, so one call can emit several documents.
const raw = execFileSync(process.execPath, [
path.join(ROOT, 'scripts', 'compliance-report.js'),
Expand Down