nsauditor-ai 0.2.35 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,10 +13,14 @@ A modular, AI-assisted network security audit platform that scans, understands,
13
13
 
14
14
  NSAuditor AI is the open-source core of a privacy-first security intelligence platform built by [Nsasoft US LLC](https://www.nsauditor.com/ai/). It orchestrates 27 specialized scanning plugins against target hosts, fuses their results through an intelligent concluder, and optionally produces AI-powered vulnerability reports — all running entirely on your machine.
15
15
 
16
- **Zero Data Exfiltration by design.** NSAuditor AI works fully offline. AI analysis, CVE correlation, and continuous monitoring all happen locally. External calls (to AI APIs, NVD, etc.) are opt-in and use your own API keys. We never see your scan data.
16
+ **Zero Data Exfiltration by design — and stated precisely.** We never see your scan data: no customer data is collected, transmitted, or stored by Nsasoft US LLC, and the product has no telemetry or phone-home endpoint. Scanning, analysis, license verification and report generation all run on your machine. Two clarifications that matter operationally: AI enrichment is **opt-in** and uses your own API keys (point it at a local Ollama and nothing leaves the host), while **CVE correlation queries NIST's public NVD API by default** — set `NSAUDITOR_OFFLINE_ONLY=1` with a local NVD store to make it fully local, which reports an explicit coverage gap instead of a silent clean.
17
17
 
18
18
  ## What's New
19
19
 
20
+ **Three flags, one new command, and a startup veto that stops swallowing itself (CE 0.2.37 / Enterprise 0.2.37 + 0.33.0).** Paired with Enterprise 0.33.0, and **this CE is required by it** — `compliance attest`, `--sla-policy <file>` and `--compliance-history <dir>` live here, so the Enterprise peer floor is raised to 0.2.37. `compliance attest` produces a Type II recurring-scan attestation over a directory of prior scans; the two flags reach the SLA/MTTR engine, which had been wired inside the compliance phase behind options nothing set. A startup posture preflight now refuses a configuration contradiction — `NSAUDITOR_OFFLINE_ONLY=1` together with a configured outbound path — with exit 2, at every tier and every command. And the bare `catch {}` around the Enterprise stage is split: "Enterprise is not installed" stays silent, but "Enterprise ran and threw" now names the lost stage on stderr instead of exiting 0 with no compliance report and no output. Also corrected on this frozen page: the MCP tool tables listed seven tools that are not registered and omitted two that are; WORM evidence storage was sold with a ✅ though its writer has no caller; and `--help` told operators to select plugin 1023 with `--plugins`, which cannot work — it needs a discovery plugin to confirm the host is up first.
21
+
22
+ **The scan summary stops hiding its own scope limits, and a new tool ends matrix guesswork (CE 0.2.36 / Enterprise 0.32.11).** `scan_cloud`'s markdown rendered `CRITICAL · HIGH · MEDIUM · LOW · PASS` — **INFO was not a column** — so on a real AWS scan 55 of 62 INFO records appeared in no summary anywhere, several of them declarations of what was **not** assessed. INFO is now a column and rolls up by category, and scope boundaries get their own `[🔎 SCOPE NOT ASSESSED]` channel: surface nobody assessed must not read like surface assessed and found clean. **New MCP tool `compliance_matrix`** returns the shipped coverage matrix for any of the seven frameworks, derived from the framework maps at call time and failing closed rather than returning an empty one. `pdfExport` is withdrawn (registered and minted, implemented nowhere), and **every capability now ships a written description** — a bare flag name is a claim with no text behind it. `--watch` is described honestly, including the part the obvious correction got wrong: each tick writes that tick's artifacts; what is missing is any relation between them.
23
+
20
24
  **The dependency gate starts measuring what you install (CE 0.2.35 / Enterprise 0.32.10).** Paired with Enterprise 0.32.10 — **no CE behaviour change**. On the Enterprise side the advisory gate had been auditing the maintainer's working tree while calling it the production closure; it now packs the tarball, installs it the way you would, and audits **that**, and it will not report a clean result until it has proved an advisory database actually answered. The two trees shared 5 of 26 advisory packages and **zero** high-severity ones. Also: the SOC 2 matrix is enumerated in full (**10 / 4 / 37 = 51** — completeness, no control moved), and the Type II docs now say which mechanisms are reachable versus roadmap.
21
25
 
22
26
  **Internal-provenance strip + a false-clean closure (CE 0.2.34 / Enterprise 0.32.9).** Paired with Enterprise 0.32.9. The evidence pack a customer hands their auditor no longer carries internal engineering markers — **686 unexplained occurrences → 0** on a rebuilt three-cloud pack, with a positive control so the zero is a measurement rather than an absence of looking. Two behavioural fixes ride with it: a cloud plugin that returns `up:false` (optional SDK absent, credentials unusable) now fail-closes an evidence gap on every in-scope control of that cloud instead of leaving them PASS, and an archived scan conclusion re-mapped through the new build warns instead of failing clean. CE's side: the MCP `scan_cloud` summary strips **both** spellings of the evidence-gap routing prefix, so an operator pairing this CE with an older Enterprise still gets a clean one-line summary. **⚠️ If you suppress by `titlePattern` on the old violation text, re-point it — see the [Enterprise CHANGELOG](https://github.com/nsasoft/nsauditor-ai-ee/blob/main/CHANGELOG.md).**
@@ -59,7 +63,7 @@ NSAuditor AI is available in three editions: Community (free, MIT-licensed, no r
59
63
 
60
64
  If you're heading into a **SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, CIS Controls v8, or GDPR Article 32 audit** — or need to satisfy customer security questionnaires citing those frameworks, or an IG1 attestation for cyber-insurance renewal — Enterprise turns scan output into **auditor-ready evidence packs** that pass institutional scrutiny:
61
65
 
62
- - ☁️ **28 cloud plugins** across AWS / Azure / GCP — find the configuration risks an auditor will flag, before they do (CloudTrail integrity, KMS custody, S3 Object Lock, IAM shadow-admin paths, GCP IAM impersonation chains, Azure RBAC sprawl, and more)
66
+ - ☁️ **28 enterprise plugins — 27 cloud auditors across AWS / Azure / GCP, plus **`1023 Zero Trust Assessment`**, which scores zero-trust posture (segmentation, encryption-in-transit, identity, lateral-movement risk) from a **network-host** scan** — find the configuration risks an auditor will flag, before they do (CloudTrail integrity, KMS custody, S3 Object Lock, IAM shadow-admin paths, GCP IAM impersonation chains, Azure RBAC sprawl, and more)
63
67
  - 📋 **7 compliance frameworks shipped** — generate any combination from a single scan:
64
68
  - **SOC 2** (AICPA TSC 2017) — 10 fully-covered + 4 partial controls
65
69
  - **HIPAA Security Rule §164.312** — 7 covered + 3 partial Technical Safeguards; **Zero BAA required** (ePHI never leaves your infrastructure)
@@ -68,10 +72,10 @@ If you're heading into a **SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, CIS C
68
72
  - **ISO/IEC 27001:2022** (ISO + IEC, Oct 2022; 2013 edition retired Oct 31, 2025) — **17 covered + 14 partial + 62 OOS across 93 Annex A controls** (the complete Annex A universe); per-Annex-A-code mapping auditor-canonical for ISO/IEC 17021-1 certification body assessors; Statement of Applicability per Clause 6.1.3.d discipline + ISMS Clauses 4-10 OOS-by-design with 7 Major Nonconformity classes
69
73
  - **CIS Critical Security Controls v8** (CIS, May 2021; v8.1 errata June 2024) — **17 covered + 23 partial + 113 OOS across 153 Safeguards / 18 Controls**; per-Safeguard mapping with the **Implementation Group cumulative discipline** (IG1=56 cyber-insurance baseline / IG2 cumulative=130 / IG3 cumulative=153); no-certification-body attestation discipline (INPUT to your CSAT / CIS-CAT Pro self-attestation, never "CIS certified"); Cloud Companion Guide v8 shared-responsibility + CIS-Hardened-Image substrate-evidence credit (4.1/4.2/4.6)
70
74
  - **GDPR Article 32 (Security of Processing)** (Regulation (EU) 2016/679) — **4 covered + 5 partial + 2 OOS across 11 Art. 32 sub-measure units**; **GDPR Article 32 infrastructure substrate only — NOT GDPR compliance** (GDPR is a 99-article legal regime; Art. 32 security-of-processing is the only article an infrastructure scanner can substrate-evidence; the rest is operator-side, out of scope by design). Four-factor proportionality (substrate *for* your "appropriate to the risk" determination, never an absolute pass/fail); personal-data-scope attestation (pair with your Art. 30 records of processing); **Art. 83(4) lower fine tier** (€10M/2%, not the €20M/4% headline tier); Art. 32(3)/Art. 42 cloud-provider certification-inheritance
71
- - 🔐 **Cryptographically signed evidence** — SHA-256 chain-of-custody + RFC 3161 trusted timestamps + Ed25519 suppression signing. Non-repudiation, not just integrity. Auditors can verify offline.
72
- - 🏛️ **Zero Data Exfiltration architecture** — your scan data never leaves your infrastructure. Air-gapped deployment supported. AI analysis happens locally (Ollama) or via your own API keys. Important for PCI DSS CDE-isolation threat models.
75
+ - 🔐 **Evidence with a verifiable chain of custody** — SHA-256 sidecars on every artifact, verifiable offline. RFC 3161 trusted timestamps are **opt-in since EE 0.33.0** via `NSAUDITOR_TSA_URL`, require an outbound call to the authority you name, and have never been exercised against a real one. **Ed25519 suppression signing is implemented in the package but not reachable from any shipped entry point** — no first-party run signs a suppression, so do not plan around it.
76
+ - 🏛️ **Zero Data Exfiltration architecture** — no scan data is collected, transmitted, or stored by Nsasoft; reports are written to your local disk. Air-gapped operation once configured for it (`NSAUDITOR_OFFLINE_ONLY=1` plus a local NVD store; a default run queries NIST's public CVE API). AI analysis happens locally (Ollama) or via your own API keys. Important for PCI DSS CDE-isolation threat models.
73
77
  - 🔗 **GRC connectors — Vanta + Drata + Secureframe (Enterprise)** — map compliance findings to your GRC platform's evidence/test records and push them at **scan time** (opt-in). Suppression-aware outcome mapping (Vanta) / structured records (Drata + Secureframe), idempotent retries, rate-limit handling, token redaction, and Zero-Data-Exfiltration egress redaction. Early-access, single-workspace; live validation against production tenants is in progress. See **[GRC Connectors](#grc-connectors-vanta-drata-secureframe)** below.
74
- - 🗄️ **WORM evidence storage** — S3 Object Lock COMPLIANCE-mode for SEC Rule 17a-4(f) / FINRA 4511 retention compliance
78
+ - 🗄️ **WORM posture VALIDATION** — we read your bucket's S3 Object Lock configuration per CloudTrail trail bucket and fail the control when it is absent, in GOVERNANCE mode or under-retained. That is substrate FOR a SEC Rule 17a-4(f) / FINRA 4511 retention claim, not the claim itself: the engine WRITING artifacts into the immutable store is built and **not reachable** (no caller), so the retention guarantee stays yours to make about your own archive
75
79
  - 📊 **SLA / MTTR tracking + recurring-scan attestation** — the **Type II operating-effectiveness evidence** auditors actually demand (not just point-in-time snapshots)
76
80
  - 🎯 **11 adversarial-audit Claude Code skills** authored per the Per-Framework Adversarial-Audit Skill Pairing institutional pattern — Phase-4 Compliance/GRC chain 8-of-8 COMPLETE for all shipped frameworks (SOC 2 + HIPAA + NIST CSF + PCI DSS + ISO 27001 + CIS Controls v8 + GDPR Article 32 + GRC connector)
77
81
 
@@ -102,7 +106,7 @@ How Marketplace fulfillment works (ZDE and air-gap preserved — no runtime AWS
102
106
  | Risk scoring + prioritization | — | ✅ | ✅ |
103
107
  | Parallel analysis agents | — | ✅ | ✅ |
104
108
  | **Enterprise — cloud scanning** | | | |
105
- | 28 cloud plugins (AWS / Azure / GCP) | — | — | ✅ |
109
+ | 28 enterprise plugins — 27 cloud (AWS / Azure / GCP) + 1 network-scan zero-trust check | — | — | ✅ |
106
110
  | Zero Trust assessment | — | — | ✅ |
107
111
  | **Enterprise — compliance (7 frameworks)** | | | |
108
112
  | SOC 2 (AICPA TSC 2017) — 10 covered + 4 partial controls | — | — | ✅ |
@@ -114,16 +118,16 @@ How Marketplace fulfillment works (ZDE and air-gap preserved — no runtime AWS
114
118
  | **GDPR Article 32 (Security of Processing)** — Art. 32 infrastructure substrate (4 covered + 5 partial + 2 OOS / 11 sub-measure units); **not GDPR compliance** · Art. 83(4) lower fine tier | — | — | ✅ |
115
119
  | Multi-framework `--compliance soc2,hipaa,nist-csf,pci-dss,iso-27001,cis-v8,gdpr` from one scan | — | — | ✅ |
116
120
  | **Enterprise — auditor-grade evidence** | | | |
117
- | Signed evidence packs (SHA-256 + RFC 3161 timestamps) | — | — | ✅ |
118
- | Ed25519 suppression signing | — | — | ✅ |
121
+ | Evidence packs with SHA-256 chain-of-custody (RFC 3161 timestamps opt-in, unproven against a real TSA) | — | — | ✅ |
122
+ | Suppression workflow with approver identity verification (Ed25519 SIGNING is built, not reachable) | — | — | ✅ |
119
123
  | Chain-of-custody manifests | — | — | ✅ |
120
124
  | SLA / MTTR tracking + compensating controls | — | — | ✅ |
121
125
  | Recurring-scan attestation (Type II operating-effectiveness) | — | — | ✅ |
122
- | WORM evidence storage (S3 Object Lock — SEC 17a-4 / FINRA 4511) | — | — | ✅ |
126
+ | WORM POSTURE VALIDATION on your own buckets (S3 Object Lock — SEC 17a-4 / FINRA 4511 substrate). The engine WRITING into the immutable store is built, **not reachable** | — | — | ✅ |
123
127
  | **Enterprise — integration + deployment** | | | |
124
128
  | GRC connectors — Vanta + Drata + Secureframe push (scan-time, opt-in) | — | — | ✅ |
125
- | Tabletop simulation + SIEM correlation | — | — | ✅ |
126
- | Air-gapped deployment | — | — | ✅ |
129
+ | Tabletop simulation + SIEM correlation — built, **not reachable** (no caller) | — | — | 🚧 |
130
+ | Air-gapped **operation** once installed — offline licence validation, offline CVE matching under `NSAUDITOR_OFFLINE_ONLY=1`. (Air-gapped DELIVERY is WITHDRAWN: no offline installer, image bundle or feed bundle ships.) | — | — | ✅ |
127
131
 
128
132
  **This repository is the Community Edition** — fully functional, MIT-licensed, no restrictions, no telemetry. Pro and Enterprise features ship via the [`@nsasoft/nsauditor-ai-ee`](https://www.nsauditor.com/ai/pricing) package and install alongside the CE binary once licensed.
129
133
 
@@ -263,7 +267,7 @@ Results land in `./out/<host>_<timestamp>/`:
263
267
 
264
268
  ### Pro/Enterprise Plugins (via @nsasoft/nsauditor-ai-ee)
265
269
 
266
- **28 enterprise plugins** across AWS, GCP, and Azure substrate audits — all mapped to AICPA Trust Services Criteria 2017 (10 covered + 4 partial controls). EE plugins live in the disjoint 1000+ ID range; CE reserves 001-099. Once licensed, the EE package installs alongside the CE binary and discovers automatically.
270
+ **28 enterprise plugins: 27 cloud auditors across AWS, GCP and Azure substrate, plus **`1023 Zero Trust Assessment`**, which scores zero-trust posture (segmentation, encryption-in-transit, identity, lateral-movement risk) from a **network-host** scan** — all mapped to AICPA Trust Services Criteria 2017 (10 covered + 4 partial controls). EE plugins live in the disjoint 1000+ ID range; CE reserves 001-099. Once licensed, the EE package installs alongside the CE binary and discovers automatically. A cloud `--host` pass auto-scopes on `cloudProvider`, which only the 27 declare, so `1023` never runs on a cloud pass. It runs on a network-host scan with the full plugin set — measured on the shipped CLI, selecting it by id on its own does not work, because it requires the host to be confirmed up by a discovery plugin.
267
271
 
268
272
  → **[Watch a sample scan run end-to-end](https://www.nsauditor.com/ai/docs/sample-scan/)** — synthetic Acme Corp AWS account + home-office router. Real real scan output, no signup required. See the transitive SG chain reachability finding, the multi-region GuardDuty audit, the dnsmasq CVE detection, and what the signed evidence pack actually looks like.
269
273
 
@@ -307,8 +311,8 @@ Results land in `./out/<host>_<timestamp>/`:
307
311
  | 1220 | Azure Storage Account Data-Protection (**EE 0.13.2**) | Enterprise | Dedicated Azure Storage Account encryption / transit / authorization auditor — orthogonal to the 1022 scanner's network-exposure dims (no double-emission; mirrors the AWS 1020 + 1120 two-plugin S3 split). HTTPS-only transit (`enableHttpsTrafficOnly`) + minimum TLS version + Shared Key authorization (`allowSharedKeyAccess` — bypasses Azure AD; absent = enabled, never silent-PASS) + infrastructure (double) encryption + encryption key source incl. **customer-managed-key reachability + rotation** (`keyVaultProperties` — a disabled/revoked/version-pinned CMK degrades, not silent-PASS). Conservative classifier: indeterminate field / AccessDenied → evidence-gap; single-subscription scope surfaced explicitly. **CC6.7 / CC6.1 / C1.1** |
308
312
  | 1221 | Azure NSG Perimeter (**EE 0.14.0**; UDP lane **EE 0.14.1**) | Enterprise | The Azure analog of AWS 1170 — a CC6.6 network-segmentation perimeter auditor for Azure Network Security Groups. Evaluates each NSG's inbound rules in Azure priority order (first match wins; DenyAllInbound default): all-protocol public Allow + public-source (`*`/`0.0.0.0/0`/`Internet`) to a restricted **TCP** management/data-tier port (SSH/RDP/MSSQL/MySQL/Postgres/Redis/Mongo/SMB/WinRM/etc.) + `::/0` IPv6-wildcard to a restricted port (the dimension 1022's flat lint misses) + **public-source / `::/0` to a restricted UDP service** (SNMP/CLDAP/NTP/rpcbind/IPMI/IKE/Memcached etc. — Dim 2u/3u, EE 0.14.1) + public→non-restricted INFO + PASS substrate. **Attachment-aware** (attached → CRITICAL effective; orphaned → MEDIUM latent) + effective priority/deny-override resolution + `0.0.0.0/1` split-range coverage. Non-overlapping-by-depth with 1022's coarse per-rule NSG lint. Conservative classifier: denied/indeterminate → evidence-gap; one malformed NSG degrades per-resource. **CC6.6** |
309
313
  | 1222 | Azure Key Vault Deep Auditor (**EE 0.15.0**) | Enterprise | The third dedicated Azure auditor (after 1220 storage + 1221 NSG) — the KV analog of how 1221 deepens 1022's flat NSG dim. Enumerates each vault's keys, role assignments, and diagnostic settings across 4 dims: (1) key **auto-rotation policy** + (2) key **expiry** (epoch-s/ms/Date/string coerced) + (3) **diagnostic logging → Log Analytics** (`@azure/arm-monitor`) + (4) **privileged-access depth** (RBAC `roleAssignments` admin/data-plane/scope-aware + legacy `accessPolicies` export/wide-crypto breadth). Orthogonal to 1022's vault-property dims (purge/soft-delete/network-ACL/RBAC-mode) — no double-emission. Secret/cert expiry is a deliberate data-plane scope boundary. Conservative classifier: indeterminate field / AccessDenied / arm-monitor absent → evidence-gap; one malformed vault degrades per-resource. **CC6.3 / C1.1 / CC6.1 / CC7.2** |
310
- | — | SOC 2 Compliance Engine | Enterprise | AICPA TSC 2017 mapping (10 covered + 4 partial controls), chain-of-custody, RFC 3161 timestamps, suppression workflow with Ed25519 signing. |
311
- | — | **HIPAA Compliance Engine (EE 0.9.0)** | Enterprise | HIPAA Security Rule §164.312 Technical Safeguards mapping (7 covered + 3 partial + 45 OOS within §164.312 + entire §164.308 + entire §164.310). HHS Required/Addressable discipline per control. Same institutional-grade evidence infrastructure as SOC 2 (chain-of-custody, RFC 3161 timestamps, Ed25519 suppression signing). Use `--compliance hipaa` or `--compliance soc2,hipaa` for dual-framework reports from a single scan. **Zero BAA required** — Zero Data Exfiltration architecture means ePHI never leaves customer infrastructure. |
314
+ | — | SOC 2 Compliance Engine | Enterprise | AICPA TSC 2017 mapping (10 covered + 4 partial controls), SHA-256 chain-of-custody, opt-in RFC 3161 timestamps, and the suppression workflow. (Ed25519 SIGNING of suppressions is built but unreachable — the workflow ships, the signature does not.) |
315
+ | — | **HIPAA Compliance Engine (EE 0.9.0)** | Enterprise | HIPAA Security Rule §164.312 Technical Safeguards mapping (7 covered + 3 partial + 45 OOS within §164.312 + entire §164.308 + entire §164.310). HHS Required/Addressable discipline per control. Same evidence infrastructure as SOC 2 (SHA-256 chain-of-custody; opt-in RFC 3161 timestamps; suppression signing built but not reachable). Use `--compliance hipaa` or `--compliance soc2,hipaa` for dual-framework reports from a single scan. **Zero BAA required** — Zero Data Exfiltration architecture means ePHI never leaves customer infrastructure. |
312
316
  | — | **NIST CSF 2.0 Compliance Engine (EE 0.10.0)** | Enterprise | NIST Cybersecurity Framework 2.0 Core mapping at the auditor-canonical Subcategory level — 13 covered + 10 partial + 83 OOS across 106 of CSF 2.0's 107 Subcategories. Govern function OOS-by-design (GV.SC-04 partial as substrate exception); Respond function OOS-entirely; Implementation Tiers 1-4 OOS as organizational-maturity claims. NIST SP 800-53 Rev. 5 + CIS Critical Security Controls v8 cross-references baked into `informativeReferences`. Use `--compliance nist-csf` or `--compliance soc2,hipaa,nist-csf` for triple-framework reports from a single scan. |
313
317
  | — | **PCI DSS v4.0.1 Compliance Engine (EE 0.11.0)** | Enterprise | PCI DSS v4.0.1 (PCI SSC, June 2024 errata; supersedes v4.0 March 2022; v3.2.1 retired March 31, 2024) mapping at the auditor-canonical sub-requirement level for QSA Report on Compliance workflow — **19 covered + 9 partial + 39 OOS across 67 of ~250 sub-requirements (MVP-67 density)** (Req 7.2.2 down-rated covered→partial in EE 0.19.4 — access-by-job-classification is process/HR-gated). Req 12 Information Security Program OOS-by-design entirely. Req 5 anti-malware + Req 9 physical OOS-entirely. **Defined-vs-Customized Approach discipline per Appendix E** — 15 Defined-only sub-requirements enforced at schema layer. **Cardholder Data Environment (CDE) scope operator-attested** via CDE Data Flow Diagram per Req 1.2.4 + Req 12.5.1. **Card-brand AOC enforcement priority view** (Visa CISP / Mastercard SDP / Amex DSOP / Discover DISC). **4 load-bearing schema enrichments** per control: `controlType` + `approachEligibility` + `cloudProviderAttestation` (AWS / Azure / GCP currently-named AOCs) + `cdeScope`. CAO MVP-deferred to EE 0.11.1. Use `--compliance pci-dss` or `--compliance soc2,hipaa,nist-csf,pci-dss` for quad-framework reports from a single scan. |
314
318
  | — | **ISO/IEC 27001:2022 Compliance Engine (EE 0.12.0)** | Enterprise | ISO/IEC 27001:2022 (ISO + IEC, October 2022; 2013 edition retired October 31, 2025) Annex A mapping at the auditor-canonical per-Annex-A-code level for ISO/IEC 17021-1 certification body assessors — **17 covered + 14 partial + 62 OOS across 93 Annex A controls** (the complete Annex A universe across 4 themes: A.5 Organizational 37 + A.6 People 8 + A.7 Physical 14 + A.8 Technological 34). **Statement of Applicability per Clause 6.1.3.d discipline** — engine produces substrate for INCLUDED controls; SoA inclusion/exclusion is operator-side. **ISMS Clauses 4-10 OOS-by-design** with 7 Major Nonconformity classes (absence of internal audit per Clause 9.2 OR management review per Clause 9.3 = auto-fail Stage 2). 11 NEW 2022 controls + 5-attribute taxonomy (cybersecurityConcepts 5 categories, NOT 6 like NIST CSF) + 2013-to-2022 transition discipline + Cloud-Provider Certificate Inheritance Matrix. Use `--compliance iso-27001` or any combination for multi-framework reports from a single scan. |
@@ -317,7 +321,7 @@ Results land in `./out/<host>_<timestamp>/`:
317
321
  | — | SLA & MTTR Tracking | Enterprise | Per-severity SLA targets, compensating-control flow, finding lifecycle, Type II rolling-quarter cadence. |
318
322
  | — | Recurring-Scan Attestation | Enterprise | Multi-scan chronological matrix, cadence gap detection, scope-drift surface (CC8.1). |
319
323
  | — | GRC Platform Connector | Enterprise | Vanta + Drata + Secureframe connectors — scan-time push (opt-in), retry/backoff, deterministic idempotency, rate-limit handling, circuit breaker, foreign-token detection, ZDE egress redaction. See [GRC Connectors](#grc-connectors-vanta-drata-secureframe). |
320
- | — | WORM Evidence Storage | Enterprise | S3 Object Lock COMPLIANCE-mode + resource redaction + SHA-256 manifest. SEC 17a-4 / FINRA 4511 retention-compatible. |
324
+ | — | WORM Posture Validation | Enterprise | Reads S3 Object Lock configuration on your trail buckets + resource redaction + SHA-256 manifest — substrate for a SEC 17a-4 / FINRA 4511 claim. The archive WRITER (`prepareArchive`) is built and **not reachable**: no caller, so no first-party run writes into an immutable store. |
321
325
  | — | Tabletop Simulation | Enterprise | Probe-event manifest + SIEM detection correlation, configurable coverage bands (Type II / High-Assurance presets). |
322
326
 
323
327
  **Running EE plugins** (after `nsauditor-ai license install <key>`):
@@ -357,7 +361,7 @@ nsauditor-ai scan --host aws --plugins all --compliance soc2 --aws-region all
357
361
  - **Global services** (IAM, account-level S3 enumeration) are audited once regardless of `--aws-region`; the S3 auditors resolve **each bucket's own region** and skip + disclose buckets outside the scoped set (closing latent cross-region false-cleans).
358
362
  - **MCP `scan_cloud` (Claude Desktop / Claude Code):** the same scoping is a `regions` argument — *omit* it to scan the server-configured `AWS_REGION`, or pass `["all"]` (or a region-code list like `["us-east-1","eu-west-1"]`) to fan out. Omitting does **not** fan out, so a single tool-call stays within Desktop's timeout.
359
363
 
360
- The auditor evidence pack is emitted under `out/` — cover-page Scope Attestation, SHA-256 chain-of-custody sidecars, RFC 3161 trusted-timestamps, suppression workflow, identity verification. EE is available at [`www.nsauditor.com/ai/pricing`](https://www.nsauditor.com/ai/pricing).
364
+ The auditor evidence pack is emitted under `out/` — cover-page Scope Attestation, SHA-256 chain-of-custody sidecars, the suppression workflow and approver identity verification, plus opt-in RFC 3161 trusted-timestamps (`NSAUDITOR_TSA_URL`; never run against a real authority). Ed25519 suppression SIGNING is built and not reachable. EE is available at [`www.nsauditor.com/ai/pricing`](https://www.nsauditor.com/ai/pricing).
361
365
 
362
366
  ---
363
367
 
@@ -436,32 +440,32 @@ nsauditor-ai-mcp
436
440
  npx nsauditor-ai-mcp
437
441
  ```
438
442
 
439
- **CE Tools:**
443
+ The server registers **seven** tools and lists all seven to every client. The licence is
444
+ checked when a tool is CALLED, not when the list is served — so an unlicensed call returns
445
+ an explicit 🔒 refusal naming the tier it needs, never an empty result that reads as "nothing
446
+ found". A higher tier does not add tools; it unlocks the ones already in the list.
447
+
448
+ **Available on every tier:**
440
449
 
441
450
  | Tool | Purpose |
442
451
  |---|---|
443
- | `scan_host` | Run full scan against a host with plugin selection |
444
- | `list_plugins` | List available scanner plugins with metadata |
452
+ | `scan_host` | Run a full plugin scan against a host — service detection, OS fingerprint, structured findings |
453
+ | `list_plugins` | List available scanner plugins with their IDs, priorities and requirements |
454
+ | `compliance_matrix` | Return the shipped coverage matrix for a framework — Covered / Partial / Out of scope, with the per-group out-of-scope reasons. **Needs the Enterprise pack installed**: the matrices are its data, so on a Community-only install this fails closed with an install instruction rather than returning an empty matrix (an empty matrix is what gets filled in with a guess) |
445
455
 
446
- **Pro Tools** (requires license key + `@nsasoft/nsauditor-ai-ee`):
456
+ **Unlocked by Pro** (licence key + `@nsasoft/nsauditor-ai-ee`):
447
457
 
448
458
  | Tool | Purpose |
449
459
  |---|---|
450
- | `probe_service` | Deep scan a specific port/service |
451
- | `get_vulnerabilities` | Query CVEs by CPE string |
452
- | `risk_summary` | Prioritized risk overview from last scan |
453
- | `scan_compare` | Diff two scan results with risk weighting |
454
- | `save_finding` | Save a validated finding to the finding queue (schema-checked) |
460
+ | `probe_service` | Run one specific plugin against a single `host:port` |
461
+ | `get_vulnerabilities` | Look up known CVEs for a CPE (Common Platform Enumeration) string via the NVD API |
455
462
 
456
- **Enterprise Tools** (requires Enterprise license):
463
+ **Unlocked by Enterprise:**
457
464
 
458
465
  | Tool | Purpose |
459
466
  |---|---|
460
467
  | `scan_cloud` | Audit one or more cloud accounts (AWS / GCP / Azure) using server-configured credentials; no network host. "Audit my AWS account" / "Audit my AWS and Azure accounts". |
461
- | `start_assessment` | Multi-host orchestrated assessment workflow |
462
- | `prioritize_risks` | Cross-host risk prioritization |
463
- | `compliance_check` | Compliance mapping with gap analysis |
464
- | `export_report` | Generate formatted compliance report |
468
+ | `get_findings` | Drill into the findings of the most recent `scan_cloud` run (per-provider session cache — not live state) |
465
469
 
466
470
  > `scan_cloud` runs the requested clouds' plugins **concurrently** (default up to 20 at once, 25s per-plugin
467
471
  > timeout) so a full multi-service cloud audit completes within Claude Desktop's ~60s tool-call limit. Tune with
@@ -695,7 +699,7 @@ nsauditor-ai --version (or -v, or `version`)
695
699
  | `--output-format <fmt>` | Additional output format: `sarif` (CI/CD) · `csv` (spreadsheet) · `md` or `markdown` (chat/PR/Slack quotable) | — |
696
700
  | `--fail-on <sev>` | Exit code 2 if findings ≥ severity: `critical\|high\|medium\|low\|info` | — |
697
701
  | `--insecure-https` | Accept self-signed TLS certificates | `false` |
698
- | `--watch` | Enable CTEM continuous scanning | `false` |
702
+ | `--watch` | CTEM continuous **alerting** loop — re-scan on `--interval`, diff, webhook on `--alert-severity`. Not an evidence cadence: no retention, no cross-run aggregation, skips SARIF/CSV/Markdown + `--fail-on`, dies with the process. Use a scheduler for SOC 2 Type II history. | `false` |
699
703
  | `--interval <min>` | Rescan interval in minutes (requires `--watch`) | `60` |
700
704
  | `--webhook-url <url>` | Webhook URL for delta alerts | — |
701
705
  | `--alert-severity <sev>` | Minimum severity for webhook alerts | `high` |
@@ -817,6 +821,66 @@ NSAUDITOR_LICENSE_KEY=pro_eyJhbGci... # Pro or Enterprise license key
817
821
  NSAUDITOR_PLUGIN_PATH= # Additional plugin directories (colon-separated)
818
822
  ```
819
823
 
824
+ **Longitudinal compliance evidence (Enterprise CLI, new alongside the variables below):**
825
+
826
+ A single scan shows configuration at an instant. A SOC 2 Type II auditor — and the ISO
827
+ surveillance cadence, PCI Appendix E sampling, HIPAA §164.312(b) and GDPR Art. 32(1)(d)
828
+ equivalents — asks whether controls operated over a *period*. Every scan has always written
829
+ a per-framework `scan_attestation_<framework>.json`, so a history you already have is
830
+ aggregatable today:
831
+
832
+ ```bash
833
+ # Track remediation SLA / MTTR against a directory of prior scans
834
+ nsauditor-ai scan --host aws --compliance soc2 \
835
+ --compliance-history ./out --sla-policy ./my-sla.json
836
+
837
+ # Roll a directory of prior scans up into a multi-period attestation
838
+ nsauditor-ai compliance attest --history ./out --framework soc2 --window 12m
839
+ ```
840
+
841
+ Three things the roll-up states about itself rather than leaving you to discover:
842
+ discovery reads **one** directory level (`<history-root>/<scan-id>/`), scans whose own
843
+ attestation is marked `REPORT INVALID FOR AUDIT` are **counted and named** rather than
844
+ averaged into a clean verdict, and an empty history exits **3** with status `no_evidence` —
845
+ absence of evidence is a finding, not a pass.
846
+
847
+ **Compliance evidence (`NSAUDITOR_*` — Enterprise, all opt-in):**
848
+
849
+ Every variable added from Enterprise 0.33.0 onward carries the `NSAUDITOR_` prefix, so a
850
+ deploy can tell at a glance which environment variables belong to this product. The
851
+ unprefixed families above (`AI_*`, `OPENAI_*`, `NVD_API_KEY`, `COMPLIANCE_GRC_*`) keep
852
+ working exactly as they do today — prefixed aliases arrive in a later minor, and a silent
853
+ rename will never happen.
854
+
855
+ ```ini
856
+ NSAUDITOR_OFFLINE_ONLY=1 # Exact match on '1'. Forbids outbound: CVE matching reads a local
857
+ # NVD store and reports an explicit coverage gap rather than a silent
858
+ # clean. Also VETOES the two settings below — configuring an offline
859
+ # posture and an outbound destination together is a startup error,
860
+ # never a quiet downgrade to weaker evidence.
861
+ NSAUDITOR_TSA_URL= # RFC 3161 Time-Stamp Authority endpoint. NO DEFAULT, EVER — unset
862
+ # means the feature is absent, not "use a vendor default".
863
+ NSAUDITOR_TSA_CERT_CHAIN= # Path to the TSA certificate chain (PEM), for offline verification
864
+ NSAUDITOR_TSA_POLICY_OID= # Optional policy OID to request from the TSA
865
+ NSAUDITOR_IDENTITY_REGISTRY= # Path to the approver identity registry JSON. Binds the humans named
866
+ # in your suppression file to identities an assessor can check.
867
+ # Template ships at data/compliance/identity_registry.json (Enterprise).
868
+ NSAUDITOR_SIGNING_KEY= # A REFERENCE, never key material: keychain:LABEL | /path/to/key.pem
869
+ # (mode 0600) | awskms:alias/… — see the honesty note below.
870
+ ```
871
+
872
+ Two of these are deliberately **not** capabilities yet, and saying so here is the point:
873
+
874
+ - **`NSAUDITOR_SIGNING_KEY` is groundwork, and less wired than this note used to say.** EE 0.33.0 stopped parsing it at option-resolution time — a malformed value used to abort the EE stage for a setting nothing reads — so it is now carried through unparsed; `parseSigningRef` / `resolveSigner` hold the parse and the offline veto at the point a key would be used, and no
875
+ shipped entry point signs a suppression, so setting it changes nothing today. It exists
876
+ now because the signature record's `algorithm` and `backend` fields had to be frozen
877
+ *before* the first signature reaches a customer archive — retrofitting that later breaks
878
+ every auditor holding one.
879
+ - **`NSAUDITOR_TSA_URL` is wired but unproven.** The request is built and sent; it has
880
+ **never been exercised against a real Time-Stamp Authority** — every test to date injects
881
+ a fake, which proves the plumbing and not the capability. Treat trusted timestamping as
882
+ roadmap until that live check is published.
883
+
820
884
  **Security overrides:**
821
885
 
822
886
  ```ini
@@ -1014,7 +1078,7 @@ NSAuditor AI is built on a **Zero Data Exfiltration (ZDE)** architecture:
1014
1078
  - **No data processing.** Nsasoft US LLC never sees, stores, or processes your scan results.
1015
1079
  - **AI is opt-in.** External AI calls use your own API keys. Redaction runs locally first.
1016
1080
  - **License validation is offline.** JWT signature verified locally with an embedded public key.
1017
- - **Air-gappable, once configured for it.** Scanning, analysis, license verification and evidence-pack generation all run with no outbound network access; Enterprise adds offline CVE matching from a local NVD store under `NSAUDITOR_OFFLINE_ONLY=1`, which emits an explicit coverage gap rather than a silent clean when the store cannot answer. Stated precisely because it matters operationally: a **default** scan still attempts NVD egress unless that variable is set, and the only other outbound paths — AI enrichment and the GRC push — are opt-in and off by default. Populating the local NVD store is the operator's; no feed data is delivered with the product.
1081
+ - **Air-gappable, once configured for it.** Scanning, analysis, license verification and evidence-pack generation all run with no outbound network access; Enterprise adds offline CVE matching from a local NVD store under `NSAUDITOR_OFFLINE_ONLY=1`, which emits an explicit coverage gap rather than a silent clean when the store cannot answer. Stated precisely because it matters operationally: a **default** scan still attempts NVD egress unless that variable is set. The other outbound paths — AI enrichment, the GRC push, the continuous-monitoring webhook, and the timestamping and signing paths on the roadmap — are opt-in and off by default; the paths that are *not* optional are the scan target itself, your own cloud provider's APIs during a cloud scan, and DNS resolution of the target. All of them are enumerated with their trigger and default state in the egress register (EE `docs/architecture.md` §14.1.1), which is generated from code and guarded in both directions — deliberately, so this sentence never again has to carry a completeness claim that prose alone cannot keep true. Populating the local NVD store is the operator's; no feed data is delivered with the product.
1018
1082
 
1019
1083
  Nsasoft US LLC is not a data processor, data controller, or business associate under any data protection regulation. You own and control all data produced by NSAuditor AI.
1020
1084
 
package/cli.mjs CHANGED
@@ -660,6 +660,27 @@ export async function parseArgs(argv) {
660
660
  // (override-on) for this scan; `--aws-profile <name>` selects a named profile
661
661
  // from ~/.aws/credentials. Both are wired into main() via resolveScanEnv().
662
662
  // get() returns undefined (absent), true (value-less flag), or the string value.
663
+ // EE 0.33.0 (N2): longitudinal compliance evidence. Both are forwarded verbatim to
664
+ // EE, which owns every default and every validation — CE never parses or defaults
665
+ // them. Tri-state like `--env` and NOT the `--compliance-scope` collapse-to-null
666
+ // form: a value-less `--sla-policy` must be an error in main(), because silently
667
+ // nulling it is precisely the quiet skip this feature exists to remove.
668
+ // `--history` is the spelling the `compliance attest` subcommand documents and the one
669
+ // an operator types there; `--compliance-history` is the scan-path spelling. Both
670
+ // resolve to the same value because they name the same directory. Accepting only one
671
+ // is how a DOCUMENTED flag becomes a flag that does nothing — and that is exactly what
672
+ // shipped in the first draft here, caught by RUNNING the command rather than reading
673
+ // it. The EE guard that catches this class is scoped to /--compliance[A-Za-z-]*/, and
674
+ // `--history` does not match it.
675
+ const complianceHistoryVal = get('compliance-history') || get('compliance_history') || get('history');
676
+ args.complianceHistory = complianceHistoryVal === undefined ? undefined : complianceHistoryVal;
677
+ const slaPolicyVal = get('sla-policy') || get('sla_policy');
678
+ args.slaPolicy = slaPolicyVal === undefined ? undefined : slaPolicyVal;
679
+ const attestWindowVal = get('window');
680
+ args.attestWindow = (attestWindowVal && attestWindowVal !== true) ? attestWindowVal : null;
681
+ const frameworkVal = get('framework');
682
+ args.framework = (frameworkVal && frameworkVal !== true) ? frameworkVal : null;
683
+
663
684
  const envVal = get('env');
664
685
  args.env = envVal === undefined ? undefined : envVal; // string path, or true if value-less
665
686
  const awsProfileVal = get('aws-profile');
@@ -740,21 +761,47 @@ async function scanSingleHost(pm, host, plugins, opts, promptMode) {
740
761
  // AWS accounts. EE 0.3.2 + CE 0.1.30 ship as a paired release; mixing
741
762
  // versions is supported but the EE-side cloud-harvest behavior requires
742
763
  // CE-0.1.30+ to take effect.
764
+ // ⚠️ THE IMPORT AND THE CALL GET SEPARATE CATCHES, AND THE SPLIT IS THE POINT.
765
+ // A single bare `catch {}` around both made "EE is not installed" — a legitimate,
766
+ // expected, silent skip for every Community user — indistinguishable from "EE ran and
767
+ // THREW", which silently deletes the deliverable: the scan exits 0, writes no compliance
768
+ // report, and prints nothing. That is the false-clean shape, and it is the channel a
769
+ // whole class of defects has reached production through (a malformed env var did exactly
770
+ // this until 0.33.0 moved the parse). Absence stays silent; failure is named and the run
771
+ // is marked, because an operator who asked for `--compliance` and got no report must not
772
+ // have to diff directories to discover it.
773
+ let ee = null;
774
+ try {
775
+ ee = await import('@nsasoft/nsauditor-ai-ee');
776
+ } catch { /* EE not installed — CE proceeds unchanged. The ONLY silent case. */ }
743
777
  try {
744
- const { enrichScan } = await import('@nsasoft/nsauditor-ai-ee');
745
- const eeEnrichment = await enrichScan(conclusion, {
778
+ const eeEnrichment = ee ? await ee.enrichScan(conclusion, {
746
779
  host,
747
780
  outDir,
748
781
  compliance: opts.compliance ?? process.env.COMPLIANCE_FRAMEWORKS ?? null,
749
782
  complianceScope: opts.complianceScope ?? null,
783
+ // EE 0.33.0 (N2). Forwarded verbatim; EE owns the defaults, the validation and
784
+ // the path-or-object handling for the policy.
785
+ complianceTrackSla: opts.complianceTrackSla,
786
+ complianceHistoryRoot: opts.complianceHistoryRoot,
787
+ slaPolicy: opts.slaPolicy,
750
788
  results,
751
789
  onWarn: (msg) => console.warn(`[EE] ${msg}`),
752
- });
790
+ }) : null;
753
791
  if (eeEnrichment?.enrichedPrompt) {
754
792
  conclusion.result = conclusion.result || {};
755
793
  conclusion.result.eeEnrichment = eeEnrichment;
756
794
  }
757
- } catch { /* EE not installed — CE proceeds unchanged */ }
795
+ } catch (err) {
796
+ // EE IS PRESENT AND FAILED. Never silent: name the stage that was lost, and mark the
797
+ // conclusion so a downstream reader can tell "no findings" from "the stage that finds
798
+ // them did not run". Still non-fatal — a scan that completed its CE work is worth
799
+ // keeping — but it is now a visible degradation rather than an invisible one.
800
+ console.error(`[EE] enrichment FAILED — this scan has no compliance report, no `
801
+ + `intelligence enrichment and no analysis-agent findings: ${err?.message ?? err}`);
802
+ conclusion.result = conclusion.result || {};
803
+ conclusion.result.eeEnrichmentError = String(err?.message ?? err);
804
+ }
758
805
 
759
806
  const { file_paths: ai_file_paths, ai_conclusion, ai_status, ai_error } = await maybeSendToOpenAI({ host, results, conclusion, promptMode, outDir });
760
807
  // BUG1 (fail-VISIBLE): a one-line end-of-scan AI status so a failed/aborted AI
@@ -902,6 +949,45 @@ function maxSeverityInConclusion(conclusion) {
902
949
  * @param {object} [opts] - { cmd, frameworks, importEE(test seam) }
903
950
  * @returns {Promise<{ran:boolean, reason?:string}>}
904
951
  */
952
+ /**
953
+ * STARTUP POSTURE VETO — `NSAUDITOR_OFFLINE_ONLY=1` against a configured egress path.
954
+ *
955
+ * EE raises this as an `NsauditorConfigError` from `resolveComplianceEnvOpts`, and exports
956
+ * that function specifically so CE can fail fast at startup with the SAME definition of
957
+ * "offline". Until now nothing called it, and the veto's only route into CE was through
958
+ * `enrichScan` — straight into the bare `catch {}` on the scan path, which does not abort
959
+ * the scan: it silently drops the ENTIRE EE stage (intelligence, agents, the compliance
960
+ * report). A quiet skip wearing a fail-fast's name, one repo over from the test that
961
+ * forbids exactly that.
962
+ *
963
+ * Deliberately NOT gated on the command and NOT gated on a licence tier — a contradiction
964
+ * between two operator settings is a configuration error under every command, and
965
+ * `enrichScan` never even evaluated it below Pro tier. `tests/cli_posture_preflight.test.mjs`
966
+ * pins both of those as decisions rather than accidents, and explains why this exits 2 while
967
+ * the GRC preflight below exits 1.
968
+ *
969
+ * EE stays optional: an unresolvable EE means no EE egress paths to veto, and that is a
970
+ * reason, not a failure.
971
+ *
972
+ * @param {Record<string,string|undefined>} env
973
+ * @param {{importEE?: Function}} [opts] `importEE` is the test seam — these cases must not
974
+ * require EE to be installed, the same convention `preflightGrcIfRequested` uses.
975
+ * @returns {Promise<{ran: boolean, reason?: string}>}
976
+ * @throws the EE `NsauditorConfigError` verbatim, so main() can print it and exit 2
977
+ */
978
+ export async function preflightNsauditorPosture(env, opts = {}) {
979
+ const { importEE } = opts;
980
+ let ee;
981
+ try {
982
+ ee = importEE ? await importEE() : await import('@nsasoft/nsauditor-ai-ee');
983
+ } catch {
984
+ return { ran: false, reason: 'ee-unavailable' };
985
+ }
986
+ if (typeof ee?.resolveComplianceEnvOpts !== 'function') return { ran: false, reason: 'ee-too-old' };
987
+ ee.resolveComplianceEnvOpts(env);
988
+ return { ran: true };
989
+ }
990
+
905
991
  export async function preflightGrcIfRequested(env, opts = {}) {
906
992
  const { cmd, frameworks, importEE } = opts;
907
993
  if (cmd !== 'scan') return { ran: false, reason: 'not-scan' };
@@ -922,7 +1008,7 @@ export async function preflightGrcIfRequested(env, opts = {}) {
922
1008
 
923
1009
  export async function main() {
924
1010
  const args = await parseArgs(process.argv);
925
- const { cmd, host, plugins, insecureHttps, hostFile, parallel, failOn, outputFormat, watch, intervalMinutes, webhookUrl, alertSeverity, ports, compliance, complianceScope, awsRegion } = args;
1011
+ const { cmd, host, plugins, insecureHttps, hostFile, parallel, failOn, outputFormat, watch, intervalMinutes, webhookUrl, alertSeverity, ports, compliance, complianceScope, complianceHistory, slaPolicy, attestWindow, framework, awsRegion } = args;
926
1012
 
927
1013
  // Version: handled before license verification so it works without a key.
928
1014
  // CE-0.1.30.1 — closes the discovery-flag UX gap where pre-fix
@@ -965,7 +1051,14 @@ Scan options:
965
1051
  --fail-on <severity> Exit non-zero if any finding ≥ severity
966
1052
  --output-format <fmt> Additional report format: sarif | csv | md
967
1053
  --insecure-https Skip TLS validation on probed HTTPS targets
968
- --watch CTEM continuous mode
1054
+ --watch CTEM continuous ALERTING mode: re-scan on --interval,
1055
+ diff against the previous cycle, fire --webhook-url when a
1056
+ change crosses --alert-severity. NOT an evidence cadence —
1057
+ it adds no retention or cross-run aggregation, skips SARIF/
1058
+ CSV/Markdown output and --fail-on, and dies with this process.
1059
+ Each tick is an ordinary scan, so with --compliance it writes
1060
+ that tick's artifacts; nothing relates them across ticks. For
1061
+ SOC 2 Type II history use a scheduler (cron/systemd/CI).
969
1062
  --interval <minutes> Watch interval (default 60)
970
1063
  --webhook-url <url> Send delta alerts (must be public; private/loopback blocked)
971
1064
  --alert-severity <sev> Min severity to alert on (default: high)
@@ -973,6 +1066,22 @@ Scan options:
973
1066
  of soc2,hipaa,nist-csf,pci-dss,iso-27001,cis-v8,gdpr (aliases
974
1067
  nist/pci/iso/cis). Unknown tokens fail fast. Enterprise only.
975
1068
  --compliance-scope <path> JSON file describing the assessment scope
1069
+ --compliance-history <dir> Directory of prior scans (one subdirectory per scan). Turns on
1070
+ SLA/MTTR longitudinal tracking against that history. Enterprise.
1071
+ --sla-policy <path> JSON file of SLA thresholds per severity; defaults to the
1072
+ shipped data/compliance/sla.json. Turns SLA tracking on by
1073
+ itself. Relative paths resolve against the current
1074
+ directory. Enterprise.
1075
+
1076
+ Compliance subcommands:
1077
+ nsauditor-ai compliance attest --history <dir> [--framework <fw>] [--window 6m|12m|90d]
1078
+ Aggregate the per-scan attestation records in
1079
+ <dir> into a multi-period (Type II) recurring-scan
1080
+ attestation. Reads scan_attestation_<fw>.json from
1081
+ each subdirectory, so a history you already have is
1082
+ aggregatable. Discovery is ONE level deep and the
1083
+ report says so. Exits 3 when no evidence is found —
1084
+ an empty history is a finding, not a pass.
976
1085
 
977
1086
  License subcommands:
978
1087
  nsauditor-ai license install <KEY> Verify and persist a license key (Keychain
@@ -1017,7 +1126,9 @@ Environment:
1017
1126
  NSA_ALLOW_ALL_HOSTS=1 Permit RFC1918 / loopback (local-network auditing)
1018
1127
  CLOUD_PROVIDER=aws|gcp|azure Required for cloud scanner plugins (020/021/022/023/030)
1019
1128
  AI_PROVIDER=openai|claude|ollama AI provider for report generation
1020
- COMPLIANCE_TSA_URL RFC 3161 timestamp authority for SOC 2 attestation
1129
+ NSAUDITOR_TSA_URL RFC 3161 timestamp authority for compliance attestation.
1130
+ No default, ever — unset means the feature is absent.
1131
+ Refused at startup together with NSAUDITOR_OFFLINE_ONLY=1.
1021
1132
 
1022
1133
  Cloud-scan hosts:
1023
1134
  --host aws[,gcp,azure] One or more cloud sentinel literals, comma-separated
@@ -1039,8 +1150,10 @@ Cloud-scan hosts:
1039
1150
  is safe here.
1040
1151
  Note: the composite zero-trust checker (1023) has no
1041
1152
  single cloud and is therefore skipped under this
1042
- auto-scope; to run it, select it explicitly
1043
- (--plugins 023,...) or scan a network host/CIDR.
1153
+ auto-scope. It runs on a NETWORK host/CIDR scan with
1154
+ the full plugin set; selecting it by id on its own
1155
+ does not work, because it needs a discovery plugin to
1156
+ confirm the host is up first.
1044
1157
  INVERSE (cloud scope integrity): a cloud auditor runs
1045
1158
  ONLY on its own sentinel host. On a NETWORK host (IP /
1046
1159
  CIDR / hostname) the cloud-scanner plugins NEVER run —
@@ -1109,6 +1222,17 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
1109
1222
  // --env load) so a bad token / control-map / provider fails fast instead of after a
1110
1223
  // full scan. The helper gates on the exact push condition (so a framework-less recon
1111
1224
  // scan / non-scan command is never hard-failed) + silently skips if EE isn't installed.
1225
+ // Posture veto FIRST — before the GRC preflight and before any scan work. An operator who
1226
+ // has forbidden outbound connections and also configured one must hear about it here, not
1227
+ // from an EE stage that a bare catch would drop on the floor. Exit 2 matches
1228
+ // `compliance attest`, the other door into the same veto.
1229
+ try {
1230
+ await preflightNsauditorPosture(process.env);
1231
+ } catch (err) {
1232
+ console.error(`Fatal: ${err.message}`);
1233
+ process.exit(2);
1234
+ }
1235
+
1112
1236
  try {
1113
1237
  await preflightGrcIfRequested(process.env, { cmd, frameworks: args.compliance ?? process.env.COMPLIANCE_FRAMEWORKS });
1114
1238
  } catch (err) {
@@ -1173,12 +1297,22 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
1173
1297
  console.log(` nsauditor-ai (CE): ${TOOL_VERSION}`);
1174
1298
  console.log(` @nsasoft/nsauditor-ai-ee (EE): ${_eeVersion}`);
1175
1299
  } else if (rawArgs.includes('--capabilities')) {
1300
+ const { CAPABILITIES } = await import('./utils/capabilities.mjs');
1176
1301
  const tier = getTierFromEnv();
1177
1302
  const caps = resolveCapabilities(tier);
1178
1303
  console.log(`Active capabilities for tier: ${tier}\n`);
1304
+ // ⚠️ PRINT THE DESCRIPTION, NOT JUST THE NAME. This list is read back by assistants,
1305
+ // and a bare identifier is a claim with no text behind it: `✓ airGapped` was expanded
1306
+ // into "air-gapped deployment" — a withdrawn phrase — and `✓ pdfExport` into "the
1307
+ // output is auditor-consumable via pdfExport", for a function that throws. A reader
1308
+ // given reviewed text quotes it; a reader given an identifier invents one.
1179
1309
  for (const [name, enabled] of Object.entries(caps)) {
1310
+ const desc = CAPABILITIES[name] && CAPABILITIES[name].desc;
1180
1311
  console.log(` ${enabled ? '✓' : '✗'} ${name}`);
1312
+ if (desc) console.log(` ${desc}`);
1181
1313
  }
1314
+ console.log('\nEach line above states what the capability DOES. Quote these descriptions '
1315
+ + 'rather than expanding the flag name — the name is an identifier, not a claim.');
1182
1316
  } else if (rawArgs.includes('--plugins')) {
1183
1317
  // CE-0.1.30.3 — real enumeration of discovered plugins, grouped by
1184
1318
  // source (CE / EE / custom NSAUDITOR_PLUGIN_PATH). Pre-fix this
@@ -1979,6 +2113,55 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
1979
2113
  process.exit(exitCode);
1980
2114
  }
1981
2115
 
2116
+ // ── `compliance attest` (EE 0.33.0, N2) ───────────────────────────────────
2117
+ // Placed BEFORE the unknown-command guard, or it would be dead code that exits 2.
2118
+ // Deliberately a THIN forward: EE owns framework validation, the offline posture
2119
+ // veto, framework-alias resolution and every default. CE contributes the flag
2120
+ // surface and the exit code — nothing here can drift out of sync with the engine
2121
+ // because nothing here decides anything.
2122
+ if (cmd === 'compliance') {
2123
+ const sub = process.argv[3];
2124
+ if (sub !== 'attest') {
2125
+ console.error('Usage: nsauditor-ai compliance attest --history <dir> [--framework <fw>] [--window 6m|12m|90d]');
2126
+ process.exit(2);
2127
+ }
2128
+ if (typeof complianceHistory !== 'string' || complianceHistory.length === 0) {
2129
+ console.error('Fatal: compliance attest requires --history <dir>');
2130
+ process.exit(2);
2131
+ }
2132
+ let runAttestCommand;
2133
+ try {
2134
+ ({ runAttestCommand } = await import('@nsasoft/nsauditor-ai-ee'));
2135
+ } catch {
2136
+ console.error('compliance attest requires the Enterprise package (@nsasoft/nsauditor-ai-ee).');
2137
+ process.exit(2);
2138
+ }
2139
+ try {
2140
+ const { report, files, exitCode } = await runAttestCommand({
2141
+ // EE gates on this, exactly as it does for a compliance scan. CE resolves the
2142
+ // tier it already resolved for `--compliance`; presenting it here rather than
2143
+ // letting EE default keeps ONE definition of "Enterprise" across both paths.
2144
+ capabilities: resolveCapabilities(getTierFromEnv()),
2145
+ rootDir: complianceHistory,
2146
+ outDir: resolveBaseOutDir(),
2147
+ framework: framework || compliance || 'soc2',
2148
+ window: attestWindow || '6m',
2149
+ onWarn: (msg) => console.warn(`[EE] ${msg}`),
2150
+ });
2151
+ console.log(`Recurring attestation (${report.framework}): ${report.summary.scanCount} scans in window, status ${report.summary.complianceStatus}`);
2152
+ if (report.summary.invalidForAudit > 0) {
2153
+ console.warn(` ${report.summary.invalidForAudit} of them are marked REPORT INVALID FOR AUDIT by their own attestation — see scansWithWarnings`);
2154
+ }
2155
+ for (const f of files) console.log(` wrote ${f.path ?? f}`);
2156
+ process.exit(exitCode);
2157
+ } catch (err) {
2158
+ // A posture contradiction or an unknown framework arrives here. It must NOT be
2159
+ // swallowed: the whole point of the veto is that it stops the run loudly.
2160
+ console.error(`Fatal: ${err.message}`);
2161
+ process.exit(2);
2162
+ }
2163
+ }
2164
+
1982
2165
  if (cmd !== 'scan') {
1983
2166
  console.error(`Unknown command: ${cmd}`);
1984
2167
  process.exit(2);
@@ -2007,6 +2190,30 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
2007
2190
  if (ports) opts.ports = ports;
2008
2191
  if (compliance) opts.compliance = compliance;
2009
2192
  if (complianceScope) opts.complianceScope = complianceScope;
2193
+ // A value-less flag is a mistake worth stopping for: `--sla-policy` with no path
2194
+ // would otherwise scan on and produce a report indistinguishable from one where the
2195
+ // operator never asked for SLA tracking at all.
2196
+ for (const [flag, val] of [['--compliance-history', complianceHistory], ['--sla-policy', slaPolicy]]) {
2197
+ if (val === true) {
2198
+ console.error(`Fatal: ${flag} requires a value`);
2199
+ process.exit(2);
2200
+ }
2201
+ }
2202
+ if (typeof complianceHistory === 'string' && complianceHistory.length > 0) {
2203
+ opts.complianceHistoryRoot = complianceHistory;
2204
+ // The history root IS the request: asking for a history and not tracking against
2205
+ // it would be a flag that reads as doing something and does nothing.
2206
+ opts.complianceTrackSla = true;
2207
+ }
2208
+ if (typeof slaPolicy === 'string' && slaPolicy.length > 0) {
2209
+ opts.slaPolicy = slaPolicy;
2210
+ // …and it TURNS TRACKING ON. Setting the policy alone was a byte-identical no-op:
2211
+ // the whole SLA block is gated on complianceTrackSla, so an operator who passed
2212
+ // --sla-policy and nothing else got a report indistinguishable from passing nothing,
2213
+ // with no warning in either channel. A flag that reads as configuring a feature must
2214
+ // not silently require a second flag to have any effect.
2215
+ opts.complianceTrackSla = true;
2216
+ }
2010
2217
  if (awsRegionIntent) opts.awsRegionIntent = awsRegionIntent;
2011
2218
  const pm = await PluginManager.create(`${__dirname}/plugins`);
2012
2219
  const promptMode = String(process.env.OPENAI_PROMPT_MODE || 'basic').toLowerCase().trim();
package/mcp_server.mjs CHANGED
@@ -29,6 +29,8 @@ import { resolveCapabilities } from './utils/capabilities.mjs';
29
29
  import { buildMarkdownReport } from './utils/report_md.mjs';
30
30
  import { summarizeCloudFindings, renderCloudFindingsMarkdown, describeFinding, RESOURCE_KEYS } from './utils/cloud_finding_summary.mjs';
31
31
  import { authorizeMcpServerStartup, getMcpAuthKeyAge, getRotationWarningDays, reportMcpAuthSource } from './utils/mcp_auth.mjs';
32
+ import _nodeFs from 'node:fs';
33
+ import _nodePath from 'node:path';
32
34
 
33
35
  const _require = createRequire(import.meta.url);
34
36
  const { version: TOOL_VERSION } = _require('./package.json');
@@ -68,7 +70,7 @@ function requireProCapability(toolName) {
68
70
  return {
69
71
  content: [{
70
72
  type: 'text',
71
- text: `🔒 **${toolName}** requires a Pro license.\n\nView Pro/Enterprise pricing at https://www.nsauditor.com/ai/pricing/\n\n**CE tools available:** scan_host, list_plugins`,
73
+ text: `🔒 **${toolName}** requires a Pro license.\n\nView Pro/Enterprise pricing at https://www.nsauditor.com/ai/pricing/\n\n**CE tools available:** scan_host, list_plugins, compliance_matrix`,
72
74
  }],
73
75
  isError: true,
74
76
  };
@@ -79,7 +81,7 @@ export function requireEnterpriseCapability(toolName) {
79
81
  return {
80
82
  content: [{
81
83
  type: 'text',
82
- text: `🔒 **${toolName}** (cloud account auditing) requires an Enterprise license.\n\nView Enterprise pricing at https://www.nsauditor.com/ai/pricing/\n\n**CE tools available:** scan_host, list_plugins`,
84
+ text: `🔒 **${toolName}** (cloud account auditing) requires an Enterprise license.\n\nView Enterprise pricing at https://www.nsauditor.com/ai/pricing/\n\n**CE tools available:** scan_host, list_plugins, compliance_matrix`,
83
85
  }],
84
86
  isError: true,
85
87
  };
@@ -209,6 +211,29 @@ function appendCallSentinel(text, callId) {
209
211
  // Max findings per page — also referenced in the TOOLS inputSchema description below.
210
212
  const GET_FINDINGS_MAX_LIMIT = 20; // ~500 chars/finding -> ~10KB page (Desktop 60s budget)
211
213
 
214
+ // ---------------------------------------------------------------------------
215
+ // FRAMEWORK_STEMS — the ONE CE-side name for the shipped compliance framework set.
216
+ //
217
+ // ⚠️ WHY A CE-SIDE CONSTANT AND NOT AN EE IMPORT. EE owns this set
218
+ // (`@nsasoft/nsauditor-ai-ee/utils/framework_ids.mjs` → FRAMEWORK_STEMS) and that specifier IS
219
+ // exported and resolvable — but EE is not a CE dependency at all: CE must load and serve its
220
+ // tool registry with EE absent. `TOOLS` below is built at MODULE LOAD, so importing EE here
221
+ // would make the tool REGISTRY itself conditional on an optional package, which is a strictly
222
+ // worse failure than a stale list (no tools at all, on the customers who never buy EE).
223
+ // The equality is therefore enforced where a hard EE dependency is allowed — at test time, by
224
+ // tests/framework_stem_parity.test.mjs, which holds this array in TWO-WAY equality with both
225
+ // EE's declaration and the on-disk `data/compliance/*.json` census, and which FAILS (never
226
+ // skips) when EE is unresolvable.
227
+ //
228
+ // It is declared HERE, above TOOLS, because the set had drifted into FOUR transcriptions: the
229
+ // compliance_matrix enum, the handler's validation list, this suite's own copy — and the tool
230
+ // DESCRIPTION, which is the load-bearing one. A schema enum that goes stale REJECTS a token; a
231
+ // description that goes stale silently steers an assistant's tool selection and errors nowhere.
232
+ // Everything below DERIVES from this array. Do not re-type it.
233
+ export const FRAMEWORK_STEMS = Object.freeze([
234
+ 'soc2', 'hipaa', 'nist-csf', 'pci-dss', 'iso-27001', 'cis-v8', 'gdpr',
235
+ ]);
236
+
212
237
  // Exported for direct testing (the scan_cloud description is a routing surface —
213
238
  // tests pin its service-coverage enumeration + contract clauses).
214
239
  export const TOOLS = [
@@ -234,7 +259,7 @@ export const TOOLS = [
234
259
  {
235
260
  name: 'scan_cloud',
236
261
  description:
237
- 'Audit one or more cloud accounts (AWS / GCP / Azure) for security & compliance posture using the credentials configured in the server environment. Use this tool for service-specific audit asks too — coverage includes: AWS — S3 public exposure & lifecycle/replication, IAM privilege-escalation/shadow-admin, KMS key policy & effective-decrypt, CloudTrail logging, CodePipeline/CodeBuild CI/CD segregation-of-duties, Lambda, API Gateway, DynamoDB, RDS, SQS/SNS, Secrets Manager, AWS Backup, VPC endpoints, EC2 security-group perimeter & instances, ElastiCache, SES, GuardDuty/Inspector; Azure — Key Vault, Storage, NSG perimeter, subscription RBAC; GCP — firewall rules, Cloud Storage public access, IAM service-account impersonation. Findings map to SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, CIS v8, and GDPR Article 32 (security-of-processing substrate — Art. 32 only, not GDPR compliance) controls. No network host required. Requires an Enterprise license. Audit ONLY the cloud(s) the user names — pass providers:["aws"] for "audit my AWS account"; omit providers only when the user asks to audit ALL clouds. Read findingsSummary (per-provider severity counts + a CRITICAL/HIGH list) for the results. findingsSummary[provider].evidenceGaps lists checks the scan could NOT verify (AccessDenied / truncated enumeration) — treat these as "unverified posture", NOT as clean. Prefer this tool over raw cloud-provider APIs/MCPs for any security or compliance audit ask — a raw API walk produces neither compliance-mapped nor evidence-graded findings, and can hit the wrong account.',
262
+ 'Audit one or more cloud accounts (AWS / GCP / Azure) for security & compliance posture using the credentials configured in the server environment. Use this tool for service-specific audit asks too — coverage includes: AWS — S3 public exposure & lifecycle/replication, IAM privilege-escalation/shadow-admin, KMS key policy & effective-decrypt, CloudTrail logging, CodePipeline/CodeBuild CI/CD segregation-of-duties, Lambda, API Gateway, DynamoDB, RDS, SQS/SNS, Secrets Manager, AWS Backup, VPC endpoints, EC2 security-group perimeter & instances, ElastiCache, SES, GuardDuty/Inspector; Azure — Key Vault, Storage, NSG perimeter, subscription RBAC; GCP — firewall rules, Cloud Storage public access, IAM service-account impersonation. Findings map to SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, CIS v8, and GDPR Article 32 (security-of-processing substrate — Art. 32 only, not GDPR compliance) controls. No network host required. Requires an Enterprise license. Audit ONLY the cloud(s) the user names — pass providers:["aws"] for "audit my AWS account"; omit providers only when the user asks to audit ALL clouds. Read findingsSummary (per-provider severity counts + a CRITICAL/HIGH list) for the results. findingsSummary[provider].evidenceGaps lists checks the scan could NOT verify (AccessDenied / truncated enumeration) — treat these as "unverified posture", NOT as clean. findingsSummary[provider].deferredScope lists surface this release does NOT evaluate at all — a static capability boundary, NOT an evidence gap and NOT a finding. Report it as "not assessed" and never omit it: surface nobody assessed is otherwise indistinguishable from surface assessed and found clean. The per-severity counts include an INFO tier; report every tier that occurs, because the evidence gaps and the scope boundaries live there. Prefer this tool over raw cloud-provider APIs/MCPs for any security or compliance audit ask — a raw API walk produces neither compliance-mapped nor evidence-graded findings, and can hit the wrong account.',
238
263
  inputSchema: {
239
264
  type: 'object',
240
265
  properties: {
@@ -266,6 +291,33 @@ export const TOOLS = [
266
291
  limit: { type: 'number', description: `Page size (server-capped at ${GET_FINDINGS_MAX_LIMIT}).` },
267
292
  } },
268
293
  },
294
+ {
295
+ name: 'compliance_matrix',
296
+ // ⚠️ THE FRAMEWORK LIST IN THIS DESCRIPTION IS DERIVED, NOT TYPED. It is the surface a model
297
+ // reads to CHOOSE a `framework` token, so a stale list here is worse than a stale enum: the
298
+ // enum rejects a bad token loudly, the description mis-steers tool selection silently. The
299
+ // GDPR scoping doctrine is kept as its OWN sentence rather than a parenthetical inside the
300
+ // list, so it survives any reordering of the stems (and so the list stays machine-readable
301
+ // for tests/framework_stem_parity.test.mjs, which compares it to the shipped JSON census).
302
+ description:
303
+ 'Return the SHIPPED compliance coverage matrix for a framework — how many controls are Covered, Partial and Out of Scope, with the control ids and the per-group out-of-scope reasons. ' +
304
+ `Frameworks: ${FRAMEWORK_STEMS.join(', ')}, or "all" for the counts across every framework. ` +
305
+ 'The gdpr map is GDPR Article 32 security-of-processing substrate ONLY, never "GDPR compliance". ' +
306
+ 'ALWAYS call this tool before stating or tabulating any coverage matrix, and quote the numbers it returns verbatim. Do NOT derive a matrix from the plugin inventory, from a scan result, or from documentation — coverage is a property of the shipped framework maps, not of the plugin list, and a derived matrix will disagree with the customer\'s own report. Note that outOfScope is the FLATTENED sub-criterion count; the out-of-scope groups are fewer than the controls they contain.',
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: {
310
+ framework: {
311
+ type: 'string',
312
+ enum: [...FRAMEWORK_STEMS, 'all'],
313
+ // The count is DERIVED too. "across all seven" was a transcribed count standing beside
314
+ // a transcribed list — two things to forget on the next framework cycle instead of none.
315
+ description: `Which framework matrix to return. Omit or pass "all" for the counts across all ${FRAMEWORK_STEMS.length} shipped frameworks.`,
316
+ },
317
+ },
318
+ required: [],
319
+ },
320
+ },
269
321
  {
270
322
  name: 'probe_service',
271
323
  description:
@@ -638,6 +690,123 @@ export async function handleListPlugins() {
638
690
  }
639
691
 
640
692
  /** Map tool name to handler. Exported for testing. */
693
+ // ---------------------------------------------------------------------------
694
+ // compliance_matrix — the SHIPPED coverage matrix, derived at call time.
695
+ //
696
+ // Gate-3 prompt 5 published a wrong SOC 2 matrix (5/18/38 = 61 against a shipped
697
+ // 10/4/37 = 51) and its own note gave the reason: "No MCP tool returns the coverage
698
+ // matrix." With no authoritative surface, an assistant asked for an exact matrix will
699
+ // derive one from whatever it CAN see — here, the plugin inventory. This tool does not
700
+ // supply knowledge; it removes the option to synthesise.
701
+ //
702
+ // TWO RULES, both load-bearing:
703
+ // 1. DERIVE, never transcribe. The numbers are read out of the shipped framework JSON on
704
+ // every call. A constant here would rot exactly like the transcribed matrices this repo
705
+ // keeps catching — inside the instrument built to stop them.
706
+ // 2. FAIL CLOSED. `plugin_discovery.mjs` swallows an unresolvable EE into `catch {}` so CE
707
+ // runs standalone, and copying that here would answer "no matrix" — which is precisely
708
+ // the void an assistant fills with a synthesised one.
709
+ //
710
+ // The framework set this handler validates against is the module-level FRAMEWORK_STEMS declared
711
+ // above TOOLS — the SAME array the enum and the tool description are built from. It used to be
712
+ // a second copy right here, which is how rule 1 was being obeyed for the NUMBERS and quietly
713
+ // broken for the framework LIST inside the very tool built to stop transcription rot.
714
+
715
+ function _defaultResolveEE() {
716
+ // `@nsasoft/nsauditor-ai-ee/data/compliance/*.json` is NOT importable — EE's `exports` map
717
+ // publishes ./plugins/*, ./utils/*, ./agents/*, ./mcp/* and no ./data/*, so a direct
718
+ // specifier throws ERR_PACKAGE_PATH_NOT_EXPORTED even though `files[]` ships the directory.
719
+ // Resolve the manifest and path-join, the same way plugin_discovery.mjs finds ./plugins.
720
+ return _nodePath.dirname(_require.resolve('@nsasoft/nsauditor-ai-ee/package.json'));
721
+ }
722
+
723
+ function _readFrameworkSummary(eeRoot, stem) {
724
+ const file = _nodePath.join(eeRoot, 'data', 'compliance', `${stem}.json`);
725
+ const raw = JSON.parse(_nodeFs.readFileSync(file, 'utf8'));
726
+ const cs = raw.coverageSummary || {};
727
+ const covered = Array.isArray(cs.covered) ? cs.covered : [];
728
+ const partial = Array.isArray(cs.partial) ? cs.partial : [];
729
+ const groups = Array.isArray(cs.outOfScope) ? cs.outOfScope : [];
730
+ // ⚠️ `outOfScope` is an array of GROUPS ({ids, title, reason}), not of ids. soc2's 11
731
+ // groups flatten to 37 ids; publishing `groups.length` yields a plausible-looking 10/4/11.
732
+ const oosIds = groups.reduce((n, g) => n + (Array.isArray(g.ids) ? g.ids.length : 0), 0);
733
+ // FAIL CLOSED ON EMPTY DATA, not just on a missing package (added at review). Every default
734
+ // above degrades to [], so a file that parses but carries no usable coverageSummary produced
735
+ // `0 covered / 0 partial / 0 out-of-scope` — published with the same authority as a real
736
+ // matrix, over a framework whose file was simply unreadable. A tool built to stop an
737
+ // assistant synthesising a matrix must not hand it a zero to synthesise around.
738
+ if (covered.length + partial.length + oosIds === 0) {
739
+ throw new Error(
740
+ `the shipped coverage matrix for '${stem}' could not be read: ${file} has no usable ` +
741
+ 'coverageSummary (covered / partial / outOfScope are all empty). This is a broken or ' +
742
+ 'incompatible Enterprise install, NOT a framework with zero coverage. Reinstall ' +
743
+ '@nsasoft/nsauditor-ai-ee and restart the MCP server. Do NOT reconstruct the matrix from ' +
744
+ 'the plugin inventory or from documentation.',
745
+ );
746
+ }
747
+ return {
748
+ framework: stem,
749
+ label: raw.frameworkLabel || raw.framework || stem,
750
+ version: raw.version || null,
751
+ covered: covered.length,
752
+ partial: partial.length,
753
+ outOfScope: oosIds,
754
+ total: covered.length + partial.length + oosIds,
755
+ coveredIds: covered,
756
+ partialIds: partial,
757
+ outOfScopeGroups: groups.map((g) => ({ ids: g.ids || [], title: g.title || null, reason: g.reason || null })),
758
+ };
759
+ }
760
+
761
+ export async function handleComplianceMatrix(args = {}) {
762
+ const which = args.framework == null ? 'all' : String(args.framework);
763
+ if (which !== 'all' && !FRAMEWORK_STEMS.includes(which)) {
764
+ throw new Error(`unknown framework '${which}' (valid: ${FRAMEWORK_STEMS.join(', ')}, all)`);
765
+ }
766
+ const resolveEE = typeof args._resolveEE === 'function' ? args._resolveEE : _defaultResolveEE;
767
+ let eeRoot;
768
+ try {
769
+ eeRoot = resolveEE();
770
+ } catch {
771
+ throw new Error(
772
+ 'The compliance coverage matrix is shipped with the Enterprise plugin pack, and ' +
773
+ '@nsasoft/nsauditor-ai-ee is not installed / not resolvable from this server. ' +
774
+ 'Install it (npm i -g @nsasoft/nsauditor-ai-ee) and restart the MCP server. ' +
775
+ 'Do NOT reconstruct the matrix from the plugin inventory or from documentation — ' +
776
+ 'coverage is a property of the shipped framework maps, not of the plugin list.',
777
+ );
778
+ }
779
+
780
+ if (which !== 'all') return _readFrameworkSummary(eeRoot, which);
781
+
782
+ // The all-frameworks view drops the per-group `reason` prose: keeping it for all seven is
783
+ // ~52 KB against a ~10 KB per-response norm here, and a response an assistant truncates is
784
+ // a response an assistant partially synthesises.
785
+ const frameworks = {};
786
+ const unavailable = [];
787
+ for (const stem of FRAMEWORK_STEMS) {
788
+ try {
789
+ const { coveredIds, partialIds, outOfScopeGroups, ...counts } = _readFrameworkSummary(eeRoot, stem);
790
+ frameworks[stem] = counts;
791
+ } catch (err) {
792
+ // Partial-tolerant, but never silent: a framework that could not be read is NAMED, so a
793
+ // caller can tell "this framework has no coverage" from "this framework was not readable".
794
+ unavailable.push({ framework: stem, reason: err && err.message ? err.message : String(err) });
795
+ }
796
+ }
797
+ if (!Object.keys(frameworks).length) {
798
+ throw new Error('no shipped coverage matrix could be read from the Enterprise pack at ' +
799
+ `${eeRoot}: ${unavailable.map((u) => u.framework).join(', ')}. Do NOT reconstruct the matrix.`);
800
+ }
801
+ return {
802
+ frameworks,
803
+ ...(unavailable.length ? { unavailable } : {}),
804
+ note: 'Counts are derived from the shipped data/compliance/<framework>.json coverageSummary at call time. ' +
805
+ 'outOfScope is the FLATTENED sub-criterion count, not the number of out-of-scope groups. ' +
806
+ 'Call this tool with a single framework to get the control ids and the per-group out-of-scope reasons.',
807
+ };
808
+ }
809
+
641
810
  export const toolHandlers = {
642
811
  scan_host: handleScanHost,
643
812
  scan_cloud: handleScanCloud,
@@ -645,6 +814,7 @@ export const toolHandlers = {
645
814
  probe_service: handleProbeService,
646
815
  get_vulnerabilities: handleGetVulnerabilities,
647
816
  list_plugins: handleListPlugins,
817
+ compliance_matrix: handleComplianceMatrix,
648
818
  };
649
819
 
650
820
  // ---------------------------------------------------------------------------
@@ -755,7 +925,7 @@ export function createServer() {
755
925
  `If versions in this response don't match your shell, the response was\n` +
756
926
  `AI-generated rather than retrieved from the MCP server (see CE 0.1.33 advisory).`;
757
927
 
758
- const tierSuffix = `\n\nCurrent tier: ${tierLabel[_tier] ?? _tier}. ${_capabilities.proMCP ? '' : 'Upgrade to Pro for probe_service, get_vulnerabilities, risk_summary, and more.'}`;
928
+ const tierSuffix = `\n\nCurrent tier: ${tierLabel[_tier] ?? _tier}. ${_capabilities.proMCP ? '' : 'Upgrade to Pro for probe_service and get_vulnerabilities.'}`;
759
929
 
760
930
  return {
761
931
  content: [{ type: 'text', text: appendCallSentinel(JSON.stringify(result, null, 2) + tierSuffix + versionLines, callId) }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "Modular AI-assisted network security audit platform — Community Edition",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,34 +1,58 @@
1
1
  // utils/capabilities.mjs
2
2
 
3
+ /**
4
+ * The capability registry.
5
+ *
6
+ * ⚠️ EVERY FLAG CARRIES A `desc`, AND THAT IS A CLAIM-HONESTY MECHANISM, NOT DOCUMENTATION.
7
+ * Until 0.32.11 the product shipped bare flag NAMES: `license --capabilities` printed
8
+ * `✓ airGapped` and nothing else. An assistant asked what the product does then EXPANDS the
9
+ * identifier — and "airGapped" expands to "air-gapped deployment", the exact phrase withdrawn
10
+ * from the README, the skill, six web surfaces and the Marketplace listing. That is a claim
11
+ * surface with NO TEXT TO SWEEP: at sweep time the claim exists only as an identifier, so no
12
+ * gate can see it, and it is regenerated fresh on every reading. A `desc` written in the
13
+ * reviewed register is what a reader quotes INSTEAD of inventing one — Gate-3 prompt 7
14
+ * demonstrated the converse, where reviewed text survived retelling verbatim.
15
+ *
16
+ * So: when adding a flag, write its `desc` in the register the claim guards allow, and say
17
+ * what the code DOES, not what the name suggests.
18
+ *
19
+ * Keep in lockstep with EE index.mjs EE_CAPABILITIES + license-manager keygen.mjs +
20
+ * licensing/api.sls.licensing keygenclass.js (the DEPLOYED signer — a flag left there is
21
+ * still minted into customer JWTs, which is what an assistant actually reads).
22
+ */
3
23
  export const CAPABILITIES = {
4
24
  // CE (always available)
5
- coreScanning: { tier: 'ce' },
6
- aiAnalysis: { tier: 'ce' }, // Any provider (OpenAI/Claude/Ollama), basic prompts
7
- basicCTEM: { tier: 'ce' },
8
- basicRedaction: { tier: 'ce' },
9
- basicMCP: { tier: 'ce' },
10
- findingQueue: { tier: 'ce' },
25
+ coreScanning: { tier: 'ce', desc: 'Network and host scanning plugins (ports, protocols, TLS, OS fingerprinting).' },
26
+ aiAnalysis: { tier: 'ce', desc: 'AI analysis of scan output via any configured provider (OpenAI / Claude / Ollama), basic prompts.' },
27
+ basicCTEM: { tier: 'ce', desc: 'Continuous watch mode: interval re-scan, delta report, webhook alert. An ALERTING loop — it adds no evidence retention or cross-run aggregation.' },
28
+ basicRedaction: { tier: 'ce', desc: 'Redaction of secrets and identifiers from AI prompt payloads before egress.' },
29
+ basicMCP: { tier: 'ce', desc: 'MCP tools scan_host, list_plugins and compliance_matrix (the coverage matrix is ungated — it reports what the installed framework maps say).' },
30
+ findingQueue: { tier: 'ce', desc: 'The finding-queue data structure. NOTE: the persisted scan_finding_queue.json artifact is written by the Enterprise plugin pack behind the Pro-tier intelligenceEngine gate, so a CE-only install has the structure and not the file.' },
11
31
 
12
32
  // Pro
13
- intelligenceEngine: { tier: 'pro' },
14
- riskScoring: { tier: 'pro' },
15
- proAI: { tier: 'pro' },
16
- analysisAgents: { tier: 'pro' },
17
- advancedCTEM: { tier: 'pro' },
18
- enhancedRedaction: { tier: 'pro' },
19
- proMCP: { tier: 'pro' },
20
- pdfExport: { tier: 'pro' },
33
+ intelligenceEngine: { tier: 'pro', desc: 'Offline CPE generation and NVD CVE matching against detected services.' },
34
+ riskScoring: { tier: 'pro', desc: 'Risk weighting applied to intelligence-engine findings.' },
35
+ proAI: { tier: 'pro', desc: 'Pro analysis prompts (deeper context than the CE prompt set).' },
36
+ analysisAgents: { tier: 'pro', desc: 'Auth, crypto, config and service analysis agents over scan evidence. Analysis of collected evidence — NOT active exploitation probes.' },
37
+ advancedCTEM: { tier: 'pro', desc: 'Extended scan-history retention beyond the CE window.' },
38
+ enhancedRedaction: { tier: 'pro', desc: 'Additional redaction patterns over the CE set for AI prompt payloads.' },
39
+ proMCP: { tier: 'pro', desc: 'MCP tools probe_service and get_vulnerabilities.' },
21
40
 
22
41
  // Enterprise
23
- cloudScanners: { tier: 'enterprise' },
24
- zeroTrust: { tier: 'enterprise' },
25
- complianceEngine: { tier: 'enterprise' },
26
- enterpriseMCP: { tier: 'enterprise' },
27
- airGapped: { tier: 'enterprise' },
42
+ cloudScanners: { tier: 'enterprise', desc: 'AWS / Azure / GCP cloud posture audit plugins (Enterprise plugin pack).' },
43
+ zeroTrust: { tier: 'enterprise', desc: 'Zero-trust posture checks over collected identity and network evidence.' },
44
+ complianceEngine: { tier: 'enterprise', desc: 'Maps findings to SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, CIS v8 and GDPR Art. 32 controls, and renders the report artifacts.' },
45
+ enterpriseMCP: { tier: 'enterprise', desc: 'MCP tools scan_cloud and get_findings.' },
46
+ airGapped: { tier: 'enterprise', desc: 'Offline OPERATION: licence validation is local (ES256 against an embedded key, no callback, no phone-home) and CVE matching runs against a local feed. This describes how the software RUNS, not how it is delivered or installed.' },
28
47
  // Removed 2026-07-21 (capability claim audit): verificationEngine / brandedReports /
29
48
  // usageMetering / dockerIsolation (no implementation) + zdePolicyEngine / enterpriseCTEM
30
49
  // (real cores ship + are claimed in prose; no distinct engine/datastore behind the flag).
31
- // Keep in lockstep with EE index.mjs EE_CAPABILITIES + license-manager keygen.mjs.
50
+ // Removed 2026-08-05 (0.32.11): pdfExport — registered, minted into licences and printed
51
+ // to customers as `✓ pdfExport`, while renderBrandedReport() throws 'Not implemented',
52
+ // puppeteer is not a dependency, and NO code read the flag. Gate-3 caught it generating a
53
+ // claim end to end: an assistant read the flag list and wrote "the output is auditor-
54
+ // consumable via pdfExport" into a customer-facing document. HTML reports remain
55
+ // printable from a browser; that is the browser's capability, not a licensed one.
32
56
  };
33
57
 
34
58
  const TIER_CAPS = {
@@ -58,17 +58,35 @@ function pickClause(issues, prefer) {
58
58
  return String(issues[0]);
59
59
  }
60
60
 
61
- /** Robust one-line descriptor from any plugin finding shape. Never empty, never a raw object dump. */
61
+ /**
62
+ * Robust one-line descriptor from any plugin finding shape. Never empty, never a raw object
63
+ * dump, and — since 0.32.11 — never MULTI-LINE.
64
+ *
65
+ * ⚠️ ONE-LINE IS A RENDERING CONTRACT, not tidiness. The markdown renderer emits one list
66
+ * item per line (`- **[TAG]** plugin: title`), so an embedded newline terminates the item and
67
+ * dumps the remainder as unformatted body text. Several producers build their issue as
68
+ * `"…:\n- alpha\n- beta"`, which is exactly the shape that breaks it, and the breakage is
69
+ * worst on the disclosures that matter most — a truncated scope declaration reads as a
70
+ * complete one.
71
+ *
72
+ * @param {object} x
73
+ * @param {{prefer?: 'gap', max?: number}} [opts] `max` overrides the 160-char budget for
74
+ * channels whose whole purpose is to state a boundary (see the deferred-scope channel,
75
+ * where a 160-char slice discloses one deferred dimension out of eight and so claims more
76
+ * coverage than it has).
77
+ */
62
78
  export function describeFinding(x, opts = {}) {
63
- if (!x || typeof x !== 'object') return String(x ?? '').slice(0, 160);
79
+ const max = Number.isFinite(opts.max) ? opts.max : 160;
80
+ const flat = (v) => String(v ?? '').replace(/\s+/g, ' ').trim();
81
+ if (!x || typeof x !== 'object') return flat(x).slice(0, max);
64
82
  let res = '';
65
83
  for (const k of RESOURCE_KEYS) { if (x[k]) { res = String(x[k]); break; } }
66
84
  let why = '';
67
85
  if (Array.isArray(x.issues) && x.issues.length) why = pickClause(x.issues, opts.prefer);
68
86
  else for (const k of REASON_KEYS) { if (x[k]) { why = String(x[k]); break; } }
69
- why = why.replace(ROUTING_PREFIX_RE, '');
70
- const s = ((res ? res + ' — ' : '') + why).trim();
71
- if (s) return s.length > 160 ? s.slice(0, 160).replace(/\s+\S*$/, '') + '…' : s;
87
+ why = flat(why).replace(ROUTING_PREFIX_RE, '');
88
+ const s = ((res ? flat(res) + ' — ' : '') + why).trim();
89
+ if (s) return s.length > max ? s.slice(0, max).replace(/\s+\S*$/, '') + '…' : s;
72
90
  const sev = String(x.severity || x.level || '').toUpperCase();
73
91
  return sev ? sev + ' finding (no description)' : 'finding (no description)';
74
92
  }
@@ -108,10 +126,26 @@ export function summarizeCloudFindings(results, providerOf, cap = Number(process
108
126
  // bucketed under 'unknown' rather than silently dropped (defense-in-depth against a
109
127
  // future id collision / cloudProvider drift — never let a real finding vanish).
110
128
  const prov = providerOf(r?.id ?? r?.result?.id) || UNKNOWN_PROVIDER;
111
- const bucket = (out[prov] ||= { counts: {}, findings: [], evidenceGaps: [], truncated: false, _rollup: { MEDIUM: new Map(), LOW: new Map() } });
129
+ const bucket = (out[prov] ||= { counts: {}, findings: [], evidenceGaps: [], deferredScope: [], truncated: false, _rollup: { MEDIUM: new Map(), LOW: new Map(), INFO: new Map() } });
112
130
  for (const x of found) {
113
131
  const sev = String(x?.severity || x?.level || 'INFO').toUpperCase();
114
132
  bucket.counts[sev] = (bucket.counts[sev] || 0) + 1;
133
+ // A STATIC CAPABILITY BOUNDARY, not a per-scan gap. Its own channel, deliberately:
134
+ // contract-v1 §2 freezes `deferredScope` as "must NOT route as a live gap", and the
135
+ // 2026-06-10 operator decision unmarked these from `evidenceGap` precisely so a
136
+ // boundary would not fail every scan universally. Surfacing it must not undo that by
137
+ // the back door — so it gets a channel and a badge of its own, and never the gap badge.
138
+ if (x && typeof x === 'object' && x.details && x.details.deferredScope === true) {
139
+ bucket.deferredScope.push({
140
+ plugin: String(r?.id ?? ''),
141
+ scopeId: x.details.deferredScopeId ? String(x.details.deferredScopeId) : null,
142
+ category: x.details.category ? String(x.details.category) : null,
143
+ // A boundary statement gets a wider budget than a finding: these declarations run
144
+ // to several hundred characters across four to eight deferred dimensions, and a
145
+ // 160-char slice discloses ONE of them while reading like the whole boundary.
146
+ title: describeFinding(x, { max: 420 }),
147
+ });
148
+ }
115
149
  let findingEntry = null;
116
150
  if (sev === 'CRITICAL' || sev === 'HIGH') {
117
151
  findingEntry = { severity: sev, plugin: String(r?.id ?? ''), title: describeFinding(x) };
@@ -136,7 +170,13 @@ export function summarizeCloudFindings(results, providerOf, cap = Number(process
136
170
  if (findingEntry) gapEntry._findingRef = findingEntry;
137
171
  bucket.evidenceGaps.push(gapEntry);
138
172
  }
139
- if ((sev === 'MEDIUM' || sev === 'LOW') && !(x && x.details && x.details.evidenceGap === true)) {
173
+ // INFO joined MEDIUM/LOW at 0.32.11. It was the ONLY populated tier with no channel at
174
+ // all: not in the header, not in findings[], not in the rollup — so on a real AWS scan
175
+ // 55 of 62 INFO records appeared in no summary anywhere. Records that already have a
176
+ // channel (evidence gaps, scope boundaries) are excluded, or the same record is counted
177
+ // twice and a cross-cloud roll-up becomes arithmetically wrong.
178
+ const hasOwnChannel = !!(x && x.details && (x.details.evidenceGap === true || x.details.deferredScope === true));
179
+ if ((sev === 'MEDIUM' || sev === 'LOW' || sev === 'INFO') && !hasOwnChannel) {
140
180
  const cat = (x && x.details && x.details.category)
141
181
  ? String(x.details.category)
142
182
  : `uncategorized(${String(r?.id ?? '')})`;
@@ -160,12 +200,9 @@ export function summarizeCloudFindings(results, providerOf, cap = Number(process
160
200
  delete g._findingRef; // internal ref must never reach the MCP payload
161
201
  }
162
202
  }
163
- b.rollup = {
164
- MEDIUM: [...b._rollup.MEDIUM].map(([category, count]) => ({ category, count }))
165
- .sort((a, c) => c.count - a.count || a.category.localeCompare(c.category)),
166
- LOW: [...b._rollup.LOW].map(([category, count]) => ({ category, count }))
167
- .sort((a, c) => c.count - a.count || a.category.localeCompare(c.category)),
168
- };
203
+ const rollupOf = (tier) => [...b._rollup[tier]].map(([category, count]) => ({ category, count }))
204
+ .sort((a, c) => c.count - a.count || a.category.localeCompare(c.category));
205
+ b.rollup = { MEDIUM: rollupOf('MEDIUM'), LOW: rollupOf('LOW'), INFO: rollupOf('INFO') };
169
206
  delete b._rollup;
170
207
  }
171
208
  return out;
@@ -208,13 +245,27 @@ export function renderCloudFindingsMarkdown(summary, providers, opts = {}) {
208
245
  for (const prov of order) {
209
246
  const b = summary[prov]; if (!b) continue;
210
247
  const c = b.counts || {};
211
- lines.push(`## ${String(prov).toUpperCase()} — ${c.CRITICAL || 0} CRITICAL · ${c.HIGH || 0} HIGH · ${c.MEDIUM || 0} MEDIUM · ${c.LOW || 0} LOW · ${c.PASS || 0} PASS`);
248
+ // INFO is a column. It was omitted until 0.32.11, so a header could read "…0 LOW · 17
249
+ // PASS" over 62 unreported INFO records — the tier that carries the evidence gaps and the
250
+ // scope boundaries, i.e. exactly the material a reader needs to tell an incomplete scan
251
+ // from a clean one.
252
+ lines.push(`## ${String(prov).toUpperCase()} — ${c.CRITICAL || 0} CRITICAL · ${c.HIGH || 0} HIGH · ${c.MEDIUM || 0} MEDIUM · ${c.LOW || 0} LOW · ${c.INFO || 0} INFO · ${c.PASS || 0} PASS`);
212
253
  for (const f of (b.findings || [])) lines.push(`- **[${f.severity}]** ${f.plugin}: ${f.title}`);
213
254
  if (b.truncated) lines.push(`- _…CRITICAL/HIGH list truncated; see counts above for totals._`);
214
255
  for (const g of (b.evidenceGaps || [])) lines.push(`- **[⚠ EVIDENCE GAP — unverified]** ${g.plugin}: ${g.title}${g.action ? ` · actionable: ${g.action}` : ''}`);
215
256
  if (b.evidenceGapsTruncated) lines.push(`- _…evidence-gap list truncated; see LOW count for totals._`);
257
+ // A DISTINCT badge, and the wording is load-bearing in both directions: it must not say
258
+ // "gap" (that is the unverified-this-scan channel, and contract-v1 forbids routing a
259
+ // boundary as one), and it must not be silent (surface nobody assessed is otherwise
260
+ // indistinguishable from surface assessed and found clean — the cardinal class here).
261
+ for (const d of (b.deferredScope || [])) {
262
+ lines.push(`- **[🔎 SCOPE NOT ASSESSED — outside this release's evidence scope, not a finding]** ${d.plugin}: ${d.title}`);
263
+ }
216
264
  const drill = opts.getFindingsAvailable ? ' — drill any category via get_findings' : '';
217
- for (const tier of ['MEDIUM', 'LOW']) {
265
+ if ((b.deferredScope || []).length && opts.getFindingsAvailable) {
266
+ lines.push('- _Scope declarations are abridged here; read them in full via get_findings._');
267
+ }
268
+ for (const tier of ['MEDIUM', 'LOW', 'INFO']) {
218
269
  const rows = (b.rollup && b.rollup[tier]) || [];
219
270
  if (!rows.length) continue;
220
271
  const total = c[tier] || rows.reduce((n, rr) => n + rr.count, 0);