nsauditor-ai 0.2.38 → 0.2.40

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
@@ -17,25 +17,13 @@ NSAuditor AI is the open-source core of a privacy-first security intelligence pl
17
17
 
18
18
  ## What's New
19
19
 
20
- **The opt-in RFC 3161 register stops understating itself (CE 0.2.38 / Enterprise 0.33.1).** Paired with Enterprise 0.33.1 — **no CE behaviour change; wording only, and it is the wording npm freezes.** Trusted timestamping is opt-in, became operator-configurable in 0.33.0, and has since been proven end to end against a real Time-Stamp Authority twice over: on the npm path, and separately from inside the published Marketplace container image. Six places in this README still told you the container leg was unverified. They now state what is true — **RFC 3161 timestamping is opt-in via `NSAUDITOR_TSA_URL`, with no default ever and an outbound call to the authority you name** — and keep the one caveat that survives: retained images at `:0.32.11` and earlier carry no `openssl` at all, so nothing here speaks for them. **Ed25519 suppression signing is unchanged and still not reachable** — the workflow ships, the signature does not, and no shipped entry point signs a suppression.
20
+ **The suppression-approval commands land in the CLI (CE 0.2.40 / Enterprise 0.35.0).** `compliance suppress | review | renew | keygen` are new subcommands — thin forwards, the same discipline `compliance attest` established: Enterprise owns validation, defaults, the signing decision and every refusal; CE contributes flags and an exit code. `keygen` writes an approval keypair for a capability that is not yet proven — Ed25519, private half `0600` — and prints an identity-registry member to paste, refusing to overwrite an existing signing key — regenerating one makes every signature it ever produced unverifiable, and nothing reports that until an auditor checks an archived approval. `suppress` signs the approval it writes when `NSAUDITOR_SIGNING_KEY` names a local Ed25519 key, and a malformed key fails at the command with nothing written. `renew` warns that renewing a signed approval invalidates its signature, because the expiry and the renewal record are inside the signed payload. Ed25519 suppression signing is thereby reachable and **not yet proven** — its verification gate has not run against published bytes, so a produced signature is not yet evidence anything vouches for.
21
21
 
22
- **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.
22
+ **Exploit intelligence lands in Pro, and a risk-score claim gets corrected here (CE 0.2.39 / Enterprise 0.34.0).** Paired with Enterprise 0.34.0 — **no CE behaviour change.** Enterprise findings that carry CVEs are now joined by CVE-ID against a local **CISA KEV** catalog and a local **FIRST EPSS** scores file, banded and ordered exploit-first, so a KEV-listed MEDIUM outranks an unexploited CRITICAL. Both stores are **operator-populated — no feed data ships** — and fail closed when stale, so an out-of-date catalog never reports "not exploited". The CE-side change is a correction this README owed you: *"a composite risk score (severity × exploitability × impact × exposure)"* described inputs the code never had. It computes **CVSS weighted by verification status, with an uplift for initial-access techniques**, and that is what it now says. `prioritize()` in the documented FindingQueue sorts by **severity rank alone** — the exploit-aware ordering is Enterprise-side. Community Edition scanning, plugins and output are unchanged.
23
23
 
24
- **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.
24
+ **The opt-in RFC 3161 register stops understating itself (CE 0.2.38 / Enterprise 0.33.1).** Paired with Enterprise 0.33.1 — **no CE behaviour change; wording only, and it is the wording npm freezes.** Trusted timestamping is opt-in, became operator-configurable in 0.33.0, and has since been proven end to end against a real Time-Stamp Authority twice over: on the npm path, and separately from inside the published Marketplace container image. Six places in this README still told you the container leg was unverified. They now state what is true — **RFC 3161 timestamping is opt-in via `NSAUDITOR_TSA_URL`, with no default ever and an outbound call to the authority you name** — and keep the one caveat that survives: retained images at `:0.32.11` and earlier carry no `openssl` at all, so nothing here speaks for them. **Ed25519 suppression signing was unchanged at that release and still not reachable then** — it became reachable in EE 0.35.0 via `compliance suppress` and is **not yet proven**.
25
25
 
