diff --git a/README.md b/README.md
index 652b9fa..f6ac142 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ On every push or PR, the scanner produces:
| `incident-response-plan.md` | `docs/policies/` | NIST SP 800-61 based IRP |
| `security.txt` | `.well-known/` | RFC 9116 security contact file |
| `risk-assessment.md` | `.grc/` | Likelihood x impact matrix with framework mappings |
-| `nist-csf-report.md` | `.grc/` | 18 NIST CSF controls with SOC 2 + ISO 27001 cross-mapping |
+| `nist-csf-report.md` | `.grc/` | 18 NIST CSF 2.0 subcategories (across all six functions — Govern, Identify, Protect, Detect, Respond, Recover) with SOC 2 TSC 2017 (rev. 2022) + ISO/IEC 27001:2022 cross-mapping |
| `security-headers-report.md` | `.grc/` | Header status + starter-snippet fixes (CSP typically needs manual review) |
| `access-controls-report.md` | `.grc/` | Branch protection and auth findings |
@@ -156,7 +156,7 @@ Policies live at `docs/policies/` and `.well-known/security.txt` - those DO get
The dashboard shows compliance posture across all your repos:
-- Org-wide stats (compliance %, NIST CSF %, vulnerabilities, secrets)
+- Org-wide stats (score vs. mapped controls, NIST CSF coverage, vulnerabilities, secrets)
- Per-repo detail with data collection, headers, TLS, deps, access controls, artifacts
- NIST CSF tab with per-function scores and SOC 2 / ISO 27001 cross-references
- **AI tab** with detected AI systems (provider, SDK, category), risk tier, and data flows
@@ -271,7 +271,7 @@ GRC-Observability-Dashboard/
index.ts # Scanner entry point
rules/ # Detection rules
generators/ # Report generators
- frameworks/ # NIST CSF + SOC 2 + ISO 27001 cross-mappings
+ frameworks/ # NIST CSF 2.0 + EU AI Act + cross-mappings to SOC 2 TSC / ISO 27001:2022 / ISO/IEC 42001:2023 / NIST AI RMF
templates/ # Handlebars policy templates
ai/ # Optional AI enhancement layer
examples/
diff --git a/dashboard/views/render.ts b/dashboard/views/render.ts
index 53f3d32..313930c 100644
--- a/dashboard/views/render.ts
+++ b/dashboard/views/render.ts
@@ -1311,7 +1311,7 @@ export function renderInventoryView(rows: InventoryRow[], orgName: string = ""):
tableHtml += ``;
}
tableHtml += ``;
- tableHtml += `
Inventory scope. This view aggregates AI systems across every repo currently scanned by the dashboard. Rows marked with \u2605 have their risk tier set by an explicit override in .grc/config.yml; other rows are heuristic classifications. Use. This list is intended as the source document for EU AI Act Article 60 registration of high-risk systems and for auditor evidence packages. Export as CSV via the button above.
`;
+ tableHtml += `Inventory scope. This view aggregates AI systems across every repo currently scanned by the dashboard. Rows marked with \u2605 have their risk tier set by an explicit override in .grc/config.yml; other rows are heuristic classifications. Use. This list is intended as an internal AI systems inventory feeding the EU AI Act Article 49 / Article 26(8) registration flow (EU database established by Article 71) and for auditor evidence packages. Export as CSV via the button above.
`;
tableHtml += ``;
}
diff --git a/dashboard/worker.ts b/dashboard/worker.ts
index 5c7626e..a214a6e 100644
--- a/dashboard/worker.ts
+++ b/dashboard/worker.ts
@@ -237,7 +237,7 @@ function calcNistScore(results: ReturnType): number {
}
export function getNistFunctionScores(results: ReturnType): FunctionScore[] {
- return ["Identify", "Protect", "Detect", "Respond", "Recover"].map(fn => {
+ return ["Govern", "Identify", "Protect", "Detect", "Respond", "Recover"].map(fn => {
const controls = results.filter(r => r.control.function === fn);
const applicable = controls.filter(r => r.status !== "not-applicable");
const passed = applicable.filter(r => r.status === "pass").length;
diff --git a/docs/grc-fundamentals.md b/docs/grc-fundamentals.md
index 41e1d1f..662dad4 100644
--- a/docs/grc-fundamentals.md
+++ b/docs/grc-fundamentals.md
@@ -10,30 +10,35 @@ Quick reference for core GRC concepts as they relate to this project.
## Key Frameworks
-### NIST Cybersecurity Framework (CSF)
-Five core functions:
-1. **Identify** — Know your assets, data, and risks
-2. **Protect** — Implement safeguards (access controls, encryption, training)
-3. **Detect** — Monitor for anomalies and incidents
-4. **Respond** — Have a plan for when incidents occur
-5. **Recover** — Restore capabilities after an incident
+### NIST Cybersecurity Framework (CSF) 2.0
+Published February 2024 (NIST.CSWP.29). **Six** core functions — the previous five functions, plus `Govern` newly promoted to the organizational core:
+1. **Govern (GV)** — Establish and monitor cybersecurity risk management strategy, expectations, and policy (NEW in 2.0)
+2. **Identify (ID)** — Know your assets, data, dependencies, and risks
+3. **Protect (PR)** — Implement safeguards (identity, access, data security, platform security, training)
+4. **Detect (DE)** — Monitor for adverse events
+5. **Respond (RS)** — Manage and communicate during incidents
+6. **Recover (RC)** — Restore assets and operations after an incident
+
+Subcategory IDs in CSF 2.0 are zero-padded (e.g. `ID.AM-01`, `PR.AA-01`); older `ID.AM-1`-style IDs are CSF 1.1.
### SOC 2
-Five Trust Service Criteria:
-1. **Security** — Protection against unauthorized access
+The AICPA's Trust Services Criteria, 2017 edition (revised 2022). Five Trust Services Categories:
+1. **Security** — Protection against unauthorized access (mandatory baseline)
2. **Availability** — System is available for operation and use
3. **Processing Integrity** — System processing is complete, valid, accurate
4. **Confidentiality** — Information designated as confidential is protected
5. **Privacy** — Personal information is collected, used, retained properly
-Type I = controls exist at a point in time. Type II = controls work over a period (usually 6-12 months).
+Type I = a point-in-time attestation that controls exist and are designed appropriately. Type II = a period-of-time attestation (usually 3-12 months) that controls operated effectively.
+
+### ISO/IEC 27001:2022
+International standard for Information Security Management Systems (ISMS), 2022 revision. Annex A lists 93 controls organised into four themes:
+- `A.5` Organizational (37 controls)
+- `A.6` People (8 controls)
+- `A.7` Physical (14 controls)
+- `A.8` Technological (34 controls)
-### ISO 27001
-International standard for Information Security Management Systems (ISMS). Has ~93 controls across:
-- Organizational controls
-- People controls
-- Physical controls
-- Technological controls
+Codes use two components (e.g. `A.5.23`, `A.8.9`). The 2013 four-component form (`A.8.1.1`, `A.12.6.1`) is obsolete — the transition ended October 2025.
### GDPR (General Data Protection Regulation)
EU regulation. Key requirements:
diff --git a/docs/implementation-checklist.md b/docs/implementation-checklist.md
index a77961a..a91636c 100644
--- a/docs/implementation-checklist.md
+++ b/docs/implementation-checklist.md
@@ -104,15 +104,15 @@ The single source of truth for the GRC Observability Dashboard roadmap. Each ite
- **GRC concept:** Risk treatment options (accept, mitigate, transfer, avoid)
### Item 11: Framework Mapping — DONE
-- [x] NIST CSF 2.0 as primary framework (18 controls mapped)
-- [x] Each scan check maps to NIST CSF subcategories with pass/partial/fail/N/A evaluation
-- [x] Per-function compliance percentages (Identify, Protect, Detect, Respond, Recover)
-- [x] Cross-mapped to SOC 2 Trust Service Criteria (12 controls)
-- [x] Cross-mapped to ISO 27001 Annex A (22 controls)
+- [x] NIST CSF 2.0 as primary framework — 18 subcategories mapped (NIST.CSWP.29, February 2024)
+- [x] Each scan check maps to CSF 2.0 subcategory IDs (`GV.PO-01`, `ID.AM-01`, `PR.AA-01`, etc.) with pass/partial/fail/N/A evaluation
+- [x] Per-function scores across all six CSF 2.0 functions: **Govern, Identify, Protect, Detect, Respond, Recover**
+- [x] Cross-mapped to SOC 2 Trust Services Criteria, 2017 edition (revised 2022)
+- [x] Cross-mapped to ISO/IEC 27001:2022 Annex A (two-component IDs matching the current standard; 2013's four-component codes are obsolete)
- [x] Evidence strings for every control assessment
- [x] Gaps section highlighting failures with specific evidence
- **GRC concept:** Control frameworks, control objectives, evidence collection
-- **Known limitation:** 18 of NIST CSF 2.0's ~100 subcategories. "75% NIST CSF compliant" is 75% of our 18 controls, not the full framework.
+- **Known limitation:** 18 of NIST CSF 2.0's ~106 subcategories are evaluated. The overall score reports % of mapped controls passing — it is **not** a claim of full framework compliance. Report titles use "Coverage Report" / "Assessment Report" rather than "Compliance Report" for this reason.
## Phase 4: AI Enhancement Layer — VALIDATED
@@ -262,7 +262,7 @@ Optional module — scanner works fully without AI. If an API key is provided, A
## Phase 8: AI Compliance Layer — DONE
-**Why this direction:** The EU AI Act becomes enforceable August 2026 with fines up to €35M or 7% of global turnover. The scanner already detects AI SDK usage via dependency scanning but does nothing AI-compliance-specific with those findings. This phase turns "security compliance scanner" into "security + AI compliance scanner."
+**Why this direction:** The EU AI Act (Regulation (EU) 2024/1689) applies in staggered phases under Article 113 — prohibitions from 2 Feb 2025, governance and GPAI from 2 Aug 2025, most high-risk provisions from 2 Aug 2026, and Article 6(1) products embedded in harmonized legislation from 2 Aug 2027. Article 99 sets tiered fines: up to €35M or 7% of worldwide annual turnover for Article 5 prohibited practices; €15M or 3% for most other obligations; €7.5M or 1% for supplying incorrect information. The scanner already detects AI SDK usage via dependency scanning but did nothing AI-compliance-specific with those findings. This phase turns "security compliance scanner" into "security + AI compliance scanner."
**Our own meta-obligation:** The scanner uses Anthropic/OpenAI in its AI layer. That makes the dashboard itself an "AI system" under the EU AI Act. When we ship this, the scanner should scan itself and produce its own AI compliance documentation.
@@ -306,12 +306,12 @@ Optional module — scanner works fully without AI. If an API key is provided, A
### Sub-phase C: EU AI Act Framework Mapping — DONE
- [x] New framework file `scanner/frameworks/eu-ai-act.ts` with 13 articles as `AIFrameworkControl` entries and per-article `check(manifest)` / `evidence(manifest)` functions
-- [x] Covers Articles 4 (AI literacy), 5 (prohibited), 9 (risk management), 10 (data governance), 11 (technical docs), 12 (record keeping), 13 (transparency), 14 (human oversight), 15 (accuracy/robustness), 27 (FRIA), 50 (transparency to users), 60 (registration), 73 (incident notification)
+- [x] Covers Articles 4 (AI literacy), 5 (prohibited practices), 9 (risk management), 10 (data governance), 11 (technical documentation), 12 (record-keeping), 13 (transparency to deployers), 14 (human oversight), 15 (accuracy / robustness / cybersecurity), 27 (FRIA), 50 (transparency to users), **71 (EU database for Annex III high-risk AI)** — registration obligations live in Articles 49 (providers) and 26(8) (public-sector deployers) — and 73 (serious incident reporting)
- [x] Articles grouped by NIST AI RMF phase (Govern/Map/Measure/Manage) to parallel NIST CSF's 5-function structure
- [x] Extended `scanner/frameworks/cross-map.ts` with `AICrossMapping` + `AI_CROSS_MAPPINGS` — each article cross-referenced to NIST AI RMF subcategories and ISO/IEC 42001 Annex A controls
- [x] New report generator `scanner/generators/ai-compliance-report.ts` written to `.grc/ai-compliance-report.md` on every scan (mirrors NIST CSF report layout)
- [x] `evaluateEUAIAct(manifest)`, `calcAIComplianceScore(results)`, and `getAIPhaseScores(results)` exported for dashboard consumption
-- [x] Risk-tier-based applicability: high-risk-only articles (9/11/12/13/14/15/27/60/73) show `not-applicable` unless a `high`/`prohibited` system is detected. Article 5 always applies. Article 50 applies at `limited` and above. Articles 27 and 60 additionally require `eu_market: true`.
+- [x] Risk-tier-based applicability: high-risk-only articles (9/11/12/13/14/15/27/71/73) show `not-applicable` unless a `high`/`prohibited` system is detected. Article 5 always applies. Article 50 applies at `limited` and above. Articles 27 (FRIA) and 71 (EU database) additionally require `eu_market: true`; Article 27 further requires a specific deployer type (public authority, private provider of public services, or Annex III 5(b)/(c) credit/insurance deployer).
- [x] `euMarket` field added to `AISystem`; propagated at classifier time from the `ai_systems:` override or defaulted from `jurisdiction` (GDPR → EU market by default)
- **GRC concept:** Framework pluralism — same findings, multiple framework views
- **Known limitation:** Many articles resolve to `partial` with instructional evidence because they are program-level obligations the scanner cannot auto-verify (AI literacy training, runtime logging, registration status). The tool surfaces the obligation; closing it is off-scanner.
diff --git a/scanner/frameworks/cross-map.ts b/scanner/frameworks/cross-map.ts
index 938730d..91a3ab9 100644
--- a/scanner/frameworks/cross-map.ts
+++ b/scanner/frameworks/cross-map.ts
@@ -1,54 +1,78 @@
/**
- * Cross-mapping from NIST CSF controls to SOC 2 and ISO 27001.
- * Each NIST CSF control maps to equivalent controls in other frameworks.
+ * Cross-mappings from NIST CSF 2.0 subcategories to:
+ *
+ * - SOC 2 Trust Services Criteria, 2017 edition (revised 2022), published
+ * by the AICPA. Codes use the standard `CCx.y` format for the Common
+ * Criteria and `A1` / `C1` / `PI1` for Availability / Confidentiality /
+ * Processing Integrity where applicable.
+ *
+ * - ISO/IEC 27001:2022 Annex A. This is the current standard (the 2013
+ * transition ended October 2025) with 93 controls organized into four
+ * themes: A.5 Organizational, A.6 People, A.7 Physical, A.8
+ * Technological. IDs use the two-component form (e.g. `A.5.23`,
+ * `A.8.9`) — the four-component form `A.X.Y.Z` from :2013 is obsolete.
+ *
+ * These mappings are illustrative — "this subcategory's intent is tracked
+ * by these external controls" — not a formal equivalence declaration.
*/
export interface CrossMapping {
- nistId: string;
+ csfId: string;
soc2: string[];
iso27001: string[];
}
export const CROSS_MAPPINGS: CrossMapping[] = [
+ // GOVERN
+ { csfId: "GV.PO-01", soc2: ["CC1.1", "CC1.2", "CC5.3"], iso27001: ["A.5.1"] },
+ { csfId: "GV.SC-01", soc2: ["CC9.2"], iso27001: ["A.5.19", "A.5.20", "A.5.21", "A.5.22"] },
+
// IDENTIFY
- { nistId: "ID.AM-1", soc2: ["CC6.1"], iso27001: ["A.8.1.1"] },
- { nistId: "ID.AM-2", soc2: ["CC6.1"], iso27001: ["A.8.1.1", "A.12.5.1"] },
- { nistId: "ID.GV-1", soc2: ["CC1.1", "CC1.2"], iso27001: ["A.5.1.1", "A.5.1.2"] },
- { nistId: "ID.RA-1", soc2: ["CC3.2", "CC7.1"], iso27001: ["A.12.6.1"] },
- { nistId: "ID.RA-2", soc2: ["CC3.2"], iso27001: ["A.6.1.4"] },
+ { csfId: "ID.AM-01", soc2: ["CC6.1"], iso27001: ["A.5.9"] },
+ { csfId: "ID.AM-02", soc2: ["CC6.1"], iso27001: ["A.5.9", "A.8.19"] },
+ { csfId: "ID.RA-01", soc2: ["CC3.2", "CC7.1"], iso27001: ["A.8.8"] },
+ { csfId: "ID.RA-02", soc2: ["CC3.2"], iso27001: ["A.5.6"] },
+ { csfId: "ID.RA-08", soc2: ["CC7.3"], iso27001: ["A.5.24", "A.6.8"] },
+ { csfId: "ID.IM-02", soc2: ["CC4.1", "CC8.1"], iso27001: ["A.8.8"] },
// PROTECT
- { nistId: "PR.AC-1", soc2: ["CC6.1", "CC6.2"], iso27001: ["A.9.2.1", "A.9.4.3"] },
- { nistId: "PR.AC-4", soc2: ["CC6.1", "CC6.3"], iso27001: ["A.9.1.2", "A.9.4.1"] },
- { nistId: "PR.DS-1", soc2: ["CC6.1", "CC6.7"], iso27001: ["A.8.2.3", "A.10.1.1"] },
- { nistId: "PR.DS-2", soc2: ["CC6.1", "CC6.7"], iso27001: ["A.10.1.1", "A.13.1.1"] },
- { nistId: "PR.DS-5", soc2: ["CC6.1"], iso27001: ["A.13.1.1", "A.13.1.3"] },
- { nistId: "PR.IP-1", soc2: ["CC7.1", "CC8.1"], iso27001: ["A.12.1.2", "A.14.2.2"] },
- { nistId: "PR.IP-12", soc2: ["CC7.1"], iso27001: ["A.12.6.1"] },
+ { csfId: "PR.AA-01", soc2: ["CC6.1", "CC6.2"], iso27001: ["A.5.16", "A.5.17"] },
+ { csfId: "PR.AA-05", soc2: ["CC6.1", "CC6.3"], iso27001: ["A.5.15", "A.8.3"] },
+ { csfId: "PR.DS-01", soc2: ["CC6.1", "CC6.7"], iso27001: ["A.5.10", "A.8.24"] },
+ { csfId: "PR.DS-02", soc2: ["CC6.1", "CC6.7"], iso27001: ["A.8.24", "A.8.20"] },
+ { csfId: "PR.DS-10", soc2: ["CC6.1"], iso27001: ["A.8.20", "A.8.22", "A.8.23"] },
+ { csfId: "PR.PS-01", soc2: ["CC7.1", "CC8.1"], iso27001: ["A.8.9"] },
// DETECT
- { nistId: "DE.CM-4", soc2: ["CC7.1"], iso27001: ["A.12.2.1"] },
- { nistId: "DE.CM-8", soc2: ["CC7.1"], iso27001: ["A.12.6.1"] },
+ { csfId: "DE.CM-09", soc2: ["CC7.1"], iso27001: ["A.8.7", "A.8.16"] },
// RESPOND
- { nistId: "RS.RP-1", soc2: ["CC7.3", "CC7.4"], iso27001: ["A.16.1.1", "A.16.1.5"] },
- { nistId: "RS.CO-2", soc2: ["CC7.3"], iso27001: ["A.16.1.2"] },
+ { csfId: "RS.MA-01", soc2: ["CC7.3", "CC7.4"], iso27001: ["A.5.24", "A.5.26"] },
+ { csfId: "RS.CO-02", soc2: ["CC7.3"], iso27001: ["A.5.25", "A.6.8"] },
// RECOVER
- { nistId: "RC.RP-1", soc2: ["CC7.5"], iso27001: ["A.17.1.1"] },
- { nistId: "RC.IM-1", soc2: ["CC7.5"], iso27001: ["A.16.1.6"] },
+ { csfId: "RC.RP-01", soc2: ["CC7.5"], iso27001: ["A.5.29", "A.5.30"] },
];
-export function getCrossMapping(nistId: string): CrossMapping | undefined {
- return CROSS_MAPPINGS.find(m => m.nistId === nistId);
+export function getCrossMapping(csfId: string): CrossMapping | undefined {
+ return CROSS_MAPPINGS.find(m => m.csfId === csfId);
}
/**
- * Cross-mapping from EU AI Act articles to NIST AI RMF (AI 100-1)
- * subcategories and ISO/IEC 42001:2023 Annex A controls. Mappings are
- * drawn from the published crosswalks maintained by NIST and ISO — they
- * are illustrative ("this article's intent is tracked by these external
- * controls"), not a formal equivalence.
+ * Cross-mapping from EU AI Act articles to:
+ *
+ * - NIST AI Risk Management Framework (NIST AI 100-1, v1.0, January
+ * 2023) subcategories. The AI RMF structures its 72 subcategories
+ * under four functions: GOVERN, MAP, MEASURE, MANAGE.
+ *
+ * - ISO/IEC 42001:2023 Annex A — AI management system controls across
+ * 10 objectives (A.2 through A.10).
+ *
+ * These are illustrative cross-references drawing on the NIST AI RMF
+ * crosswalk publications and ISO/IEC 42001 alignment guidance. They are
+ * not an authoritative regulatory mapping — EU AI Act obligations are
+ * distinct legal requirements; AI RMF and 42001 provide complementary
+ * governance programs that happen to cover overlapping concerns.
*/
export interface AICrossMapping {
aiActId: string;
@@ -57,26 +81,30 @@ export interface AICrossMapping {
}
export const AI_CROSS_MAPPINGS: AICrossMapping[] = [
- // GOVERN
- { aiActId: "ART-4", nistAiRmf: ["GOVERN 2.2", "GOVERN 3.2"], iso42001: ["A.3.2", "A.4.2"] },
- { aiActId: "ART-9", nistAiRmf: ["GOVERN 1.4", "MAP 5.1", "MANAGE 1.3"], iso42001: ["A.5.2", "A.5.4", "A.6.1.2"] },
- { aiActId: "ART-10", nistAiRmf: ["MAP 2.3", "MEASURE 2.2"], iso42001: ["A.7.2", "A.7.3", "A.7.4"] },
+ // GOVERN phase
+ { aiActId: "ART-4", nistAiRmf: ["GOVERN 2.2", "GOVERN 3.2"], iso42001: ["A.3.2", "A.4.2"] },
+ { aiActId: "ART-9", nistAiRmf: ["GOVERN 1.4", "MAP 5.1", "MANAGE 1.3"], iso42001: ["A.5.2", "A.5.4", "A.6.1.2"] },
+ { aiActId: "ART-10", nistAiRmf: ["MAP 2.3", "MEASURE 2.2"], iso42001: ["A.7.2", "A.7.3", "A.7.4"] },
- // MAP
- { aiActId: "ART-5", nistAiRmf: ["GOVERN 1.1", "MAP 1.1"], iso42001: ["A.5.3", "A.6.1.2"] },
- { aiActId: "ART-11", nistAiRmf: ["MAP 4.1", "MEASURE 1.3"], iso42001: ["A.6.2.2", "A.6.2.3"] },
+ // MAP phase
+ { aiActId: "ART-5", nistAiRmf: ["GOVERN 1.1", "MAP 1.1"], iso42001: ["A.5.3", "A.6.1.2"] },
+ { aiActId: "ART-11", nistAiRmf: ["MAP 4.1", "MEASURE 1.3"], iso42001: ["A.6.2.2", "A.6.2.3"] },
- // MEASURE
- { aiActId: "ART-12", nistAiRmf: ["MEASURE 2.8", "MANAGE 4.1"], iso42001: ["A.6.2.8", "A.8.4"] },
- { aiActId: "ART-15", nistAiRmf: ["MEASURE 2.5", "MEASURE 2.7"], iso42001: ["A.6.2.4", "A.8.2"] },
- { aiActId: "ART-27", nistAiRmf: ["MAP 5.2", "MEASURE 3.2"], iso42001: ["A.5.5", "A.8.3"] },
+ // MEASURE phase
+ { aiActId: "ART-12", nistAiRmf: ["MEASURE 2.8", "MANAGE 4.1"], iso42001: ["A.6.2.8", "A.8.4"] },
+ { aiActId: "ART-15", nistAiRmf: ["MEASURE 2.5", "MEASURE 2.7"], iso42001: ["A.6.2.4", "A.8.2"] },
+ { aiActId: "ART-27", nistAiRmf: ["MAP 5.2", "MEASURE 3.2"], iso42001: ["A.5.5", "A.8.3"] },
- // MANAGE
- { aiActId: "ART-13", nistAiRmf: ["GOVERN 4.2", "MANAGE 3.1"], iso42001: ["A.6.2.6", "A.8.1"] },
- { aiActId: "ART-14", nistAiRmf: ["MEASURE 2.6", "MANAGE 2.1"], iso42001: ["A.6.2.7", "A.9.2"] },
- { aiActId: "ART-50", nistAiRmf: ["GOVERN 5.1", "MANAGE 3.2"], iso42001: ["A.8.1", "A.9.3"] },
- { aiActId: "ART-60", nistAiRmf: ["GOVERN 4.1"], iso42001: ["A.2.3"] },
- { aiActId: "ART-73", nistAiRmf: ["MANAGE 4.3"], iso42001: ["A.8.5", "A.10.3"] },
+ // MANAGE phase
+ { aiActId: "ART-13", nistAiRmf: ["GOVERN 4.2", "MANAGE 3.1"], iso42001: ["A.6.2.6", "A.8.1"] },
+ { aiActId: "ART-14", nistAiRmf: ["MEASURE 2.6", "MANAGE 2.1"], iso42001: ["A.6.2.7", "A.9.2"] },
+ { aiActId: "ART-50", nistAiRmf: ["GOVERN 5.1", "MANAGE 3.2"], iso42001: ["A.8.1", "A.9.3"] },
+ // ART-71 is the actual EU AI Act article establishing the EU database
+ // for high-risk AI systems listed in Annex III. The registration
+ // obligations themselves are split across Article 49 (providers) and
+ // Article 26(8) (deployers of certain public-sector systems).
+ { aiActId: "ART-71", nistAiRmf: ["GOVERN 4.1"], iso42001: ["A.2.3"] },
+ { aiActId: "ART-73", nistAiRmf: ["MANAGE 4.3"], iso42001: ["A.8.5", "A.10.3"] },
];
export function getAICrossMapping(aiActId: string): AICrossMapping | undefined {
diff --git a/scanner/frameworks/eu-ai-act.test.ts b/scanner/frameworks/eu-ai-act.test.ts
index 9c0ac18..a3c9bad 100644
--- a/scanner/frameworks/eu-ai-act.test.ts
+++ b/scanner/frameworks/eu-ai-act.test.ts
@@ -144,14 +144,20 @@ describe("evaluateEUAIAct", () => {
expect(r.status).toBe("not-applicable");
});
- it("fails when EU-market high-risk exists and no FRIA artifact", () => {
+ it("is partial-for-review when EU-market high-risk exists (Art. 27 scope depends on deployer type)", () => {
+ // Article 27(1) narrows FRIA to public authorities, private providers
+ // of public services, and Annex III 5(b)/(c) deployers. The scanner
+ // can't tell those apart from the manifest, so it reports "partial"
+ // asking for human scope verification rather than "fail" on every
+ // high-risk EU system.
const r = evaluateEUAIAct(
baseManifest({ aiSystems: [aiSystem({ riskTier: "high", euMarket: true })] }),
).find(x => x.articleId === "ART-27")!;
- expect(r.status).toBe("fail");
+ expect(r.status).toBe("partial");
+ expect(r.evidence).toMatch(/public authority|public service|credit|insurance|Article 27/i);
});
- it("is partial when EU-market high-risk exists and FRIA artifact is present", () => {
+ it("stays partial when EU-market high-risk exists and FRIA artifact is present (template alone isn't sign-off)", () => {
const r = evaluateEUAIAct(
baseManifest({
aiSystems: [aiSystem({ riskTier: "high", euMarket: true })],
diff --git a/scanner/frameworks/eu-ai-act.ts b/scanner/frameworks/eu-ai-act.ts
index 1a0780f..f17887a 100644
--- a/scanner/frameworks/eu-ai-act.ts
+++ b/scanner/frameworks/eu-ai-act.ts
@@ -121,15 +121,15 @@ export const EU_AI_ACT_CONTROLS: AIFrameworkControl[] = [
article: 5,
title: "Prohibited AI practices",
phase: "Map",
- description: "Placing on the market or using AI for social scoring, subliminal manipulation, real-time biometric identification in public, or exploiting vulnerabilities is prohibited.",
+ description: "Article 5(1) prohibits eight categories of AI practice: (a) subliminal / manipulative techniques causing significant harm; (b) exploitation of vulnerabilities of specific groups; (c) social scoring by public or private actors leading to detrimental treatment; (d) predictive policing based solely on profiling; (e) untargeted scraping of facial images to build recognition databases; (f) emotion inference in workplace or educational institutions; (g) biometric categorization inferring sensitive attributes; (h) real-time remote biometric identification in public spaces by law enforcement (with narrow exceptions).",
check: (m) => prohibitedSystems(m).length > 0 ? "fail" : "pass",
evidence: (m) => {
const p = prohibitedSystems(m);
if (p.length > 0) {
const locs = p.map(s => `${s.provider} @ ${s.location}`).join("; ");
- return `Flagged as potentially prohibited: ${locs}. Verify purpose; if misclassified, override in .grc/config.yml via ai_systems.risk_tier. If correct, placing this on the EU market is prohibited.`;
+ return `Flagged as potentially prohibited: ${locs}. Verify which of Article 5(1)(a)–(h) applies; if the scanner misclassified, override in .grc/config.yml via ai_systems.risk_tier. If the classification is correct, the system cannot be placed on the EU market.`;
}
- if (hasAny(m)) return "No AI systems classified as prohibited.";
+ if (hasAny(m)) return "No AI systems classified as prohibited under Article 5(1)(a)–(h).";
return "No AI systems detected.";
},
},
@@ -168,7 +168,7 @@ export const EU_AI_ACT_CONTROLS: AIFrameworkControl[] = [
},
evidence: (m) => {
if (highRiskSystems(m).length === 0) return "No high-risk AI systems detected.";
- return "Automatic event logging is required for high-risk AI systems (six-month minimum retention recommended by guidance). Scanner cannot verify runtime logging — confirm your AI call paths emit structured logs with input/output hashes, user ID, and timestamps.";
+ return "Automatic event logging over the system's operational lifetime is required by Article 12(1). Retention is set by Article 12(3) (appropriate to the intended purpose) and, for providers, cross-referenced with Article 19's separate 10-year documentation retention obligation. The scanner cannot verify runtime logging — confirm AI call paths emit structured logs with request hashes, user identifiers, timestamps, and outcome classifications.";
},
},
@@ -212,19 +212,23 @@ export const EU_AI_ACT_CONTROLS: AIFrameworkControl[] = [
article: 27,
title: "Fundamental Rights Impact Assessment (FRIA)",
phase: "Measure",
- description: "Deployers of high-risk AI placed on the EU market in certain sectors must perform a FRIA before first use.",
+ description: "Article 27(1) requires a FRIA only from specific categories of deployer: (a) bodies governed by public law; (b) private entities providing public services; or (c) deployers of certain Annex III point 5 systems (credit scoring under 5(b); risk assessment and pricing for life/health insurance under 5(c)). A general commercial deployer of an Annex III high-risk system is not automatically in scope for Article 27.",
check: (m) => {
if (euMarketHighRisk(m).length === 0) return "not-applicable";
+ // We don't know the deployer's legal nature from the scan, so this
+ // check flags for review rather than failing outright. The template
+ // artifact nudges the deployer to complete the scoping themselves.
if (m.artifacts.fria === "present") return "partial";
- return "fail";
+ return "partial";
},
evidence: (m) => {
const scoped = euMarketHighRisk(m);
if (scoped.length === 0) return "No EU-market high-risk AI systems detected — FRIA not required.";
+ const base = `High-risk systems on the EU market: ${listProviders(scoped)}. Whether Article 27 applies depends on the deployer: public authority, private provider of a public service, or credit/insurance-specific Annex III deployer.`;
if (m.artifacts.fria === "present") {
- return `FRIA template generated at fria.md for EU-market high-risk systems: ${listProviders(scoped)}. The template contains placeholder sections (affected persons, specific risks of harm, oversight measures) that must be completed by a human and signed before first use.`;
+ return `${base} FRIA template generated at fria.md — the deployer must first confirm Article 27 scope, then complete the placeholder sections (affected persons, specific risks, oversight measures) and sign before first use.`;
}
- return `FRIA required for EU-market high-risk systems: ${listProviders(scoped)}. Scanner will generate a fria.md template on the next run.`;
+ return `${base} If in scope, a FRIA is required before first use; the scanner will generate a fria.md template on the next run.`;
},
},
@@ -296,11 +300,11 @@ export const EU_AI_ACT_CONTROLS: AIFrameworkControl[] = [
},
{
- id: "ART-60",
- article: 60,
- title: "EU database registration",
+ id: "ART-71",
+ article: 71,
+ title: "EU database registration (Annex III high-risk)",
phase: "Manage",
- description: "Providers of stand-alone high-risk AI must register the system in the EU public database before placing on the market.",
+ description: "Article 71 establishes the EU database for high-risk AI systems listed in Annex III. The underlying registration obligations live in Article 49 (providers, before placing on the market) and Article 26(8) (deployers that are public authorities, agencies of the Union, or bodies governed by public law, before first use).",
check: (m) => {
if (euMarketHighRisk(m).length === 0) return "not-applicable";
return "fail";
@@ -308,7 +312,7 @@ export const EU_AI_ACT_CONTROLS: AIFrameworkControl[] = [
evidence: (m) => {
const scoped = euMarketHighRisk(m);
if (scoped.length === 0) return "No EU-market high-risk AI systems detected — registration not required.";
- return `Registration in the EU database is required before market placement for: ${listProviders(scoped)}. Scanner cannot verify registration — track out-of-band and record the registration ID in your compliance documentation.`;
+ return `Registration in the EU database (Article 71) is required for: ${listProviders(scoped)}. Providers register under Article 49 before market placement; certain deployers register under Article 26(8) before first use. Scanner cannot verify registration — track out-of-band and record the registration ID in your compliance documentation.`;
},
},
@@ -328,7 +332,7 @@ export const EU_AI_ACT_CONTROLS: AIFrameworkControl[] = [
if (m.artifacts.incidentResponsePlan === "missing") {
return "No incident response plan present — required baseline for serious-incident reporting under Article 73.";
}
- return `Incident response plan: ${m.artifacts.incidentResponsePlan}. Verify it includes AI-specific incident types (hallucination causing harm, discrimination, unexpected autonomy) and the market-surveillance-authority notification timeline (without undue delay, in any event within 15 days).`;
+ return `Incident response plan: ${m.artifacts.incidentResponsePlan}. Verify it includes AI-specific incident types (hallucination causing harm, discrimination, unexpected autonomy) and the tiered Article 73(4) reporting deadlines: at the latest 15 days for serious incidents; 10 days when an incident led to a person's death; 2 days for widespread infringement or serious and irreversible disruption of critical infrastructure.`;
},
},
];
diff --git a/scanner/frameworks/nist-csf.ts b/scanner/frameworks/nist-csf.ts
index 805c255..e6e8688 100644
--- a/scanner/frameworks/nist-csf.ts
+++ b/scanner/frameworks/nist-csf.ts
@@ -1,15 +1,33 @@
/**
- * NIST Cybersecurity Framework (CSF) 2.0 mapping
+ * NIST Cybersecurity Framework (CSF) 2.0 mapping.
*
- * Maps scanner checks to NIST CSF subcategories.
- * Each control has a check function that evaluates the manifest to determine compliance.
+ * Subcategory IDs, function list, and category names follow NIST CSF 2.0 as
+ * published 26 February 2024 (NIST.CSWP.29). Key differences from CSF 1.1
+ * that are reflected here:
+ *
+ * - A sixth function, GOVERN (GV), was added and is typed first in the
+ * union because CSF 2.0 positions it as the organizational core.
+ * - Subcategory identifiers are zero-padded (e.g. `ID.AM-01`, not
+ * `ID.AM-1`).
+ * - The PR.AC (Access Control) category was renamed to PR.AA (Identity
+ * Management, Authentication, and Access Control). PR.IP (Information
+ * Protection Processes) was dissolved — configuration baseline moved
+ * to PR.PS (Platform Security); vulnerability-management planning
+ * moved to ID.IM (Improvement).
+ * - DE.CM-4/DE.CM-8 consolidated into DE.CM-09 (monitoring of hardware,
+ * software, runtime environments, and their data).
+ * - Supply chain risk management now has its own category, GV.SC.
+ *
+ * This scanner evaluates 18 subcategories. That is a small slice of CSF
+ * 2.0's ~106 total subcategories — "X% of our mapped controls" is always
+ * the honest framing; full-framework compliance would need the other 88.
*/
import { Manifest } from "../types.js";
export interface FrameworkControl {
id: string;
- function: "Identify" | "Protect" | "Detect" | "Respond" | "Recover";
+ function: "Govern" | "Identify" | "Protect" | "Detect" | "Respond" | "Recover";
category: string;
subcategory: string;
description: string;
@@ -19,34 +37,14 @@ export interface FrameworkControl {
export const NIST_CSF_CONTROLS: FrameworkControl[] = [
// ═══════════════════════════════════════════
- // IDENTIFY (ID)
+ // GOVERN (GV) — new function in CSF 2.0
// ═══════════════════════════════════════════
{
- id: "ID.AM-1",
- function: "Identify",
- category: "Asset Management",
- subcategory: "Physical devices and systems are inventoried",
- description: "Infrastructure hosting is documented",
- check: (m) => m.https?.enforced !== undefined ? "pass" : "fail",
- evidence: (m) => m.https ? `Live site checked at scan time. HTTPS enforced: ${m.https.enforced}` : "No live site URL provided",
- },
- {
- id: "ID.AM-2",
- function: "Identify",
- category: "Asset Management",
- subcategory: "Software platforms and applications are inventoried",
- description: "Dependencies and third-party services are tracked",
- check: (m) => m.dependencies !== null ? "pass" : "fail",
- evidence: (m) => m.dependencies
- ? `${m.thirdPartyServices.length} third-party services identified. Dependency audit performed.`
- : "No dependency scan performed",
- },
- {
- id: "ID.GV-1",
- function: "Identify",
- category: "Governance",
- subcategory: "Organizational cybersecurity policy is established",
- description: "Security policies and governance documents exist",
+ id: "GV.PO-01",
+ function: "Govern",
+ category: "Policy",
+ subcategory: "Policy for managing cybersecurity risks is established based on organizational context, cybersecurity strategy, and priorities and is communicated and enforced",
+ description: "Governance and security policy documents exist",
check: (m) => {
const artifacts = m.artifacts;
const has = [
@@ -67,10 +65,55 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
},
},
{
- id: "ID.RA-1",
+ id: "GV.SC-01",
+ function: "Govern",
+ category: "Cybersecurity Supply Chain Risk Management",
+ subcategory: "A cybersecurity supply chain risk management program, strategy, objectives, policies, and processes are established and agreed to by organizational stakeholders",
+ description: "Third-party dependencies and services are inventoried and tracked",
+ check: (m) => {
+ // Both dependency scanning (direct deps + their advisories) and
+ // third-party-service inventory are precursors to a real supply-chain
+ // risk program. Having them enabled is necessary but not sufficient.
+ if (!m.dependencies) return "fail";
+ if (m.thirdPartyServices.length === 0 && (m.vulnerabilities?.length ?? 0) === 0) return "partial";
+ return "partial";
+ },
+ evidence: (m) => {
+ const services = m.thirdPartyServices.length;
+ const advisories = m.vulnerabilities?.length ?? 0;
+ if (!m.dependencies) return "No dependency scan performed — no supply-chain inventory.";
+ return `${services} third-party services identified, ${advisories} open advisories tracked. A supply-chain program additionally requires supplier agreements, risk tiering, and periodic review (not scanner-verifiable).`;
+ },
+ },
+
+ // ═══════════════════════════════════════════
+ // IDENTIFY (ID)
+ // ═══════════════════════════════════════════
+ {
+ id: "ID.AM-01",
+ function: "Identify",
+ category: "Asset Management",
+ subcategory: "Inventories of hardware managed by the organization are maintained",
+ description: "Infrastructure hosting is documented",
+ check: (m) => m.https?.enforced !== undefined ? "pass" : "fail",
+ evidence: (m) => m.https ? `Live site checked at scan time. HTTPS enforced: ${m.https.enforced}` : "No live site URL provided",
+ },
+ {
+ id: "ID.AM-02",
+ function: "Identify",
+ category: "Asset Management",
+ subcategory: "Inventories of software, services, and systems managed by the organization are maintained",
+ description: "Dependencies and third-party services are tracked",
+ check: (m) => m.dependencies !== null ? "pass" : "fail",
+ evidence: (m) => m.dependencies
+ ? `${m.thirdPartyServices.length} third-party services identified. Dependency audit performed.`
+ : "No dependency scan performed",
+ },
+ {
+ id: "ID.RA-01",
function: "Identify",
category: "Risk Assessment",
- subcategory: "Asset vulnerabilities are identified and documented",
+ subcategory: "Vulnerabilities in assets are identified, validated, and recorded",
description: "Dependency vulnerabilities are scanned",
check: (m) => {
if (!m.dependencies) return "fail";
@@ -82,23 +125,47 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
: "No dependency scan performed",
},
{
- id: "ID.RA-2",
+ id: "ID.RA-02",
function: "Identify",
category: "Risk Assessment",
- subcategory: "Cyber threat intelligence is received",
- description: "Known vulnerability databases are checked (npm audit, CVE)",
+ subcategory: "Cyber threat intelligence is received from information sharing forums and sources",
+ description: "Known vulnerability databases are checked (npm audit, GitHub Advisory Database)",
check: (m) => m.dependencies !== null ? "pass" : "fail",
evidence: (m) => m.dependencies ? `Last audit: ${m.dependencies.lastAudit}` : "No audit performed",
},
+ {
+ id: "ID.RA-08",
+ function: "Identify",
+ category: "Risk Assessment",
+ subcategory: "Processes for receiving, analyzing, and responding to vulnerability disclosures are established",
+ description: "Vulnerability disclosure process + security contact published",
+ check: (m) => {
+ const hasDisclosure = m.artifacts.vulnerabilityDisclosure !== "missing";
+ const hasSecTxt = m.artifacts.securityTxt !== "missing";
+ if (hasDisclosure && hasSecTxt) return "pass";
+ if (hasDisclosure || hasSecTxt) return "partial";
+ return "fail";
+ },
+ evidence: (m) => `security.txt: ${m.artifacts.securityTxt}, Vulnerability Disclosure: ${m.artifacts.vulnerabilityDisclosure}`,
+ },
+ {
+ id: "ID.IM-02",
+ function: "Identify",
+ category: "Improvement",
+ subcategory: "Security tests and exercises, including those done in coordination with suppliers and relevant third parties, are used to improve",
+ description: "Automated scanning runs on code changes",
+ check: (m) => m.dependencies !== null ? "pass" : "fail",
+ evidence: () => "GRC scanner runs on push/PR via GitHub Actions; findings feed back into the manifest as scan-time tests.",
+ },
// ═══════════════════════════════════════════
// PROTECT (PR)
// ═══════════════════════════════════════════
{
- id: "PR.AC-1",
+ id: "PR.AA-01",
function: "Protect",
- category: "Access Control",
- subcategory: "Identities and credentials are issued, managed, and verified",
+ category: "Identity Management, Authentication, and Access Control",
+ subcategory: "Identities and credentials for authorized users, services, and hardware are managed by the organization",
description: "No secrets/credentials in source code",
check: (m) => m.secretsScan.detected ? "fail" : "pass",
evidence: (m) => m.secretsScan.detected
@@ -106,10 +173,10 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
: "No secrets detected in source code",
},
{
- id: "PR.AC-4",
+ id: "PR.AA-05",
function: "Protect",
- category: "Access Control",
- subcategory: "Access permissions and authorizations are managed",
+ category: "Identity Management, Authentication, and Access Control",
+ subcategory: "Access permissions, entitlements, and authorizations are defined in a policy, managed, enforced, and reviewed",
description: "Branch protection and code review requirements",
check: (m) => {
if (m.accessControls.branchProtection === null) return "not-applicable";
@@ -118,15 +185,15 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
return "fail";
},
evidence: (m) => {
- if (m.accessControls.branchProtection === null) return "GitHub CLI not available — cannot check";
+ if (m.accessControls.branchProtection === null) return "GitHub API not reachable — cannot check";
return `Branch protection: ${m.accessControls.branchProtection ? "enabled" : "disabled"}, Required reviews: ${m.accessControls.requiredReviews ?? "none"}`;
},
},
{
- id: "PR.DS-1",
+ id: "PR.DS-01",
function: "Protect",
category: "Data Security",
- subcategory: "Data-at-rest is protected",
+ subcategory: "The confidentiality, integrity, and availability of data-at-rest are protected",
description: "Data collection points are documented with retention policies",
check: (m) => {
if (m.dataCollection.length === 0) return "pass";
@@ -138,11 +205,11 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
evidence: (m) => `${m.dataCollection.length} data collection points. ${m.dataCollection.filter(d => d.retention === "unknown").length} with undefined retention.`,
},
{
- id: "PR.DS-2",
+ id: "PR.DS-02",
function: "Protect",
category: "Data Security",
- subcategory: "Data-in-transit is protected",
- description: "HTTPS enforced with valid certificate and security headers",
+ subcategory: "The confidentiality, integrity, and availability of data-in-transit are protected",
+ description: "HTTPS enforced with valid certificate and HSTS",
check: (m) => {
if (!m.https) return "not-applicable";
const httpsOk = m.https.enforced;
@@ -157,11 +224,11 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
},
},
{
- id: "PR.DS-5",
+ id: "PR.DS-10",
function: "Protect",
category: "Data Security",
- subcategory: "Protections against data leaks are implemented",
- description: "Security headers prevent common attack vectors",
+ subcategory: "The confidentiality, integrity, and availability of data-in-use are protected",
+ description: "Security headers mitigate in-browser data leakage and tampering",
check: (m) => {
if (!m.securityHeaders) return "not-applicable";
const h = m.securityHeaders;
@@ -177,70 +244,55 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
},
},
{
- id: "PR.IP-1",
+ id: "PR.PS-01",
function: "Protect",
- category: "Information Protection",
- subcategory: "Configuration management baseline is established",
- description: "Security configuration is documented and scannable",
+ category: "Platform Security",
+ subcategory: "Configuration management practices are established and applied",
+ description: "Security configuration baseline is documented and scannable",
check: (m) => {
- // If we have a manifest at all, the baseline is being tracked
return m.scanDate ? "pass" : "fail";
},
- evidence: () => "GRC scanner produces manifest.yml documenting security configuration on every scan",
- },
- {
- id: "PR.IP-12",
- function: "Protect",
- category: "Information Protection",
- subcategory: "A vulnerability management plan is developed and implemented",
- description: "Dependency scanning and vulnerability tracking",
- check: (m) => m.dependencies !== null ? "pass" : "fail",
- evidence: (m) => m.dependencies
- ? `Automated dependency scanning via npm audit. Last scan: ${m.dependencies.lastAudit}`
- : "No vulnerability scanning configured",
+ evidence: () => "Scanner produces manifest.yml documenting configuration baseline on every scan; diffs between scans highlight drift.",
},
// ═══════════════════════════════════════════
// DETECT (DE)
// ═══════════════════════════════════════════
{
- id: "DE.CM-4",
- function: "Detect",
- category: "Continuous Monitoring",
- subcategory: "Malicious code is detected",
- description: "Secrets scanning detects leaked credentials",
- check: (m) => m.secretsScan.detected ? "fail" : "pass",
- evidence: (m) => m.secretsScan.detected
- ? `${m.secretsScan.findings.length} potential secrets found`
- : "Secrets scan clean — no credentials detected in source code",
- },
- {
- id: "DE.CM-8",
+ id: "DE.CM-09",
function: "Detect",
category: "Continuous Monitoring",
- subcategory: "Vulnerability scans are performed",
- description: "Automated scanning on code changes",
- check: (m) => m.dependencies !== null ? "pass" : "fail",
- evidence: () => "GRC scanner runs on push/PR via GitHub Actions",
+ subcategory: "Computing hardware and software, runtime environments, and their data are monitored to find potentially adverse events",
+ description: "Secrets scanning + dependency monitoring on every push/PR",
+ check: (m) => {
+ if (m.secretsScan.detected) return "fail";
+ if (!m.dependencies) return "partial";
+ return "pass";
+ },
+ evidence: (m) => {
+ const s = m.secretsScan.detected ? `${m.secretsScan.findings.length} potential secrets` : "secrets clean";
+ const d = m.dependencies ? `last audit ${m.dependencies.lastAudit}` : "no dependency audit";
+ return `Monitoring: ${s}; ${d}.`;
+ },
},
// ═══════════════════════════════════════════
// RESPOND (RS)
// ═══════════════════════════════════════════
{
- id: "RS.RP-1",
+ id: "RS.MA-01",
function: "Respond",
- category: "Response Planning",
- subcategory: "Response plan is executed during or after an incident",
+ category: "Incident Management",
+ subcategory: "The incident response plan is executed in coordination with relevant third parties once an incident is declared",
description: "Incident Response Plan exists",
check: (m) => m.artifacts.incidentResponsePlan !== "missing" ? "pass" : "fail",
evidence: (m) => `IRP status: ${m.artifacts.incidentResponsePlan}`,
},
{
- id: "RS.CO-2",
+ id: "RS.CO-02",
function: "Respond",
- category: "Communications",
- subcategory: "Incidents are reported consistent with established criteria",
+ category: "Incident Response Reporting and Communication",
+ subcategory: "Internal and external stakeholders are notified of incidents",
description: "Vulnerability disclosure and security contact are published",
check: (m) => {
const hasDisclosure = m.artifacts.vulnerabilityDisclosure !== "missing";
@@ -256,21 +308,12 @@ export const NIST_CSF_CONTROLS: FrameworkControl[] = [
// RECOVER (RC)
// ═══════════════════════════════════════════
{
- id: "RC.RP-1",
+ id: "RC.RP-01",
function: "Recover",
- category: "Recovery Planning",
- subcategory: "Recovery plan is executed during or after an incident",
+ category: "Incident Recovery Plan Execution",
+ subcategory: "The recovery portion of the incident response plan is executed once initiated from the incident response process",
description: "IRP includes recovery procedures",
check: (m) => m.artifacts.incidentResponsePlan !== "missing" ? "pass" : "fail",
evidence: (m) => `IRP includes recovery section: ${m.artifacts.incidentResponsePlan !== "missing" ? "yes" : "no"}`,
},
- {
- id: "RC.IM-1",
- function: "Recover",
- category: "Improvements",
- subcategory: "Recovery plans incorporate lessons learned",
- description: "IRP includes post-incident review process",
- check: (m) => m.artifacts.incidentResponsePlan !== "missing" ? "pass" : "fail",
- evidence: (m) => `IRP includes lessons learned section: ${m.artifacts.incidentResponsePlan !== "missing" ? "yes" : "no"}`,
- },
];
diff --git a/scanner/generators/ai-compliance-report.ts b/scanner/generators/ai-compliance-report.ts
index 1b5055e..d7afa11 100644
--- a/scanner/generators/ai-compliance-report.ts
+++ b/scanner/generators/ai-compliance-report.ts
@@ -36,10 +36,11 @@ export function generateAIComplianceReport(
const euMarketCount = manifest.aiSystems.filter(s => s.euMarket === true).length;
const lines: string[] = [
- `# EU AI Act Compliance Report — ${config.siteName}\n`,
+ `# EU AI Act Assessment Report — ${config.siteName}\n`,
`**Scope:** ${config.siteUrl}`,
`**Date:** ${new Date(manifest.scanDate).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}`,
- `**Framework:** Regulation (EU) 2024/1689 (EU AI Act)`,
+ `**Framework:** Regulation (EU) 2024/1689 (EU AI Act), as published in OJ L, 12 July 2024.`,
+ `**Cross-references:** NIST AI Risk Management Framework (NIST AI 100-1, v1.0, January 2023); ISO/IEC 42001:2023 Annex A.`,
`**Branch:** ${manifest.branch} (${manifest.commit})\n`,
"---\n",
`## AI Inventory Snapshot`,
@@ -47,10 +48,10 @@ export function generateAIComplianceReport(
`- High-risk or prohibited tier: **${highRiskCount}**`,
`- Declared on EU market: **${euMarketCount}**\n`,
"---\n",
- `## Overall Compliance: ${overallPct}%\n`,
+ `## Overall Score: ${overallPct}%\n`,
`${progressBar(overallPct, 30)} ${overallPct}%\n`,
- `${passed} passed, ${partial} partial, ${failed} failed out of ${applicable.length} applicable articles (${results.length - applicable.length} not applicable).\n`,
- `## Compliance by NIST AI RMF Phase\n`,
+ `${passed} passed, ${partial} partial, ${failed} failed out of ${applicable.length} applicable articles (${results.length - applicable.length} not applicable). This is a scan-derived assessment, not a conformity declaration.\n`,
+ `## Score by NIST AI RMF Phase\n`,
"| Phase | Score | Status |",
"|-------|-------|--------|",
];
@@ -146,7 +147,7 @@ export function generateAIComplianceReport(
lines.push("## Methodology\n");
lines.push("Articles are evaluated against the repo's scan manifest and AI-system classifications. Many obligations are program-level and cannot be verified from code alone; those resolve to **partial** with instructional evidence pointing to what still needs documentation.");
lines.push("");
- lines.push("Applicability follows EU AI Act Article 6 and Annex III: most obligations (Art. 9/11/12/13/14/15/27/60/73) are marked **N/A** unless a system classified as `high` or `prohibited` is detected. Article 5 (prohibited practices) always applies. Article 50 (user transparency) applies when any system is at least `limited` tier. Article 27 (FRIA) and Article 60 (registration) additionally require the system to be on the EU market (`eu_market: true` in `.grc/config.yml` or `gdpr` in jurisdiction defaults).");
+ lines.push("Applicability follows EU AI Act Article 6 and Annex III: most high-risk obligations (Art. 9/11/12/13/14/15/27/71/73) are marked **N/A** unless a system classified as `high` or `prohibited` is detected. Article 5 (prohibited practices) always applies. Article 50 (user transparency) applies when any system is at least `limited` tier. Article 27 (FRIA) applies only to specific deployer categories — public bodies, private providers of public services, and deployers of Annex III point 5 credit/insurance systems. Article 71 (EU database) covers high-risk Annex III systems placed on the EU market; the underlying registration obligations live in Article 49 (providers) and Article 26(8) (certain public-sector deployers).");
lines.push("");
lines.push("**Scoring:** Pass = 1.0, Partial = 0.5, Fail = 0, N/A = excluded from calculation.");
lines.push("");
diff --git a/scanner/generators/exports/exports.test.ts b/scanner/generators/exports/exports.test.ts
index 30b207c..433b9ec 100644
--- a/scanner/generators/exports/exports.test.ts
+++ b/scanner/generators/exports/exports.test.ts
@@ -78,14 +78,14 @@ describe("CSV exports", () => {
it("emits one row per control + a header", () => {
const nist: ControlResult[] = [
{
- control: { id: "ID.GV-1", function: "Identify", category: "Governance", subcategory: "x", description: "y", check: () => "pass", evidence: () => "" },
- status: "pass", evidence: "ok", soc2: ["CC1.1"], iso27001: ["A.5"],
+ control: { id: "GV.PO-01", function: "Govern", category: "Policy", subcategory: "x", description: "y", check: () => "pass", evidence: () => "" },
+ status: "pass", evidence: "ok", soc2: ["CC1.1"], iso27001: ["A.5.1"],
},
];
const out = generateCsvNistCsf(baseManifest(), nist);
const lines = out.trim().split("\n");
expect(lines.length).toBe(2); // header + 1 row
- expect(lines[1]).toContain("ID.GV-1");
+ expect(lines[1]).toContain("GV.PO-01");
expect(lines[1]).toContain("CC1.1");
});
@@ -180,7 +180,7 @@ describe("OSCAL export", () => {
it("emits one OSCAL result per framework — NIST always, EU AI Act only when results present", () => {
const nistResults: ControlResult[] = [
- { control: { id: "ID.GV-1", function: "Identify", category: "Governance", subcategory: "x", description: "y", check: () => "pass", evidence: () => "" }, status: "pass", evidence: "ok", soc2: [], iso27001: [] },
+ { control: { id: "GV.PO-01", function: "Govern", category: "Policy", subcategory: "x", description: "y", check: () => "pass", evidence: () => "" }, status: "pass", evidence: "ok", soc2: [], iso27001: [] },
];
// No EU AI Act results → only one OSCAL result in the output.
diff --git a/scanner/generators/exports/oscal.ts b/scanner/generators/exports/oscal.ts
index b268b29..adff3f2 100644
--- a/scanner/generators/exports/oscal.ts
+++ b/scanner/generators/exports/oscal.ts
@@ -6,7 +6,7 @@ import type { ControlResult } from "../framework-report.js";
*
* Two assessment results, one per framework:
* 1. NIST CSF 2.0 evaluation — cites framework subcategory IDs directly
- * (e.g. "ID.GV-1") since NIST CSF is the primary framework.
+ * (e.g. "GV.PO-01") since NIST CSF 2.0 is the primary framework.
* 2. EU AI Act evaluation — cites article identifiers ("ART-5") with
* regulation metadata in props.
*
diff --git a/scanner/generators/exports/sarif.ts b/scanner/generators/exports/sarif.ts
index a628d6b..d2e4dd3 100644
--- a/scanner/generators/exports/sarif.ts
+++ b/scanner/generators/exports/sarif.ts
@@ -204,7 +204,7 @@ function buildAccessControlResults(manifest: Manifest): SarifResult[] {
return [{
ruleId: "grc/unprotected-route",
level: "warning",
- message: { text: "Branch protection is disabled on this repository. Enabling branch protection with required reviewers is a baseline governance control for NIST CSF PR.AC-4 and SOC 2 CC6.1." },
+ message: { text: "Branch protection is disabled on this repository. Enabling branch protection with required reviewers is a baseline governance control for NIST CSF 2.0 PR.AA-05 and SOC 2 CC6.1." },
locations: [{
physicalLocation: { artifactLocation: { uri: ".github/branch-protection.md" } },
}],
diff --git a/scanner/generators/framework-report.ts b/scanner/generators/framework-report.ts
index ba121c8..49ee434 100644
--- a/scanner/generators/framework-report.ts
+++ b/scanner/generators/framework-report.ts
@@ -38,7 +38,7 @@ export function evaluateFramework(manifest: Manifest): ControlResult[] {
}
function calcFunctionScores(results: ControlResult[]): FunctionScore[] {
- const functions = ["Identify", "Protect", "Detect", "Respond", "Recover"] as const;
+ const functions = ["Govern", "Identify", "Protect", "Detect", "Respond", "Recover"] as const;
return functions.map(fn => {
const controls = results.filter(r => r.control.function === fn);
@@ -83,20 +83,21 @@ export function generateFrameworkReport(
: 100;
const lines: string[] = [
- `# NIST CSF Compliance Report — ${config.siteName}\n`,
+ `# NIST CSF Coverage Report — ${config.siteName}\n`,
`**Scope:** ${config.siteUrl}`,
`**Date:** ${new Date(manifest.scanDate).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}`,
- `**Framework:** NIST Cybersecurity Framework (CSF) 2.0`,
+ `**Framework:** NIST Cybersecurity Framework (CSF) 2.0 (NIST.CSWP.29, February 2024)`,
+ `**Cross-references:** SOC 2 Trust Services Criteria, 2017 edition (revised 2022); ISO/IEC 27001:2022 Annex A.`,
`**Branch:** ${manifest.branch} (${manifest.commit})\n`,
"---\n",
// Overall score
- `## Overall Compliance: ${overallPct}%\n`,
+ `## Overall Score: ${overallPct}%\n`,
`${progressBar(overallPct, 30)} ${overallPct}%\n`,
- `${overallPassed} passed, ${overallPartial} partial, ${applicable.length - overallPassed - overallPartial} failed out of ${applicable.length} applicable controls\n`,
+ `${overallPassed} passed, ${overallPartial} partial, ${applicable.length - overallPassed - overallPartial} failed out of ${applicable.length} mapped controls. This score reflects ${applicable.length} of NIST CSF 2.0's ~106 subcategories — it is **not** a claim of full framework compliance.\n`,
// Function breakdown
- "## Compliance by Function\n",
+ "## Score by Function\n",
"| Function | Score | Status |",
"|----------|-------|--------|",
];
@@ -111,7 +112,7 @@ export function generateFrameworkReport(
// Detailed results by function
lines.push("## Detailed Control Assessment\n");
- for (const fn of ["Identify", "Protect", "Detect", "Respond", "Recover"]) {
+ for (const fn of ["Govern", "Identify", "Protect", "Detect", "Respond", "Recover"]) {
const fnResults = results.filter(r => r.control.function === fn);
const score = scores.find(s => s.name === fn)!;
diff --git a/scanner/generators/risk-assessment.ts b/scanner/generators/risk-assessment.ts
index e26bbf9..88b9e07 100644
--- a/scanner/generators/risk-assessment.ts
+++ b/scanner/generators/risk-assessment.ts
@@ -48,7 +48,7 @@ export function assessRisks(
severity: "critical",
status: "open",
mitigation: "Run `npm audit fix` or update affected packages. Review each CVE for applicability to your usage.",
- framework: ["NIST CSF ID.RA-1", "SOC 2 CC7.1", "ISO 27001 A.12.6.1"],
+ framework: ["NIST CSF ID.RA-01", "SOC 2 CC7.1", "ISO 27001 A.8.8"],
});
}
if (d.highVulnerabilities > 0) {
@@ -62,7 +62,7 @@ export function assessRisks(
severity: "high",
status: "open",
mitigation: "Review each vulnerability. Apply patches where available. If no patch exists, evaluate workarounds or alternative packages.",
- framework: ["NIST CSF ID.RA-1", "SOC 2 CC7.1"],
+ framework: ["NIST CSF ID.RA-01", "SOC 2 CC7.1"],
});
}
}
@@ -85,7 +85,7 @@ export function assessRisks(
severity: hasCsp ? "critical" : "medium",
status: "open",
mitigation: "Add security headers middleware to Express application. See security-headers-report.md for copy-paste implementation.",
- framework: ["NIST CSF PR.DS-2", "SOC 2 CC6.1", "ISO 27001 A.13.1.1"],
+ framework: ["NIST CSF PR.DS-02", "SOC 2 CC6.1", "ISO 27001 A.8.20"],
});
}
}
@@ -103,7 +103,7 @@ export function assessRisks(
severity: "critical",
status: "open",
mitigation: "Configure HTTP to HTTPS redirect in your server or reverse proxy.",
- framework: ["NIST CSF PR.DS-2", "SOC 2 CC6.7", "GDPR Art. 32"],
+ framework: ["NIST CSF PR.DS-02", "SOC 2 CC6.7", "GDPR Art. 32"],
});
}
@@ -123,7 +123,7 @@ export function assessRisks(
severity: daysLeft <= 7 ? "critical" : "high",
status: "open",
mitigation: "Renew certificate immediately. Consider using Let's Encrypt with auto-renewal (certbot).",
- framework: ["NIST CSF PR.DS-2", "SOC 2 CC6.7"],
+ framework: ["NIST CSF PR.DS-02", "SOC 2 CC6.7"],
});
}
}
@@ -141,7 +141,7 @@ export function assessRisks(
severity: "critical",
status: "open",
mitigation: "Rotate all exposed credentials immediately. Move secrets to environment variables or a secrets manager. Add patterns to .gitignore.",
- framework: ["NIST CSF PR.AC-1", "SOC 2 CC6.1", "ISO 27001 A.9.4.3"],
+ framework: ["NIST CSF PR.AA-01", "SOC 2 CC6.1", "ISO 27001 A.5.17"],
});
}
@@ -157,7 +157,7 @@ export function assessRisks(
severity: "high",
status: "open",
mitigation: "Enable branch protection rules: require PR reviews, status checks, and restrict direct pushes. See access-controls-report.md.",
- framework: ["NIST CSF PR.AC-4", "SOC 2 CC6.1", "ISO 27001 A.9.4.1"],
+ framework: ["NIST CSF PR.AA-05", "SOC 2 CC6.1", "ISO 27001 A.8.3"],
});
}
@@ -173,7 +173,7 @@ export function assessRisks(
severity: "high",
status: "open",
mitigation: "Add authentication middleware to all admin, settings, and destructive routes.",
- framework: ["NIST CSF PR.AC-1", "SOC 2 CC6.1"],
+ framework: ["NIST CSF PR.AA-01", "SOC 2 CC6.1"],
});
}
@@ -196,7 +196,7 @@ export function assessRisks(
severity: missingArtifacts.includes("Privacy Policy") ? "high" : "medium",
status: "open",
mitigation: "Deploy the auto-generated documents from the .grc/ directory to the live site.",
- framework: ["NIST CSF ID.GV-1", "SOC 2 CC1.1", "GDPR Art. 13", "CCPA §1798.100"],
+ framework: ["NIST CSF GV.PO-01", "SOC 2 CC1.1", "GDPR Art. 13", "CCPA §1798.100 (as amended by CPRA)"],
});
}
@@ -230,7 +230,7 @@ export function assessRisks(
severity: "low",
status: "open",
mitigation: "Define retention periods for each data collection point in .grc/config.yml. Implement automated data deletion where appropriate.",
- framework: ["GDPR Art. 5(1)(e)", "NIST CSF PR.IP-6"],
+ framework: ["GDPR Art. 5(1)(e)", "NIST CSF PR.DS-01"],
});
}
diff --git a/scanner/index.ts b/scanner/index.ts
index 8a58b04..869c43b 100644
--- a/scanner/index.ts
+++ b/scanner/index.ts
@@ -326,7 +326,7 @@ async function main() {
const passed = applicable.filter(r => r.status === "pass").length;
const partial = applicable.filter(r => r.status === "partial").length;
const overallPct = Math.round(((passed + partial * 0.5) / applicable.length) * 100);
- console.log(`📄 NIST CSF report written to ${frameworkReportPath} (${overallPct}% compliant)`);
+ console.log(`📄 NIST CSF report written to ${frameworkReportPath} (${overallPct}% of mapped controls passing)`);
// Generate EU AI Act compliance report (Phase 8 Sub-phase C).
// Articles evaluate against the manifest — scoping by risk tier and euMarket
diff --git a/scanner/templates/incident-response-plan.hbs b/scanner/templates/incident-response-plan.hbs
index 90013b4..f7a39df 100644
--- a/scanner/templates/incident-response-plan.hbs
+++ b/scanner/templates/incident-response-plan.hbs
@@ -8,7 +8,7 @@
This Incident Response Plan (IRP) defines the process for detecting, responding to, and recovering from security incidents affecting [{{config.siteUrl}}]({{config.siteUrl}}) and related systems.
-This plan follows the NIST SP 800-61 incident response lifecycle: **Prepare → Detect → Contain → Eradicate → Recover → Lessons Learned**.
+This plan follows the NIST SP 800-61 Rev. 2 incident response lifecycle: **Preparation → Detection & Analysis → Containment, Eradication & Recovery → Post-Incident Activity** (which includes lessons learned).
---
diff --git a/scripts/smoke-dashboard.ts b/scripts/smoke-dashboard.ts
index 0f95ead..fbe725c 100644
--- a/scripts/smoke-dashboard.ts
+++ b/scripts/smoke-dashboard.ts
@@ -143,7 +143,7 @@ function summaryFor(m: Manifest): RepoSummary {
}
function functionScoresFor(summary: RepoSummary) {
- return ["Identify", "Protect", "Detect", "Respond", "Recover"].map(name => {
+ return ["Govern", "Identify", "Protect", "Detect", "Respond", "Recover"].map(name => {
const controls = summary.nistResults.filter(r => r.control.function === name);
const applicable = controls.filter(r => r.status !== "not-applicable");
const passed = applicable.filter(r => r.status === "pass").length;