26
- **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.
27
-
28
- **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).**
29
-
30
- **Capability-claim honesty pass, part 2 — the air-gapped-delivery class (CE 0.2.33 / Enterprise 0.32.8).** Paired with Enterprise 0.32.8, a **documentation-and-prose release** — no detection, routing, or compliance behaviour changes. 0.32.7 withdrew six phantom capability *flags*; an audit of the air-gapped-delivery claims found the same class one layer down, in prose, across **27 sites** (distinct lines) in the three published packages. **CE fix this cycle — `--out` with a version-named directory.** `--out .../ee-0.32.8` silently wrote artifacts to the *parent* directory: the file-vs-directory check keyed on "has an extension", and `path.parse('ee-0.32.8').ext` is `'.8'`. The same shape broke `v1.2.3` and `release-2026.07`. An extension now counts as file-like only if it starts with a letter, so version-named output directories work and `--out report.json` still resolves to its parent. Found by the pre-publish smoke gate running the documented command end-to-end. **CE docs change this cycle:** the ZDE section's *"Fully air-gappable. Every feature works without internet access"* is corrected to **"Air-gappable, once configured for it"** — scanning, analysis, license verification and evidence-pack generation all run with no outbound network access, and Enterprise adds offline CVE matching under `NSAUDITOR_OFFLINE_ONLY=1` (which emits an explicit coverage gap rather than a silent clean when the local store cannot answer), but 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. The claim that Enterprise "includes offline NVD feeds" is **WITHDRAWN**: no feed data is delivered with the product, and populating the local store is the operator's. Held by a new mutation-proven guard (`tests/capability_claim_honesty.test.mjs`) that fails the build both if the absolute claim returns **and** if the honest qualifier is deleted. On the Enterprise side the same sweep found **five** rendered runtime strings — prose that reaches the assessor-facing evidence pack — making claims the code does not support. Plugin count UNCHANGED at 28; all seven coverage matrices UNCHANGED. See **[CHANGELOG.md](./CHANGELOG.md)** for the full per-release history.
31
-
32
- **Cross-framework routing + capability-claim honesty pass (CE 0.2.32 / Enterprise 0.32.7).** Paired with Enterprise 0.32.7, a **matrix-neutral** release. The network-scan analysis agents' findings now route in **all seven compliance frameworks** — a host serving cleartext or exposing SMB previously failed the SOC 2 report and read clean in HIPAA / NIST CSF / ISO 27001 / CIS v8 / PCI / GDPR off the *same scan*; that gap is closed (48 mapping rules across the seven framework files, a mutation-proven drift guard, all coverage matrices byte-identical). And a **capability-claim honesty pass** withdraws several Pro/Enterprise capabilities that were advertised without a shipping implementation. **CE code change this cycle:** the tier-capability map (`utils/capabilities.mjs`) drops six flags — `verificationEngine`, `brandedReports`, `usageMetering`, `dockerIsolation` (no implementation) and `zdePolicyEngine`, `enterpriseCTEM` (real cores ship + are claimed in prose; no distinct engine behind the name) — in lockstep with Enterprise and both licensing repos, so `nsauditor-ai license --capabilities` and every minted license now name only capabilities that ship. `resolveCapabilities(tier)` derives from tier and ignores the JWT capability array, so **issued licenses are unaffected**. Plugin count UNCHANGED at 28; all seven coverage matrices UNCHANGED. See **[CHANGELOG.md](./CHANGELOG.md)** for the full per-release history.
33
-
34
- **Network-scan false-negative closures on the analysis agents (CE 0.2.31 / Enterprise 0.32.6).** No CE code change this cycle — a paired bump for Enterprise 0.32.6, a **matrix-neutral** false-negative-hardening release on the EE analysis-agent (network-scan) path: **cleartext transport** is now flagged where before only weak-TLS-where-TLS-exists was — a service that should be encrypted but offers no TLS at all used to read clean (→ **SOC 2 CC6.7**); **SMB-alone** exposure is now its own **HIGH** finding rather than being silenced by an `SMB && RDP` conjunction — SMB without RDP is the higher-risk case (→ **CC6.6**); and **WinRM 5985/5986 · Elasticsearch 9300 · MSRPC 135 · a new aggregate open-port-count rule** are new exposure signals (→ **CC6.6**). Routed SOC 2-first with drift-detector coverage; **SOC 2 routing only this cycle — cross-framework mappings (HIPAA §164.312(e)(1) · CIS · NIST) deferred.** Plugin count UNCHANGED at 28; all seven coverage matrices UNCHANGED. See **[CHANGELOG.md](./CHANGELOG.md)** for the full per-release history.
35
-
36
- **Marketplace registration in CLI help + report-quality / routing-integrity release (CE 0.2.30 / Enterprise 0.32.5).** CE now surfaces **AWS Marketplace license registration** in `--help` and in `nsauditor-ai license --status`, so a Marketplace buyer can find the registration step without leaving the CLI. Paired with Enterprise 0.32.5, a **matrix-neutral** report-quality release: nine API Gateway (plugin 1050) mapping rules matched zero findings — **eight were re-sourced, and the ninth (a HIPAA §164.312(c)(1) Integrity mapping) was removed on doctrine rather than repaired**, because a WAF integration that fails closed alters no ePHI. **Routing was never broken and no finding was ever missed**: correctly-sourced catch-all rules already failed the same controls, verified by ablation. The real defect was report **prose** — first-match-wins surfaced a **stale** auditor-facing rationale while the accurate per-class rationale sat unreachable. Also: six auditor-facing rationales in that API-Gateway cohort were rewritten (a **cohort fix, not a class fix** — other rationales across the framework files still carry internal-development-history wording), the JSON report artifact no longer serializes raw routing regexes or rule sources, **a genuine CI/CD false-clean was closed** (a `codebuild:ListProjects` AccessDenied was warning-only, and warnings route to zero controls, so CI/CD controls could read clean over an un-enumerated inventory — now fail-closed into the existing evidence gap), and three compliance guards were hardened (one had been suppressed by a factually false comment; another had silently become a tautology; a third adds an expected-unmapped equality table). Plugin count UNCHANGED at 28; all seven coverage matrices UNCHANGED.
37
-
38
- **RDS false-negative depth pass, part 2 (CE 0.2.29 / Enterprise 0.32.4).** No CE code change this cycle — a paired bump for Enterprise 0.32.4, a **matrix-neutral** RDS false-negative-and-report-quality depth pass on plugin 1140: **RDS Proxy client↔proxy TLS** (a proxy with `RequireTLS` off accepts cleartext client connections — a transit leg distinct from the DB-engine SSL parameter — now a fail-closed HIGH), a new **retained / cross-region-replicated automated-backup at-rest surface** (an unencrypted automated backup that survives instance/cluster deletion, invisible to the live-resource and snapshot scans, is now caught), the **Aurora cluster-member double-audit closure** (a provisioned Aurora cluster's members no longer double-report the cluster-scoped SSL / Multi-AZ settings as instance-level false positives), and a **cross-framework report-quality leak closure** (a renderer backstop strips foreign framework control-ids out of the violation prose that renders into every framework report). No new plugins, all seven coverage matrices unchanged. See **[CHANGELOG.md](./CHANGELOG.md)** for the full per-release history.
26
+ See **[CHANGELOG.md](./CHANGELOG.md)** for the full per-release history.
39
27
 
40
28
  → See a sample EE scan output: **[walk-through with synthetic Acme Corp AWS account](https://www.nsauditor.com/ai/docs/sample-scan/)** (no signup required)
41
29
 
@@ -52,7 +40,7 @@ Scan → Analyze → Prioritize → Track → Act
52
40
  - **Smart result fusion** — the Result Concluder merges all plugin outputs into a normalized view with OS detection, service fingerprinting, and evidence linking
53
41
  - **Structured finding format** — all findings use a common schema with category, severity, evidence, and remediation — enabling consistent SARIF export and MCP integration
54
42
  - **AI-powered analysis** — send redacted scan results to OpenAI or Claude (your keys, your choice) for vulnerability assessments and remediation guidance
55
- - **Risk-scored prioritization (Pro)** — findings carry a composite risk score (severity × exploitability × impact × exposure) and a status field, and an operator suppression workflow (accepted-risk / false-positive with expiry) keeps triaged findings out of the report until they expire
43
+ - **Risk-scored prioritization (Pro)** — findings carry a composite risk score (CVSS weighted by verification status, with an uplift for initial-access techniques) and a status field, and an operator suppression workflow (accepted-risk / false-positive with expiry) keeps triaged findings out of the report until they expire
56
44
  - **Continuous monitoring (CTEM)** — watch mode rescans on a schedule, diffs against previous results, and fires webhook alerts on changes
57
45
  - **MCP integration** — expose scanning tools to AI assistants like Claude Code via Model Context Protocol
58
46
  - **CI/CD ready** — SARIF output with `--fail-on` severity gating for pipeline integration
@@ -74,7 +62,7 @@ If you're heading into a **SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, CIS C
74
62
  - **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
75
63
  - **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)
76
64
  - **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
77
- - 🔐 **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 plus the `openssl` binary, and were exercised against a live Time-Stamp Authority on the npm path on 2026-08-07: the auditor procedure `openssl ts -verify` returned OK, with a one-byte-mutated copy returning FAILED as the control. The `:0.33.0` Marketplace image is proven too: on 2026-08-08 the same round-trip was driven from inside the pushed image against a real authority, verified with the image’s own `openssl`. **Retained images at `:0.32.11` and earlier carry no `openssl` at all**, so nothing here speaks for them. **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.
65
+ - 🔐 **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 plus the `openssl` binary, and were exercised against a live Time-Stamp Authority on the npm path on 2026-08-07: the auditor procedure `openssl ts -verify` returned OK, with a one-byte-mutated copy returning FAILED as the control. The `:0.33.0` Marketplace image is proven too: on 2026-08-08 the same round-trip was driven from inside the pushed image against a real authority, verified with the image’s own `openssl`. **Retained images at `:0.32.11` and earlier carry no `openssl` at all**, so nothing here speaks for them. **Ed25519 suppression signing became reachable in EE 0.35.0 via `compliance suppress`** and is **not yet proven** — its verification gate has not run against published bytes, so do not plan around a produced signature as verified evidence.
78
66
  - 🏛️ **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.
79
67
  - 🔗 **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.
80
68
  - 🗄️ **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
@@ -106,6 +94,7 @@ How Marketplace fulfillment works (ZDE and air-gap preserved — no runtime AWS
106
94
  | **Pro features (vulnerability assessment)** | | | |
107
95
  | CVE matching + MITRE ATT&CK mapping | — | ✅ | ✅ |
108
96
  | Risk scoring + prioritization | — | ✅ | ✅ |
97
+ | Exploit intelligence — **CISA KEV** + **FIRST EPSS** joined onto the CVE matches, exploit-first ordering (a KEV-listed MEDIUM outranks an unexploited CRITICAL); stores are operator-populated — no feed data ships | — | ✅ | ✅ |
109
98
  | Parallel analysis agents | — | ✅ | ✅ |
110
99
  | **Enterprise — cloud scanning** | | | |
111
100
  | 28 enterprise plugins — 27 cloud (AWS / Azure / GCP) + 1 network-scan zero-trust check | — | — | ✅ |
@@ -121,7 +110,7 @@ How Marketplace fulfillment works (ZDE and air-gap preserved — no runtime AWS
121
110
  | Multi-framework `--compliance soc2,hipaa,nist-csf,pci-dss,iso-27001,cis-v8,gdpr` from one scan | — | — | ✅ |
122
111
  | **Enterprise — auditor-grade evidence** | | | |
123
112
  | Evidence packs with SHA-256 chain-of-custody (RFC 3161 timestamps opt-in via `NSAUDITOR_TSA_URL`, exercised against a live TSA on both the npm path and the `:0.33.0` container image; retained images `:0.32.11` and earlier carry no `openssl`) | — | — | ✅ |
124
- | Suppression workflow with approver identity verification (Ed25519 SIGNING is built, not reachable) | — | — | ✅ |
113
+ | Suppression workflow with approver identity verification (Ed25519 SIGNING reachable 0.35.0, not yet proven) | — | — | ✅ |
125
114
  | Chain-of-custody manifests | — | — | ✅ |
126
115
  | SLA / MTTR tracking + compensating controls | — | — | ✅ |
127
116
  | Recurring-scan attestation (Type II operating-effectiveness) | — | — | ✅ |
@@ -313,8 +302,8 @@ Results land in `./out/<host>_<timestamp>/`:
313
302
  | 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** |
314
303
  | 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** |
315
304
  | 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** |
316
- | — | SOC 2 Compliance Engine | Enterprise | AICPA TSC 2017 mapping (10 covered + 4 partial controls), SHA-256 chain-of-custody, opt-in RFC 3161 timestamps, exercised against a live TSA on both the npm path and the `:0.33.0` container image, and the suppression workflow. (Ed25519 SIGNING of suppressions is built and not reachable — the workflow ships, the signature does not.) |
317
- | — | **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. |
305
+ | — | SOC 2 Compliance Engine | Enterprise | AICPA TSC 2017 mapping (10 covered + 4 partial controls), SHA-256 chain-of-custody, opt-in RFC 3161 timestamps, exercised against a live TSA on both the npm path and the `:0.33.0` container image, and the suppression workflow. (Ed25519 SIGNING of suppressions is reachable from EE 0.35.0 and not yet proven — the gate has not run against published bytes.) |
306
+ | — | **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 reachable from EE 0.35.0 but not yet proven). 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. |
318
307
  | — | **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. |
319
308
  | — | **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. |
320
309
  | — | **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. |
@@ -846,6 +835,41 @@ attestation is marked `REPORT INVALID FOR AUDIT` are **counted and named** rathe
846
835
  averaged into a clean verdict, and an empty history exits **3** with status `no_evidence` —
847
836
  absence of evidence is a finding, not a pass.
848
837
 
838
+ **The approval commands (CE 0.2.40 / Enterprise 0.35.0).** Four more `compliance` subcommands, all
839
+ forwarding to Enterprise — they need `@nsasoft/nsauditor-ai-ee` installed and exit 2 with an
840
+ explanatory message if it is absent. CLI-only; the MCP surface does not reach them.
841
+
842
+ ```bash
843
+ # Create an approval keypair — private half 0600, plus an identity-registry member
844
+ # (pass --email/--role/--team too, or the member prints REPLACE ME placeholders)
845
+ nsauditor-ai compliance keygen --key ~/.nsauditor/approver.pem --approver "Ann Approver"
846
+
847
+ # Record an approval; SIGNS it when NSAUDITOR_SIGNING_KEY names a local Ed25519 key (not yet proven)
848
+ NSAUDITOR_SIGNING_KEY=~/.nsauditor/approver.pem \
849
+ nsauditor-ai compliance suppress --suppressions ./out/suppressions.json \
850
+ --source auth_agent --title-pattern "SSH password authentication enabled" \
851
+ --status accepted_risk --rationale "compensating control at the perimeter" \
852
+ --approver "Ann Approver" --attestation-level approver
853
+
854
+ # List every approval across a scan history with its expiry status
855
+ nsauditor-ai compliance review --history ./out
856
+
857
+ # Re-approve before expiry (appended to the record's renewals[] chain)
858
+ nsauditor-ai compliance renew --suppressions ./out/suppressions.json \
859
+ --id supp-abc123 --rationale "quarterly re-review" --approver "Ann Approver"
860
+ ```
861
+
862
+ Three refusals and a warning you will meet rather than a happy path: `keygen` will not overwrite an
863
+ existing signing key (every signature it made becomes unverifiable, and nothing says so until an
864
+ auditor checks an archived approval); a malformed signing key fails the command and writes
865
+ **nothing**; `awskms:` references are refused in this release; and `renew` on a SIGNED approval
866
+ **invalidates its signature** — the expiry and the renewal record are inside the signed payload — so
867
+ it warns loudly and tells you to re-approve.
868
+
869
+ ⚠️ **`--flag=value` is not supported**, here or on any flag this CLI has: use `--flag value`.
870
+ Ed25519 suppression signing is reachable from Enterprise 0.35.0 and **not yet proven** — its
871
+ verification gate has not run against published bytes.
872
+
849
873
  **Compliance evidence (`NSAUDITOR_*` — Enterprise, all opt-in):**
850
874
 
851
875
  Every variable added from Enterprise 0.33.0 onward carries the `NSAUDITOR_` prefix, so a
@@ -876,11 +900,11 @@ NSAUDITOR_SIGNING_KEY= # A REFERENCE, never key material: keychain:LA
876
900
  One of these is deliberately **not** a capability yet and the other is proven on one path only —
877
901
  saying which is which is the point:
878
902
 
879
- - **`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
880
- shipped entry point signs a suppression, so setting it changes nothing today. It exists
881
- now because the signature record's `algorithm` and `backend` fields had to be frozen
882
- *before* the first signature reaches a customer archive — retrofitting that later breaks
883
- every auditor holding one.
903
+ - **`NSAUDITOR_SIGNING_KEY` is CONSUMED from EE 0.35.0, and the note below used to say otherwise.** EE 0.33.0 stopped parsing it at option-resolution time — a malformed value used to abort the EE stage for a setting nothing read — so it is carried through unparsed; `parseSigningRef` / `resolveSigner` hold the parse and the offline veto at the point a key is used. As of EE 0.35.0 `compliance suppress` signs the approval it writes when this variable names a local Ed25519 key — a capability that is **not yet proven**, because its verification gate has
904
+ not run against published bytes, so a produced signature is not yet evidence anything vouches
905
+ for. The record's `algorithm` and `backend` fields were frozen *before* the first
906
+ signature could reach a customer archive, because retrofitting that later breaks every
907
+ auditor holding one.
884
908
  - **`NSAUDITOR_TSA_URL` is wired, and since 2026-08-07 it is proven — on the npm path only.**
885
909
  The live check this note used to say was outstanding has been run: through the installed
886
910
  binary, against a real Time-Stamp Authority, the auditor procedure `openssl ts -verify`
@@ -1100,7 +1124,7 @@ NSAuditor AI is built on a **Zero Data Exfiltration (ZDE)** architecture:
1100
1124
  - **No data processing.** Nsasoft US LLC never sees, stores, or processes your scan results.
1101
1125
  - **AI is opt-in.** External AI calls use your own API keys. Redaction runs locally first.
1102
1126
  - **License validation is offline.** JWT signature verified locally with an embedded public key.
1103
- - **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.
1127
+ - **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. The prior absolute form of this bullet — *"Fully air-gappable. Every feature works without internet access (Enterprise includes offline NVD feeds)"* — is **WITHDRAWN** (CE 0.2.33): quoted here so the withdrawal record stays on this page now that release history lives in the [CHANGELOG](./CHANGELOG.md).
1104
1128
 
1105
1129
  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.
1106
1130
 
package/cli.mjs CHANGED
@@ -678,6 +678,29 @@ export async function parseArgs(argv) {
678
678
  args.slaPolicy = slaPolicyVal === undefined ? undefined : slaPolicyVal;
679
679
  const attestWindowVal = get('window');
680
680
  args.attestWindow = (attestWindowVal && attestWindowVal !== true) ? attestWindowVal : null;
681
+
682
+ // ── the approval surface (EE 0.35.0) ────────────────────────────────────────
683
+ // Parsed HERE with every other flag rather than read from `process.argv` in the
684
+ // subcommand, so one parser owns the `--flag value` / `--flag=value` / boolean
685
+ // shapes. `attest` taught this: a second parser is a second set of edge cases.
686
+ const str = (name) => { const v = get(name); return (v && v !== true) ? v : null; };
687
+ args.approvalArgs = {
688
+ suppressions: str('suppressions'),
689
+ keyPath: str('key'),
690
+ source: str('source'),
691
+ titlePattern: str('title-pattern') ?? str('title_pattern'),
692
+ status: str('status'),
693
+ rationale: str('rationale'),
694
+ approver: str('approver'),
695
+ email: str('email'),
696
+ role: str('role'),
697
+ team: str('team'),
698
+ suppressionId: str('id'),
699
+ expiresInDays: str('expires-in-days'),
700
+ attestationLevel: str('attestation-level'),
701
+ compensatingControl: str('compensating-control'),
702
+ force: !!get('force'),
703
+ };
681
704
  const frameworkVal = get('framework');
682
705
  args.framework = (frameworkVal && frameworkVal !== true) ? frameworkVal : null;
683
706
 
@@ -955,10 +978,20 @@ function maxSeverityInConclusion(conclusion) {
955
978
  * EE raises this as an `NsauditorConfigError` from `resolveComplianceEnvOpts`, and exports
956
979
  * that function specifically so CE can fail fast at startup with the SAME definition of
957
980
  * "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.
981
+ * `enrichScan` — which at the time meant a bare `catch {}` on the scan path that did not
982
+ * abort the scan: it silently dropped the ENTIRE EE stage (intelligence, agents, the
983
+ * compliance report). A quiet skip wearing a fail-fast's name, one repo over from the test
984
+ * that forbids exactly that.
985
+ *
986
+ * ⚠️ THAT CATCH IS NO LONGER BARE — the tense above is deliberate and was wrong until now.
987
+ * The import and the call carry SEPARATE catches (see the scan path below): absence stays
988
+ * silent, because a Community user legitimately has no EE, while FAILURE is named and
989
+ * `conclusion.result.eeEnrichmentError` is set. This docblock argued from its own file's
990
+ * corrected defect, in the present tense, forty lines from the code that fixes it — and it
991
+ * was the FOURTH site of one dead premise, the other three being EE's §16.1 row, the
992
+ * `nsauditor_env` docblock and that test's assertion message. The preflight's REASON is
993
+ * unaffected: a contradiction between two operator settings should stop the run at startup
994
+ * regardless of how loudly a later failure would report itself.
962
995
  *
963
996
  * Deliberately NOT gated on the command and NOT gated on a licence tier — a contradiction
964
997
  * between two operator settings is a configuration error under every command, and
@@ -1008,7 +1041,7 @@ export async function preflightGrcIfRequested(env, opts = {}) {
1008
1041
 
1009
1042
  export async function main() {
1010
1043
  const args = await parseArgs(process.argv);
1011
- const { cmd, host, plugins, insecureHttps, hostFile, parallel, failOn, outputFormat, watch, intervalMinutes, webhookUrl, alertSeverity, ports, compliance, complianceScope, complianceHistory, slaPolicy, attestWindow, framework, awsRegion } = args;
1044
+ const { cmd, host, plugins, insecureHttps, hostFile, parallel, failOn, outputFormat, watch, intervalMinutes, webhookUrl, alertSeverity, ports, compliance, complianceScope, complianceHistory, slaPolicy, attestWindow, framework, awsRegion, approvalArgs } = args;
1012
1045
 
1013
1046
  // Version: handled before license verification so it works without a key.
1014
1047
  // CE-0.1.30.1 — closes the discovery-flag UX gap where pre-fix
@@ -1075,6 +1108,26 @@ Scan options:
1075
1108
 
1076
1109
  Compliance subcommands:
1077
1110
  nsauditor-ai compliance attest --history <dir> [--framework <fw>] [--window 6m|12m|90d]
1111
+ nsauditor-ai compliance keygen --key <path> [--approver <name>] [--email <e>] [--role <r>]
1112
+ [--team <t>] [--force]
1113
+ Generate an Ed25519 approval keypair (private 0600) and print
1114
+ the identity-registry member to paste. Refuses to overwrite an
1115
+ existing key without --force: regenerating makes every
1116
+ signature that key ever made unverifiable. Enterprise.
1117
+ nsauditor-ai compliance suppress --suppressions <file> --source <s> --title-pattern <p>
1118
+ --status <accepted_risk|false_positive> --rationale <text>
1119
+ --approver <name> [--expires-in-days <n>]
1120
+ [--attestation-level <approver|deployment>]
1121
+ [--compensating-control <text>]
1122
+ Records an approval. SIGNS it when NSAUDITOR_SIGNING_KEY names
1123
+ a local Ed25519 PEM; records it unsigned otherwise, and the
1124
+ report labels which. A malformed key fails HERE, never on a
1125
+ scan. Enterprise.
1126
+ nsauditor-ai compliance review --history <dir>
1127
+ Expiry review across a scan-history root. Enterprise.
1128
+ nsauditor-ai compliance renew --suppressions <file> --id <id> --rationale <text>
1129
+ --approver <name> [--expires-in-days <n>]
1130
+ Extends an approval and records the renewal chain. Enterprise.
1078
1131
  Aggregate the per-scan attestation records in
1079
1132
  <dir> into a multi-period (Type II) recurring-scan
1080
1133
  attestation. Reads scan_attestation_<fw>.json from
@@ -2121,8 +2174,109 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
2121
2174
  // because nothing here decides anything.
2122
2175
  if (cmd === 'compliance') {
2123
2176
  const sub = process.argv[3];
2177
+
2178
+ // ── THE APPROVAL SURFACE (EE 0.35.0) ───────────────────────────────────
2179
+ // `suppress` / `review` / `renew` / `keygen`. Same THIN-FORWARD discipline as
2180
+ // `attest` below: EE owns validation, defaults, the signing decision and every
2181
+ // refusal; CE contributes a flag surface and an exit code and decides nothing.
2182
+ //
2183
+ // ⚠️ CLI-ONLY BY RATIFIED SCOPE (D4 condition 1). These are NOT exposed as MCP
2184
+ // tools — the GRC-push precedent — because an approval surface reachable from a
2185
+ // chat client is its own decision and nobody has made it.
2186
+ //
2187
+ // ⚠️ NOTHING HERE FLIPS A CLAIM. Publishing the surface is not proving it: per D6
2188
+ // the three-part gate runs against the published registry bytes and the hedges
2189
+ // flip at N+1. If you are reading this while editing marketing copy, the answer
2190
+ // is still "not yet".
2191
+ if (sub === 'suppress' || sub === 'review' || sub === 'renew' || sub === 'keygen') {
2192
+ let ee;
2193
+ try {
2194
+ ee = await import('@nsasoft/nsauditor-ai-ee');
2195
+ } catch {
2196
+ console.error(`compliance ${sub} requires the Enterprise package (@nsasoft/nsauditor-ai-ee).`);
2197
+ process.exit(2);
2198
+ }
2199
+ const A = approvalArgs ?? {};
2200
+ const suppressionsPath = A.suppressions;
2201
+ try {
2202
+ if (sub === 'keygen') {
2203
+ const out = await ee.keygenCommand({
2204
+ privateKeyPath: A.keyPath,
2205
+ approver: A.approver || undefined,
2206
+ email: A.email || undefined,
2207
+ role: A.role || undefined,
2208
+ team: A.team || undefined,
2209
+ force: A.force,
2210
+ });
2211
+ console.log(`wrote ${out.privateKeyPath} (0600) and ${out.publicKeyPath} (0644)`);
2212
+ console.log('\nAdd this member to your identity registry:\n');
2213
+ console.log(JSON.stringify(out.registryMember, null, 2));
2214
+ console.log('\nThe private key never leaves this machine. Scanners do not need it — '
2215
+ + 'signing happens here, at approval time.');
2216
+ process.exit(0);
2217
+ }
2218
+ if (sub === 'review') {
2219
+ const { rows, summary } = await ee.reviewCommand({ rootDir: complianceHistory });
2220
+ console.log(`Suppressions: ${summary.total} total · ${summary.active} active · `
2221
+ + `${summary.approaching} approaching expiry · ${summary.expired} expired · `
2222
+ + `${summary.no_expiry} without expiry`);
2223
+ for (const r of rows) {
2224
+ console.log(` [${r.expiryStatus}] ${r.suppressionId ?? '(no-id)'} — ${r.approver ?? '(no approver)'}`);
2225
+ }
2226
+ process.exit(0);
2227
+ }
2228
+ // `suppress` and `renew` both write, and both may SIGN — EE decides, from the
2229
+ // signing reference this forwards. A malformed reference fails HERE, at the
2230
+ // command, and never on a scan (D4).
2231
+ const signingKeyRaw = process.env.NSAUDITOR_SIGNING_KEY || undefined;
2232
+ if (sub === 'suppress') {
2233
+ const out = await ee.suppressCommand({
2234
+ suppressionsPath,
2235
+ match: { source: A.source, titlePattern: A.titlePattern },
2236
+ status: A.status,
2237
+ rationale: A.rationale,
2238
+ approver: A.approver,
2239
+ expiresInDays: A.expiresInDays ? Number(A.expiresInDays) : undefined,
2240
+ compensating_control: A.compensatingControl || undefined,
2241
+ // ⚠️ PROVENANCE, BECAUSE ONLY THIS LINE KNOWS IT. EE cannot tell an env-sourced
2242
+ // reference from a hand-passed one — it receives an argument either way — so a
2243
+ // malformed key used to tell the operator who set NSAUDITOR_SIGNING_KEY about "the
2244
+ // supplied signing key", naming no setting on the one path where naming it is the
2245
+ // point. The token is what sends them back to the variable they actually typed.
2246
+ }, {
2247
+ signingKeyRaw,
2248
+ signingKeyFrom: 'environment',
2249
+ attestationLevel: A.attestationLevel || undefined,
2250
+ });
2251
+ const sig = out.suppression.signature;
2252
+ console.log(`wrote ${out.file} — ${out.suppression.id} (${out.suppression.status})`);
2253
+ console.log(sig
2254
+ ? ` signed: ${sig.algorithm}/${sig.backend}`
2255
+ + `${sig.attestationLevel ? ` at level ${sig.attestationLevel}` : ', identity model not declared'}`
2256
+ : ' unsigned — set NSAUDITOR_SIGNING_KEY to a local Ed25519 PEM to sign approvals');
2257
+ process.exit(0);
2258
+ }
2259
+ const out = await ee.renewCommand({
2260
+ suppressionsPath,
2261
+ suppressionId: A.suppressionId,
2262
+ rationale: A.rationale,
2263
+ approver: A.approver,
2264
+ expiresInDays: A.expiresInDays ? Number(A.expiresInDays) : undefined,
2265
+ });
2266
+ console.log(`renewed ${out.suppression?.id ?? A.suppressionId} in ${out.file ?? suppressionsPath}`);
2267
+ process.exit(0);
2268
+ } catch (err) {
2269
+ // Refusals arrive here and must stay loud: a malformed signing key, an
2270
+ // `awskms:` reference this release does not sign with, an existing key file
2271
+ // keygen will not overwrite. Every one of them is the operator learning
2272
+ // something true before an artifact exists.
2273
+ console.error(`Fatal: ${err.message}`);
2274
+ process.exit(2);
2275
+ }
2276
+ }
2277
+
2124
2278
  if (sub !== 'attest') {
2125
- console.error('Usage: nsauditor-ai compliance attest --history <dir> [--framework <fw>] [--window 6m|12m|90d]');
2279
+ console.error('Usage: nsauditor-ai compliance <attest|suppress|review|renew|keygen> …');
2126
2280
  process.exit(2);
2127
2281
  }
2128
2282
  if (typeof complianceHistory !== 'string' || complianceHistory.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
4
4
  "description": "Modular AI-assisted network security audit platform — Community Edition",
5
5
  "type": "module",
6
6
  "private": false,
@@ -6,6 +6,8 @@ import net from "node:net";
6
6
  import dgram from "node:dgram";
7
7
  import fsp from "node:fs/promises";
8
8
  import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { mapLimit } from "../utils/concurrency.mjs";
9
11
 
10
12
  /* ------------------------------ helpers ------------------------------ */
11
13
 
@@ -57,16 +59,29 @@ export function parsePortsSpec(spec) {
57
59
  return out;
58
60
  }
59
61
 
60
- async function loadConfigPortsFromServicesJson(cwd = process.cwd()) {
61
- // Supports the "array schema" used by tests:
62
- // { "services": [ { port, protocol }, ... ] }
63
- // and a fallback object schema: { tcp: [...], udp: [...] }
62
+ /**
63
+ * ⚠️ THE PACKAGE'S OWN DATA IS FOUND RELATIVE TO THE PACKAGE, NOT TO THE CALLER.
64
+ *
65
+ * This used to default to `process.cwd()` alone, with the `catch` below swallowing the miss.
66
+ * A globally installed CLI runs from wherever the OPERATOR happens to be, so unless that
67
+ * directory contained a `config/services.json`, the default port list came back EMPTY, the
68
+ * early return in `run()` fired, and the scan reported `up:false` with every bucket empty —
69
+ * in about a millisecond, having probed nothing. Measured 2026-08-10 on the installed build:
70
+ * cwd=/private/tmp gave 0 rows, cwd=<package> gave 50 rows and tcpOpen [80,21,53,443,…].
71
+ *
72
+ * That is a FALSE CLEAN shipped to every npm customer: an empty sweep is indistinguishable
73
+ * from a host with nothing listening, and downstream every plugin gated on `tcp_open` is then
74
+ * skipped silently. cwd is still tried FIRST so an operator can override the port set by
75
+ * placing their own `config/services.json` beside them — the documented behaviour — but the
76
+ * package copy is the floor, and a scan can no longer fall through to nothing.
77
+ */
78
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
79
+
80
+ /** Parse ONE services.json into a port set. Never throws; an unusable file yields nothing. */
81
+ async function readPortSet(fp) {
64
82
  const out = { tcp: [], udp: [] };
65
83
  try {
66
- const fp = path.join(cwd, "config", "services.json");
67
- const raw = await fsp.readFile(fp, "utf8");
68
- const cfg = JSON.parse(raw);
69
-
84
+ const cfg = JSON.parse(await fsp.readFile(fp, "utf8"));
70
85
  if (Array.isArray(cfg?.services)) {
71
86
  for (const s of cfg.services) {
72
87
  const p = Number(s?.port);
@@ -79,14 +94,33 @@ async function loadConfigPortsFromServicesJson(cwd = process.cwd()) {
79
94
  if (Array.isArray(cfg?.tcp)) out.tcp.push(...cfg.tcp);
80
95
  if (Array.isArray(cfg?.udp)) out.udp.push(...cfg.udp);
81
96
  }
82
- } catch {
83
- // no config, ignore
84
- }
97
+ } catch { /* unreadable or unparseable — yields nothing, and the caller falls through */ }
85
98
  out.tcp = uniqInts(out.tcp);
86
99
  out.udp = uniqInts(out.udp);
87
100
  return out;
88
101
  }
89
102
 
103
+ /**
104
+ * The default port set: the caller's override if it YIELDS PORTS, otherwise the package's own.
105
+ *
106
+ * ⚠️ THE RULE IS *YIELD*, NOT *READ*, AND A REVIEW CAUGHT THE DIFFERENCE. An earlier fix tried
107
+ * cwd then the package and committed to the first file that READ successfully. But
108
+ * `config/services.json` is a common filename: a FOREIGN one in the caller's directory —
109
+ * another application's, or an empty/typo'd nsauditor one (`{}`, `{"services":[]}`) — parses
110
+ * fine, populates nothing, and the package floor was then never consulted. The scan probed
111
+ * ZERO ports and reported `up:false`: the same false clean as before, re-gated on a filename
112
+ * collision instead of a missing file. Committing on read also made that fix's own comment
113
+ * ("a scan can no longer fall through to nothing") untrue.
114
+ *
115
+ * A non-empty override REPLACES rather than merges — an operator narrowing the sweep on
116
+ * purpose must not silently get the full set back.
117
+ */
118
+ async function loadConfigPortsFromServicesJson(cwd = process.cwd()) {
119
+ const override = await readPortSet(path.join(cwd, "config", "services.json"));
120
+ if (override.tcp.length || override.udp.length) return override;
121
+ return readPortSet(path.join(PKG_ROOT, "config", "services.json"));
122
+ }
123
+
90
124
  function classifyTcpError(err) {
91
125
  const code = err?.code || "";
92
126
  const msg = err?.message || String(err) || "";
@@ -261,15 +295,31 @@ export default {
261
295
 
262
296
  const data = [];
263
297
 
298
+ // ⚠️ BOUNDED CONCURRENCY, NOT A SEQUENTIAL LOOP — and the reason is a measured false clean.
299
+ // These two loops used to `await` one port at a time. A filtered or silent port costs the
300
+ // FULL per-port timeout, so the shipped 50-port sweep (43 TCP + 7 UDP) took 40,977 ms
301
+ // against a real router on 2026-08-10 — past the 30,000 ms `PLUGIN_TIMEOUT_MS` the plugin
302
+ // manager races every run against. The orchestrator therefore DISCARDED a correct result
303
+ // (`up:true`, `tcpOpen:[80,21,53,443,...]`) that arrived eleven seconds later, and
304
+ // substituted an empty one.
305
+ //
306
+ // That is not slowness, it is a FALSE CLEAN: with `ctx.tcpOpen` empty, every plugin gated
307
+ // on `tcp_open` is skipped by `plugin_manager.mjs` with a bare `return false` — no finding,
308
+ // no evidence gap — so `HTTP Probe` and `Webapp Detector` never ran, and the compliance
309
+ // layer reported PASS controls off a scan that never probed HTTP.
310
+ //
311
+ // `mapLimit` returns results IN INPUT ORDER, so the evidence rows stay deterministic; the
312
+ // cap is deliberately modest because a port sweep is latency-bound, not CPU-bound, and a
313
+ // scanner that floods a small target is its own kind of defect.
314
+ const SWEEP_CONCURRENCY = toInt(process.env.PORT_SCAN_CONCURRENCY, 12);
315
+
264
316
  // TCP scans
265
- for (const p of tcpPorts) {
266
- data.push(await scanTcpPort(host, p, { timeoutMs, bannerTimeoutMs, maxBannerBytes }));
267
- }
317
+ data.push(...await mapLimit(tcpPorts, SWEEP_CONCURRENCY, (p) =>
318
+ scanTcpPort(host, p, { timeoutMs, bannerTimeoutMs, maxBannerBytes })));
268
319
 
269
320
  // UDP scans
270
- for (const p of udpPorts) {
271
- data.push(await scanUdpPort(host, p, { timeoutMs, udpPayload }));
272
- }
321
+ data.push(...await mapLimit(udpPorts, SWEEP_CONCURRENCY, (p) =>
322
+ scanUdpPort(host, p, { timeoutMs, udpPayload })));
273
323
 
274
324
  // Buckets
275
325
  const tcpOpen = data.filter(d => d.probe_protocol === "tcp" && d.status === "open").map(d => d.probe_port);
@@ -3,12 +3,42 @@
3
3
 
4
4
  import fsp from 'node:fs/promises';
5
5
  import path from 'node:path';
6
+ import os from 'node:os';
6
7
 
7
8
  const DEFAULT_TTL_DAYS = 7;
8
9
  const MAX_ENTRIES = 10000;
9
10
 
11
+ /**
12
+ * Where the cache lives when the caller does not say.
13
+ *
14
+ * ⚠️ NOT `'.nvd_cache'`, AND THE REASON IS A MEASURED OUTAGE. That default was CWD-RELATIVE
15
+ * (`path.resolve` below), and `mcp_server.mjs` constructs its client with no `cacheDir` — so
16
+ * under the Claude Desktop MCP server, which runs from `/`, the write target was
17
+ * `/.nvd_cache` and `mkdir` returned EROFS. `get_vulnerabilities` was DEAD for every Desktop
18
+ * user (Gate 3, 2026-08-10). For CLI users it was quieter and still wrong: the cache lived
19
+ * wherever the operator happened to stand, so it never persisted between directories and
20
+ * every run re-fetched what it already had.
21
+ *
22
+ * ⚠️ SAME CLASS AS THE PORT-SCANNER FIX IN THIS SAME RELEASE (CE 0.2.39), one module over, on
23
+ * a WRITE path: package data and package state resolved against the CALLER's cwd under a
24
+ * globally installed binary. That fix addressed `config/services.json` and did not touch this
25
+ * — the named instance, not the class. [[guard_the_hazard_class_not_the_named_instance]]
26
+ *
27
+ * Per-user, not per-directory, and overridable by the env var an operator can point at a
28
+ * writable volume in a container.
29
+ */
30
+ export function defaultCacheDir() {
31
+ const fromEnv = process.env.NSAUDITOR_NVD_CACHE_DIR;
32
+ if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim();
33
+ try {
34
+ const home = os.homedir();
35
+ if (home) return path.join(home, '.nsauditor', 'nvd_cache');
36
+ } catch { /* no home in this environment — fall through */ }
37
+ return path.join(os.tmpdir(), 'nsauditor-nvd-cache');
38
+ }
39
+
10
40
  export class NvdCache {
11
- constructor(cacheDir = '.nvd_cache') {
41
+ constructor(cacheDir = defaultCacheDir()) {
12
42
  this.cacheFile = path.resolve(cacheDir, 'nvd_cache.json');
13
43
  this.ttlMs = (Number(process.env.NVD_CACHE_TTL_DAYS) || DEFAULT_TTL_DAYS) * 86400000;
14
44
  this._data = null;
@@ -45,8 +75,23 @@ export class NvdCache {
45
75
  }
46
76
 
47
77
  async _save() {
48
- await fsp.mkdir(path.dirname(this.cacheFile), { recursive: true });
49
- await fsp.writeFile(this.cacheFile, JSON.stringify(this._data, null, 2), 'utf8');
78
+ // ⚠️ A CACHE IS AN OPTIMISATION: AN UNWRITABLE LOCATION COSTS CACHING, NEVER THE TOOL.
79
+ // The Desktop outage was not that the cache was unavailable — it was that an unavailable
80
+ // cache took `get_vulnerabilities` down with it. A read-only or hostile environment now
81
+ // loses persistence and keeps the feature; the in-memory `_data` still serves the run.
82
+ try {
83
+ await fsp.mkdir(path.dirname(this.cacheFile), { recursive: true });
84
+ await fsp.writeFile(this.cacheFile, JSON.stringify(this._data, null, 2), 'utf8');
85
+ } catch (err) {
86
+ if (!this._warnedUnwritable) {
87
+ this._warnedUnwritable = true;
88
+ // Said ONCE, and said out loud: a silently non-persisting cache is the quieter
89
+ // half of this very defect.
90
+ console.error(`[nsauditor] NVD cache not persisted (${err.code || err.message}) at ` +
91
+ `${path.dirname(this.cacheFile)} — continuing without caching. Set ` +
92
+ 'NSAUDITOR_NVD_CACHE_DIR to a writable path to restore it.');
93
+ }
94
+ }
50
95
  }
51
96
 
52
97
  async get(key) {