nsauditor-ai 0.2.56 → 1.0.0

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.
Files changed (46) hide show
  1. package/README.md +55 -47
  2. package/bin/nsauditor-ai.mjs +4 -2
  3. package/cli.mjs +477 -252
  4. package/mcp_server.mjs +93 -45
  5. package/package.json +2 -2
  6. package/plugin_manager.mjs +91 -10
  7. package/plugins/040_tls_cert_auditor.mjs +246 -61
  8. package/plugins/050_tribe_health.mjs +13 -34
  9. package/plugins/060_dns_sec_auditor.mjs +30 -32
  10. package/plugins/dns_scanner.mjs +16 -8
  11. package/plugins/ftp_banner_check.mjs +21 -13
  12. package/plugins/host_up_check.mjs +36 -73
  13. package/plugins/http_probe.mjs +113 -27
  14. package/plugins/mcp_scanner.mjs +15 -2
  15. package/plugins/netbios_scanner.mjs +32 -13
  16. package/plugins/opensearch_scanner.mjs +4 -0
  17. package/plugins/ping_checker.mjs +10 -4
  18. package/plugins/port_scanner.mjs +29 -49
  19. package/plugins/result_concluder.mjs +166 -33
  20. package/plugins/snmp_scanner.mjs +20 -8
  21. package/plugins/syn_scanner.mjs +6 -2
  22. package/plugins/tls_scanner.mjs +29 -2
  23. package/plugins/upnp_scanner.mjs +82 -13
  24. package/plugins/webapp_detector.mjs +88 -15
  25. package/utils/capabilities.mjs +2 -2
  26. package/utils/conclusion_utils.mjs +39 -1
  27. package/utils/cookie_redaction.mjs +50 -0
  28. package/utils/delta_reporter.mjs +70 -25
  29. package/utils/executive_report.mjs +34 -7
  30. package/utils/export_csv.mjs +14 -22
  31. package/utils/mcp_call_digest.mjs +31 -0
  32. package/utils/net_validation.mjs +124 -37
  33. package/utils/output_dir.mjs +4 -2
  34. package/utils/plugin_discovery.mjs +8 -2
  35. package/utils/ports_spec.mjs +46 -0
  36. package/utils/report_md.mjs +49 -125
  37. package/utils/sarif.mjs +33 -93
  38. package/utils/scan_delta.mjs +173 -18
  39. package/utils/scan_delta_view.mjs +3 -3
  40. package/utils/scan_history.mjs +149 -0
  41. package/utils/scan_pin.mjs +143 -0
  42. package/utils/scheduler.mjs +17 -5
  43. package/utils/service_flags.mjs +363 -0
  44. package/utils/validate.mjs +1 -1
  45. package/utils/watch_cycle.mjs +37 -0
  46. package/utils/webhook.mjs +119 -18
package/README.md CHANGED
@@ -6,35 +6,32 @@ A modular, AI-assisted network security audit platform that scans, understands,
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/nsauditor-ai.svg)](https://www.npmjs.com/package/nsauditor-ai)
8
8
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
- [![Node.js 20+](https://img.shields.io/badge/node-20%2B-green.svg)](https://nodejs.org)
9
+ [![Node.js 24+](https://img.shields.io/badge/node-24%2B-green.svg)](https://nodejs.org)
10
10
  [![Tests](https://img.shields.io/badge/tests-passing-brightgreen.svg)](#tests)
11
11
 
12
12
  ---
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 — 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 in the scan instead of a silent clean (in Enterprise 1.2.0 that gap reaches no compliance control, so a control only the missing CVE rows would fail can read PASS).
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 in the scan instead of a silent clean (from Enterprise 1.3.0 that gap fails, as an evidence gap, the compliance controls the CVE rows map to).
17
17
 
18
18
  ## What's New
19
19
 
20
- **Latest: CE 0.2.56 + Enterprise 1.2.0 — *the delta refuses five more ways a finding could read as fixed without being fixed*.**
21
- The Pro/Enterprise `report --since` delta (introduced in CE 0.2.55) now refuses five more ways a
22
- finding could read as fixed without being fixed: a UDP service that did not answer in the
23
- other run (`port-not-measured`, listed as `<port>/udp`), a CVE row whose lookup failed there, a
24
- finding of an analysis agent that did not run, and — new in this release — **an Enterprise package
25
- that failed to load**, which used to leave its plugins running while the scan skipped its intelligence,
26
- analysis-agent and compliance stages without a word. It is now
27
- named on stderr and recorded on the scan's conclusion, and the delta refuses that host's agent and
28
- CVE-mapper rows as `evidence-gap`. And a CVE row that vanished while the same program and version
29
- still answer is `vulnerability-data-changed` — the vulnerability data moved, not the estate.
30
- **Not refused in this release:** when the two scans ran different `--plugins`, a row that an analysis agent or
31
- the CVE mapper derived from the output of a plugin one scan did not request can read resolved (and in Enterprise,
32
- with SLA tracking on, MTTR counts it closed and the control it failed can read PASS) or new — keep `--plugins`
33
- identical between compared scans. Every
34
- reason's detail names its run absolutely ("this run", "the baseline run"), and two scans of one host
35
- whose plugin runs finished in the same second get distinct directories. Plugin counts UNCHANGED at 27 Community + 29 Enterprise; every coverage matrix UNCHANGED; **Enterprise 1.2.0
36
- requires this release** (`nsauditor-ai >= 0.2.56`). The free last-vs-current webhook alerting delta
37
- is untouched and stays free; in this release it does not fire on a service, version or finding change (see
20
+ **Latest: CE 1.0.0 + Enterprise 2.0.0 — *Node.js 24; a scan connects only to the addresses its host check resolved; the AI payload carries no AWS account ID; on TCP an authoritative probe's row must identify its service to be one*.**
21
+ A scan now connects only to the addresses its host check resolved — on the CLI and through the MCP server, for the target
22
+ and each redirect hop — so a name that answers one address to the check and another at connect (DNS rebinding) cannot
23
+ redirect it; two limits are stated in the [CHANGELOG](./CHANGELOG.md) (an HTTP proxy set with `NODE_USE_ENV_PROXY`, and an
24
+ idle pooled connection). The payload sent to AI no longer carries an AWS account ID, and the system prompt sent with it is
25
+ written beside it. A refused or unidentified TCP port is no longer listed as a service. Plugin 040 tells a weak certificate
26
+ from an untrusted chain. ⚠️ **Security:** `--watch` printed the full `--webhook-url` to stdout; it now prints the host — if
27
+ you ran `--watch` with a Slack or Teams URL where stdout is logged, rotate that URL. The supported runtime is **Node.js 24**.
28
+ Two limits from Enterprise 1.3.0 still hold in the Pro delta and Enterprise MTTR: a scan made before Enterprise 1.3.0
29
+ could not record a plugin left out of it, so in a comparison with one an agent's row that scan lacks is not refused;
30
+ and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the
31
+ CVE mapper's row on a port it did not measure can read RESOLVED and count as closed in MTTR, and the control it failed
32
+ can read PASS — include the port scanner (003).
33
+ Plugin count UNCHANGED at 27 Community + 29 Enterprise; every coverage matrix UNCHANGED; **Enterprise 2.0.0 requires this
34
+ release** (`nsauditor-ai >= 1.0.0`). The free last-vs-current webhook alerting delta is untouched and stays free (see
38
35
  Continuous Monitoring below).
39
36
 
40
37
  For the full per-release history — every prior cycle, in detail — see [CHANGELOG.md](./CHANGELOG.md). This README keeps only the current release headline.
@@ -50,7 +47,7 @@ Scan → Analyze → Prioritize → Track → Act
50
47
  - **Structured finding format** — all findings use a common schema with category, severity, evidence, and remediation — enabling consistent SARIF export and MCP integration
51
48
  - **AI-powered analysis** — send redacted scan results to OpenAI or Claude (your keys, your choice) for vulnerability assessments and remediation guidance
52
49
  - **Risk-scored prioritization (Pro/Enterprise)** — findings carry a composite risk score (CVSS weighted by verification status, with an uplift for initial-access techniques) and a status field. Enterprise adds an operator suppression workflow for compliance findings (accepted-risk / false-positive with expiry): the compliance report shows a suppressed finding as suppressed, naming its approver and rationale, and an expired suppression no longer applies
53
- - **Continuous monitoring (CTEM)** — watch mode rescans on a schedule and compares each host's scan with its previous one; in this release its webhook alert does not fire on a service, version or finding change
50
+ - **Continuous monitoring (CTEM)** — watch mode rescans on a schedule, compares each host with the previous cycle and alerts a host whose scan changed and that carries a finding at or above `--alert-severity`
54
51
  - **MCP integration** — expose scanning tools to AI assistants like Claude Code via Model Context Protocol
55
52
  - **CI/CD ready** — SARIF output with `--fail-on` severity gating for pipeline integration
56
53
 
@@ -80,7 +77,7 @@ NSAuditor AI is available in three editions: Community (free, MIT-licensed, no r
80
77
  | Zero Trust assessment | — | — | ✅ |
81
78
  | **Enterprise — compliance (8 frameworks)** | | | |
82
79
  | SOC 2 (AICPA TSC 2017) — 10 covered + 4 partial controls | — | — | ✅ |
83
- | HIPAA Security Rule §164.312 — Zero BAA required | — | — | ✅ |
80
+ | HIPAA Security Rule §164.312 — no BAA with Nsasoft required | — | — | ✅ |
84
81
  | NIST CSF 2.0 Core — Subcategory-level mapping (106 of 107 Subcategories) | — | — | ✅ |
85
82
  | PCI DSS v4.0.1 — Sub-requirement-level mapping for QSA RoC | — | — | ✅ |
86
83
  | ISO/IEC 27001:2022 — per-Annex-A-code mapping + SoA discipline (93 Annex A controls) | — | — | ✅ |
@@ -162,13 +159,14 @@ npm install
162
159
  node --env-file=.env cli.mjs scan --host 192.168.1.1 --plugins all
163
160
  ```
164
161
 
165
- Results land in `./out/<host>_<timestamp>/`:
162
+ Results land in `./out/<host>_<timestamp>/` (two scans of one host whose plugin runs finished in the same second get distinct directories):
166
163
 
167
164
  | File | Contents |
168
165
  |---|---|
169
166
  | `scan_conclusion_raw.json` | Full unredacted conclusion (admin reference) |
170
167
  | `scan_conclusion_raw.html` | Admin RAW HTML with filters and full detail |
171
168
  | `scan_response_ai_payload.json` | Redacted payload sent to AI |
169
+ | `scan_response_ai_system_prompt.txt` | System prompt sent to AI, the Pro/Enterprise intelligence block included (from Community 1.0.0) |
172
170
  | `scan_response_ai.json` | Raw AI API response |
173
171
  | `scan_response_ai.txt` | AI conclusion (markdown) |
174
172
  | `scan_response_ai.html` | Styled HTML report with CVE links and badges |
@@ -176,13 +174,13 @@ Results land in `./out/<host>_<timestamp>/`:
176
174
  | `scan_results.csv` | CSV — only with `--output-format csv` |
177
175
  | `scan_report.md` | GitHub-flavored Markdown report — only with `--output-format md` (or `markdown`) |
178
176
 
179
- > Works on Node 20+ (tested on Node 22).
177
+ > Requires Node.js 24 or newer (tested on Node 24).
180
178
 
181
179
  ---
182
180
 
183
181
  ## MCP Server
184
182
 
185
- > **Heads-up on AI-client fabrication.** Some MCP clients (notably Claude Desktop) can silently substitute AI-generated responses if a `tools/call` times out, instead of surfacing the failure. Every response from this server now ends with a `── Verified MCP call ──` footer and a UUID. Run `nsauditor-ai mcp verify-call <id>` to confirm a response is genuine before acting on it. Full background and workflow: [docs/mcp-verification.md](./docs/mcp-verification.md). When in doubt, generate compliance evidence via the CLI (`nsauditor-ai scan ...`), which has no MCP client in the path.
183
+ > **Heads-up on AI-client fabrication.** Some MCP clients (notably Claude Desktop) can silently substitute AI-generated responses if a `tools/call` times out, instead of surfacing the failure. Every response from this server ends with an `── MCP call receipt ──` footer and a UUID. To check that a response is the text the server actually produced, save it (footer included) and run `nsauditor-ai mcp verify-call <id> --response <file>`: exit 0 means this exact text came from that call (the line also says how long ago it was issued), 1 means it did not (or the id was never issued), and 3 means the id was issued but nothing bound the text to it — the id alone never verifies a response, because a real id can be pasted under a fabricated one. Full background and workflow: [docs/mcp-verification.md](./docs/mcp-verification.md). When in doubt, generate compliance evidence via the CLI (`nsauditor-ai scan ...`), which has no MCP client in the path.
186
184
 
187
185
  Expose scanning capabilities to AI assistants via [Model Context Protocol](https://modelcontextprotocol.io):
188
186
 
@@ -202,7 +200,7 @@ found". A higher tier does not add tools; it unlocks the ones already in the lis
202
200
 
203
201
  | Tool | Purpose |
204
202
  |---|---|
205
- | `scan_host` | Run a full plugin scan against a host — service detection, OS fingerprint and the flags the service checks leave on each service record (weak SSH algorithms, SNMP community, weak TLS protocols / ciphers and a self-signed certificate, the MCP server checks; anonymous FTP login and zone transfer only when `FTP_CHECK_ANON` / `DNS_CHECK_AXFR` are set — both off by default). It does NOT look up CVEs, run the analysis agents, or return the HTTP probe's dangerous methods (006), SMB null sessions (014), the TLS-certificate / debug-endpoint / DNS-security auditors' findings (040 / 050 / 060) or Enterprise's zero-trust assessment (1023), so zero findings is not a clean verdict (use `get_vulnerabilities` per service CPE and `probe_service` for those plugins — both Pro — or the CLI scan with the Enterprise package and a Pro or Enterprise licence) |
203
+ | `scan_host` | Run a full plugin scan against a host — service detection, OS fingerprint and the flags the service checks leave on each service record (weak SSH algorithms, SNMP community, weak TLS protocols / ciphers and a self-signed certificate, the MCP server checks; anonymous FTP login and zone transfer only when `FTP_CHECK_ANON` / `DNS_CHECK_AXFR` are set — both off by default). Its records also carry the HTTP probe's dangerous methods (006; `methodsTested` false means no Allow header was read — not tested, never "none"), SMB null sessions (014; only with `SMB_NULL_SESSION` set) and the TLS-certificate / debug-endpoint / DNS-security auditors' findings (040 / 050 / 060 — the DNS-security audit tries a zone transfer against the domain's nameservers only with `DNS_AUDIT_AXFR_CHECK` set, also off by default, and records only an open transfer; for a domain-name target the DNS-security audit lands on the 53/udp record when the scan found a 53/udp service, otherwise in the conclusion's evidence; for an IP-address target it declines, with its reason on the manifest and a NOT TESTED line in the markdown). Each of the three record-level opt-in checks records whether it ran beside its result — `axfrTested` / `anonymousLoginTested` / `nullSessionTested` is `true` when measured, else the reason (`opt-in-off`, `no-domain`, `no-answer`) — and a null result is not tested, never "refused". It does NOT look up CVEs or run the analysis agents; with the Enterprise package the zero-trust assessment (1023) reaches it only as one score line in the evidence. So zero findings is not a clean verdict (use `get_vulnerabilities` per service CPE and `probe_service` for those plugins — both Pro — or the CLI scan with the Enterprise package and a Pro or Enterprise licence) |
206
204
  | `list_plugins` | List available scanner plugins with their IDs, priorities and requirements |
207
205
  | `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) |
208
206
 
@@ -236,7 +234,7 @@ found". A higher tier does not add tools; it unlocks the ones already in the lis
236
234
  > has no MCP client in the path — there you can raise `PLUGIN_TIMEOUT_MS` (e.g. `90000`) freely. On the CLI a
237
235
  > plugin that declares its own budget already gets it; `PLUGIN_TIMEOUT_CEILING_MS` caps those.
238
236
 
239
- Security: SSRF protection on all host inputs (blocks RFC 1918, loopback, fc00::/7, cloud metadata), port validation (1–65535), CPE format enforcement, dependency injection for test isolation. **Server-startup authentication is required** — see next section.
237
+ Security: SSRF protection on every host the MCP server is handed — the address it resolves to is checked (every answer), loopback, link-local and cloud-metadata addresses are always refused, and RFC 1918, CGNAT and fc00::/7 are refused unless `NSA_ALLOW_ALL_HOSTS` is set; a host the scanned target names — a redirect hop, a UPnP device's description URL — is checked the same way by the plugin that would follow it. Since CE 1.0.0 a scan connects to the addresses its check resolved — the target's, and each redirect hop's — so a name that answers one address to the check and another when the scan connects (DNS rebinding) is not followed; the system tools a scan runs (`ping`, `nping`, `hping3`, `nmap`, `curl`) are handed the address. Two limits: with `NODE_USE_ENV_PROXY` and a proxy configured, an HTTP request goes through the proxy by name and the proxy resolves it; and a request can reuse an idle connection the same process opened earlier to the same name, for as long as that connection stays idle in the pool — a few seconds by default; a server's Keep-Alive hint holds it longer. One exception, off by default: with `DNS_AUDIT_AXFR_CHECK` set, the DNS-security audit (060) opens TCP/53 to each of the scanned domain's nameservers, at the first IPv4 address each nameserver's name resolves to — an address no host check looked at, which neither the guard nor the pin covers. Port validation (1–65535), CPE format enforcement, dependency injection for test isolation. **Server-startup authentication is required** — see next section.
240
238
 
241
239
  ### Authentication (required)
242
240
 
@@ -320,10 +318,12 @@ the directory your shell finds `nsauditor-ai-mcp` in. Add any other variables to
320
318
  The exact `NSA_MCP_AUTH_KEY` value to paste is printed by `nsauditor-ai mcp install-key` — on macOS it's the `keychain:NSA_MCP_AUTH_KEY` placeholder shown above; on Linux/Windows it's the literal key value (and you should `chmod 600` your config file).
321
319
 
322
320
  - `NSA_MCP_AUTH_KEY` — **required** (see Authentication section above)
323
- - `NSA_ALLOW_ALL_HOSTS=1` — required to scan private/RFC 1918 addresses (e.g., `192.168.x.x`). It turns off the MCP
324
- server's check of what a host name resolves to, so a name that resolves to a loopback or cloud-metadata address then gets
325
- through too; targets written as `localhost`, `127.x`, `::1`, `169.254.x` or `metadata.google…` stay refused
326
- - `FTP_CHECK_ANON=true` — lets the FTP check try an anonymous login (off by default; without it `scan_host` never reports anonymous FTP). `DNS_CHECK_AXFR=true` with `DNS_AXFR_DOMAIN=<zone>` — lets the DNS check try a zone transfer of that zone (off by default). The same variables govern the CLI scan.
321
+ - `NSA_ALLOW_ALL_HOSTS=1` — required to scan private/RFC 1918 addresses (e.g., `192.168.x.x`); only `1`, `true`, `yes`
322
+ or `on` turn it on. It admits private ranges only: loopback, link-local and cloud-metadata addresses stay refused over
323
+ MCP whether they are written as an address in any spelling or reached through what a host name resolves to — every
324
+ answer is checked. Since CE 1.0.0 the scan connects to the answers the check resolved, so a name whose answer changes
325
+ between the check and the scan (DNS rebinding) is not followed — the two limits are in the Security note above
326
+ - `FTP_CHECK_ANON=true` — lets the FTP check try an anonymous login (off by default; without it `scan_host` never reports anonymous FTP). `DNS_CHECK_AXFR=true` with `DNS_AXFR_DOMAIN=<zone>` — lets the DNS check try a zone transfer of that zone (off by default). `DNS_AUDIT_AXFR_CHECK=true` (or `1`, `yes`, `on`) — lets the DNS-security audit (060) try a zone transfer of the scanned domain against each of its nameservers (off by default; it connects to each nameserver's own address, outside the scan's host check — see the Security note above). `SMB_NULL_SESSION=true` — lets the NetBIOS check try an SMB null session (off by default). The FTP, DNS and NetBIOS checks, when off or when they ran and could not complete (a timeout, a dropped connection), are recorded and reported as not tested with their reason — never as refused; the DNS-security audit's attempt records only an open transfer, so its absence is not a measured "closed". The same variables govern the CLI scan.
327
327
  - `PLUGIN_TIMEOUT_MS=5000` — the per-plugin budget for `scan_host` and `probe_service` (default 30000), and it binds every plugin there, including one that declares a longer budget of its own. It bounds each PLUGIN, not the call: `scan_host` runs a host's plugins one after another, so the call takes roughly the sum of their times. On one router a `scan_host` call timed out in Claude Desktop on 2026-08-10 and one returned within 138 s on 2026-09-30, so neither outcome is promised: a call that times out returned no result — never read it as a clean host. On a real home gateway the plugins of one full scan took ~113 s between them at the default budget.
328
328
  - `PLUGIN_TIMEOUT_CEILING_MS` — the upper bound on any plugin's DECLARED budget (default 120000). A few plugins declare their own budget because their cost grows with what they scan (the UPnP and MCP scanners here; several cloud auditors in Enterprise). On the CLI a declaration outranks `PLUGIN_TIMEOUT_MS`, so this is the setting that caps every plugin there. The MCP tools already bind declarations with their own limits.
329
329
  - `CLOUD_SCAN_CONCURRENCY` — max cloud plugins run at once by `scan_cloud` (default 20).
@@ -390,7 +390,7 @@ claude mcp add nsauditor-ai \
390
390
 
391
391
  **`mcp status` shows `⚠ Created: ... — > 90d threshold`** → key is older than the 90-day rotation cadence. Run `nsauditor-ai mcp rotate-key --confirm` and update Claude Desktop config with the new key. Server emits the same warning to stderr at every startup.
392
392
 
393
- **Claude Desktop reports "Current tier: CE" despite `nsauditor-ai license --status` showing Enterprise** → first run `nsauditor-ai mcp tier` to get the ground-truth tier the MCP server actually resolves at startup. If `mcp tier` reports `enterprise` but Claude Desktop's `list_plugins` says CE, the AI client is synthesizing the response without actually calling the tool — see [docs/mcp-verification.md](./docs/mcp-verification.md) and verify any suspicious response with `nsauditor-ai mcp verify-call <id>`.
393
+ **Claude Desktop reports "Current tier: CE" despite `nsauditor-ai license --status` showing Enterprise** → first run `nsauditor-ai mcp tier` to get the ground-truth tier the MCP server actually resolves at startup. If `mcp tier` reports `enterprise` but Claude Desktop's `list_plugins` says CE, the AI client is synthesizing the response without actually calling the tool — see [docs/mcp-verification.md](./docs/mcp-verification.md) and verify any suspicious response with `nsauditor-ai mcp verify-call <id> --response <file>`.
394
394
 
395
395
  If `mcp tier` itself reports CE → genuine resolution failure. Inspect the license storage:
396
396
 
@@ -463,14 +463,20 @@ nsauditor-ai --version (or -v, or `version`)
463
463
  | `--out <dir>` | Custom output directory — applies to the per-scan folder *and* to alternate-format files (SARIF/CSV/Markdown) | `out/` |
464
464
  | `--parallel <n>` | Concurrent host scans | `1` |
465
465
  | `--output-format <fmt>` | Additional output format: `sarif` (CI/CD) · `csv` (spreadsheet) · `md` or `markdown` (chat/PR/Slack quotable) | — |
466
- | `--fail-on <sev>` | Exit code 1 if a flag it gates on is ≥ severity (`critical\|high\|medium\|low\|info`): anonymous FTP login and DNS zone transfer are critical, weak SSH algorithms medium, and any concluded scan counts as info — so `--fail-on info` fails every scan that concludes. Anonymous FTP login and zone transfer are tested only when `FTP_CHECK_ANON=true` / `DNS_CHECK_AXFR=true` with `DNS_AXFR_DOMAIN` are set, so on a default scan `--fail-on high` and `--fail-on critical` never exit 1. It also reads dangerous HTTP methods (medium), but no scan's conclusion carries that flag today. It does not read the SNMP default community, weak TLS protocols / ciphers, a self-signed certificate, the MCP server checks, Enterprise's CVE rows or agent findings, so exit 0 is not a clean host; exit 2 for an unknown severity or no conclusion | — |
466
+ | `--fail-on <sev>` | Exit code 1 if any finding is ≥ severity (`critical\|high\|medium\|low\|info`). It reads the same findings the Markdown, SARIF and CSV reports count — one shared table grades them, one finding per item: anonymous FTP login, DNS zone transfer and MCP unauthenticated access or tool listing are critical; an SNMP default community, an expired certificate, an SMB null session, MCP cleartext transport and a deprecated MCP protocol are high; weak SSH algorithms, weak TLS protocols and negotiated ciphers, dangerous HTTP methods, a self-signed certificate and an exposed MCP Inspector are medium; the TLS-certificate (040), debug-endpoint (050) and DNS-security (060) audits grade each finding at its own severity, and 060 grades the scanned domain's SPF / DMARC / NS posture (and, with `DNS_AUDIT_AXFR_CHECK` set, an open zone transfer) whether or not the host answered on 53/udp. **Since 1.3.0 it reads all of these — a pipeline that passed before may fail on findings that were always there.** Anonymous FTP login, zone transfer and the SMB null session are tested only when `FTP_CHECK_ANON=true` / `DNS_CHECK_AXFR=true` with `DNS_AXFR_DOMAIN` / `SMB_NULL_SESSION=true` are set (the DNS-security audit's own zone-transfer attempt only when `DNS_AUDIT_AXFR_CHECK` is set); dangerous HTTP methods are read only where the HTTP probe read an Allow header; where it read none they are not tested, and that never trips the gate. Any concluded scan counts as info — so `--fail-on info` fails every scan that concludes. It does not read Enterprise's CVE rows or agent findings, so exit 0 is not a clean host; exit 2 for an unknown severity or no conclusion | — |
467
467
  | `--insecure-https` | Accept self-signed TLS certificates | `false` |
468
- | `--watch` | CTEM continuous **alerting** loop — re-scan on `--interval`, compare each host with its previous scan (`[ScanHistory]` lines), webhook on `--alert-severity` — but not on a service, version or finding change in this release (see Continuous Monitoring). 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` |
468
+ | `--watch` | CTEM continuous **alerting** loop — re-scan on `--interval`, compare each host with the previous cycle, webhook for a host whose scan changed and that carries a finding at or above `--alert-severity`; the first cycle sets the baseline and alerts nobody (see Continuous Monitoring). Not an evidence cadence: no retention, no cross-run aggregation, skips SARIF/CSV/Markdown + `--fail-on`, dies with the process. Scans each distinct host once per cycle (a one-shot `--host X,X` scans `X` twice). Use a scheduler for SOC 2 Type II history. | `false` |
469
469
  | `--interval <min>` | Rescan interval in minutes (requires `--watch`) | `60` |
470
470
  | `--webhook-url <url>` | Webhook URL for watch-mode alerts (see `--watch`) | — |
471
471
  | `--alert-severity <sev>` | Minimum severity for webhook alerts | `high` |
472
+ | `--alert-every-cycle` | Alert every host with a finding at or above `--alert-severity` on every watch cycle, the first included (default: only hosts whose scan changed since the previous cycle) | `false` |
473
+ | `--alert-payload <mode>` | `full` — the watch alert as it has always been · `minimal` — drops the host, the service and the description: a count, and each finding's port, protocol and severity | `full` |
472
474
  | `--compliance <fw>` | Compliance framework to map findings into. Accepts CSV for multi-framework runs (e.g. `soc2`, `hipaa`, `nist-csf`, `pci-dss`, `iso-27001`, `cis-v8`, `gdpr`, `nist-800-171`, or any combination like `soc2,hipaa,nist-csf,pci-dss,iso-27001,cis-v8,gdpr,nist-800-171` — `all` expands to the same eight). **Enterprise license required.** Supported frameworks as of EE 0.40.0 — **eight**: `soc2` (AICPA TSC 2017) + `hipaa` (HIPAA Security Rule §164.312 Technical Safeguards) + `nist-csf` (NIST Cybersecurity Framework 2.0 Core, CSWP 29 Feb 2024) + `pci-dss` (PCI DSS v4.0.1, PCI SSC June 2024 errata) + `iso-27001` (ISO/IEC 27001:2022, ISO + IEC October 2022) + `cis-v8` (CIS Critical Security Controls v8, CIS May 2021 / v8.1 errata June 2024) + `gdpr` (GDPR Article 32 / Security of Processing, Regulation (EU) 2016/679 — Art. 32 infrastructure substrate, **not** GDPR compliance) + `nist-800-171` (NIST SP 800-171 Rev 2 — evidence substrate for **CMMC Level 2 preparation**, **not** a CMMC certification, **not** a FedRAMP authorization, and no MET/NOT MET determination or SPRS score). See `@nsasoft/nsauditor-ai-ee` README for per-framework coverage details. | — |
473
475
  | `--compliance-scope <path>` | Optional JSON file describing the assessment scope (passed to the compliance engine for cover-page attestation) | — |
476
+ | `--notify-webhook <url>` | **Enterprise; one-shot scans with `--compliance` only, never `--watch`.** Posts the scan's drift (`scan_drift_<fw>.json`) when a control goes to fail or loses measurement, when a new violation is at or above `--notify-severity`, or when this run could not be compared with the previous one. Public `https` URLs only — anything else is refused with exit 2 | — |
477
+ | `--notify-format <fmt>` | `generic` · `slack` · `teams` (a Teams Workflows webhook, which takes an Adaptive Card) | `generic` |
478
+ | `--notify-severity <sev>` | Threshold for NEW violations only (`critical\|high\|medium\|low\|info`); a control going to fail or losing measurement always sends | `high` |
479
+ | `--notify-payload <mode>` | `minimal` — framework, counts, control ids, severities and scanner ids; no hostname, resource name, account id or finding text · `full` adds them | `minimal` |
474
480
  | `--help`, `-h` | Print usage block (subcommands, flags, env vars, examples) and exit 0 | — |
475
481
  | `--version`, `-v` | Print `nsauditor-ai <version>` and exit 0 | — |
476
482
 
@@ -499,8 +505,8 @@ nsauditor-ai scan --host 192.168.1.0/24 --plugins all --parallel 10
499
505
  # Targeted scan: TLS + HTTP + DNS + OS detection
500
506
  nsauditor-ai scan --host 192.168.1.8 --plugins 011,006,009,013,008
501
507
 
502
- # SARIF output for CI/CD. --fail-on high exits 1 only on anonymous FTP / zone transfer, which are tested only with
503
- # FTP_CHECK_ANON / DNS_CHECK_AXFR set — see the --fail-on row; exit 0 is not a clean host
508
+ # SARIF output for CI/CD. --fail-on high exits 1 on any finding graded high or critical — an SNMP default community,
509
+ # an expired certificate, MCP exposure, a 040 / 050 / 060 audit finding … — see the --fail-on row; exit 0 is not a clean host
504
510
  nsauditor-ai scan --host 10.0.0.5 --plugins all --output-format sarif --fail-on high
505
511
 
506
512
  # Markdown report — paste straight into a GitHub issue, Slack thread, or chat
@@ -553,7 +559,7 @@ HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=3 \
553
559
  | `--format executive` | Self-contained, print-ready HTML report; no external network reference other than an `http(s)`/`mailto` `<a href>` | — |
554
560
  | `--format jira` | Jira-importer CSV (`Summary`/`Description`/`Priority`/`Labels`/`External ID`); the import mapping is done in Jira and has not been verified against a live Jira instance. **`pass`-tier records are EXCLUDED** — a Jira issue is a work item and a passing check is not work; every other tier is kept, INFO included, because INFO carries the evidence gaps and scope boundaries. Passing checks appear in the `executive` report under their own **PASS** tier. | — |
555
561
  | `--run <id>` | Report a specific run id instead of the newest one under `--from` | newest run |
556
- | `--since <runId\|prior>` | Compare the reported run with a baseline run under `--from`: `prior` is the run that started immediately before it, or name a run id. The `executive` report lists each new, resolved and changed finding, and each finding that could NOT be compared with its reason; stdout prints the counts, the changed and not-comparable rows and the limits; the `jira` CSV carries none of it. Keep `--plugins` identical between the two scans (see What's New). Every refusal exits 2: if the baseline was found but cannot support a comparison, the `executive` report is still written, states the refusal and lists no new, resolved or changed finding; if no baseline is found, if the reported run's own record fails verification, or with `--format jira`, nothing is written | — |
562
+ | `--since <runId\|prior>` | Compare the reported run with a baseline run under `--from`: `prior` is the run that started immediately before it, or name a run id. The `executive` report lists each new, resolved and changed finding, and each finding that could NOT be compared with its reason; stdout prints the counts, the changed and not-comparable rows and the limits; the `jira` CSV carries none of it. Each new, resolved and changed row carries a **Basis** cell saying what made it comparable: for a plugin's row, the host, plugin and scope present in both runs; for an Enterprise analysis agent's or the CVE mapper's row, that the run lacking it recorded no evidence gap from that producer covering it, and — for a CVE or end-of-life row on a TCP port — the measurement itself (the service identified, or the port closed). When the TLS scanner (011) could not complete a handshake on a port the port scanner saw open, its row names the transport error (`ECONNRESET`, `ETIMEDOUT`, …) and Enterprise records the port as not measured, so an earlier TLS finding there is not comparable — except when another port of the same scan completed a TLS handshake: then the failed port is not read, and such a finding can read as resolved (a stated limit). Where the run lacking an agent row predates EE 1.3.0, the cell says that run could not record an input plugin left out of the scan, so the row is not refused. When the baseline was made before Enterprise 1.3.0, keep `--plugins` identical between the two scans (see What's New). Every refusal exits 2: if the baseline was found but cannot support a comparison, the `executive` report is still written, states the refusal and lists no new, resolved or changed finding; if no baseline is found, if the reported run's own record fails verification, or with `--format jira`, nothing is written | — |
557
563
  | `--brand <brand.json>` | Cover-page branding (company name, prepared-by, contact, logo) — `--format executive` only | unbranded |
558
564
  | `--out <path>` | Write to this path instead of `report_<runId>.<ext>` beside the run record | `report_<runId>.<ext>` |
559
565
  | `--allow-partial` | Render even if some requested hosts were never written, or the run never recorded completion — the caveat is stated on the cover, never hidden | `false` |
@@ -677,7 +683,7 @@ No license key? Everything in this repository works perfectly without one. The C
677
683
  | 024 | TCP SYN Scanner | TCP (Nmap) | SYN half-open scan via Nmap wrapper (optional) |
678
684
  | 040 | TLS Certificate & Cipher Auditor | TCP:443+ | Cert expiry, chain integrity, hostname mismatch, weak ciphers, deprecated protocols, key strength |
679
685
  | 050 | TRIBE v2 Neural API Security Probe | TCP/HTTP:8080 | Debug leak detection, stack traces in errors, header security, CORS misconfiguration, unauthenticated routes |
680
- | 060 | DNS Security Auditor | DNS/UDP:53 | SPF/DKIM/DMARC, dangling CNAMEs, DNSSEC, NS delegation, zone transfer exposure, MX security, CAA records |
686
+ | 060 | DNS Security Auditor | DNS/UDP:53 | SPF/DKIM/DMARC, dangling CNAMEs, DNSSEC, NS delegation, zone transfer exposure (opt-in: `DNS_AUDIT_AXFR_CHECK`), MX security, CAA records |
681
687
  | 070 | MCP Scanner | TCP/HTTP+SSE | Detects MCP (Model Context Protocol) servers on candidate ports (1967, 3000, 3005, 5173, 6274, 6277, 8000, 8090). Audits for cleartext transport (HTTP not HTTPS), missing/anonymous auth, anonymous tool enumeration, deprecated protocol versions, and Inspector exposure on non-loopback. Maps findings to CWE/OWASP/MITRE per the FindingSchema. STDIO-transport MCP servers are out of scope (no network port). |
682
688
 
683
689
  ### Discovery Plugins
@@ -705,7 +711,7 @@ See [Pro & Enterprise Activation](#pro--enterprise-activation) to install a lice
705
711
 
706
712
  Every compliance scan already produces a GRC-ready JSON evidence artifact. The **GRC connectors** take the next step: they map each NSAuditor compliance finding to your GRC platform's own evidence/test records and **push them at scan time** — so your Vanta, Drata, or Secureframe workspace reflects the latest cloud posture without a manual export/import round-trip.
707
713
 
708
- **Opt-in, and Zero-Data-Exfiltration by default.** The push is off unless you set the environment variables below. When it runs, finding text and target identifiers go to your platform as written unless you set `COMPLIANCE_GRC_REDACTION` to `hash` or `remove` (redaction is off by default); the persisted audit log stores a body **fingerprint** (never the raw payload), and your API token is never written to any artifact. Nothing leaves your infrastructure that you didn't opt into.
714
+ **Opt-in, and Zero-Data-Exfiltration by default.** The push is off unless you set the environment variables below. When it runs, finding text and target identifiers go to your platform as written unless you set `COMPLIANCE_GRC_REDACTION` to `hash` or `remove` (redaction is off by default); the persisted audit log stores a body **fingerprint** (never the raw payload), and your API token is never written to any artifact. The push sends only what you opted into, and nothing goes to Nsasoft.
709
715
 
710
716
  ```bash
711
717
  # Enable the scan-time push (Enterprise)
@@ -744,7 +750,7 @@ The Result Concluder (plugin 008) merges all plugin outputs into a normalized st
744
750
 
745
751
  NSAuditor AI supports three AI providers for vulnerability analysis. **All providers work in all tiers** — CE, Pro, and Enterprise. AI is optional; the platform is fully functional without it.
746
752
 
747
- **Providers:** OpenAI (GPT-4o), Anthropic Claude (Sonnet/Opus), Ollama (fully local)
753
+ **Providers:** OpenAI (default model `gpt-6-luna` from Community 1.0.0; set `OPENAI_MODEL` for another), Anthropic Claude (Sonnet/Opus), Ollama (fully local). Setup, exactly what is sent and what is written: [docs/openai.md](./docs/openai.md).
748
754
 
749
755
  **What changes by tier is the prompt content, not the provider:**
750
756
 
@@ -752,7 +758,7 @@ NSAuditor AI supports three AI providers for vulnerability analysis. **All provi
752
758
  - **Pro** — intelligence-enriched prompts (CVE matches, MITRE ATT&CK technique annotations, composite risk scores injected into the prompt). Same API call, better-grounded output
753
759
  - **Enterprise** — Pro prompts + compliance context
754
760
 
755
- **Redaction:** On by default (`OPENAI_REDACT=false` turns it off), the redaction pipeline masks IP addresses, MAC addresses, serial numbers, and configurable confidential keywords before the payload reaches an AI API. Admin RAW reports retain full detail for internal review.
761
+ **Redaction:** On by default (`OPENAI_REDACT=false` turns it off). Before the scan payload reaches an AI API it masks the scanned host, IPv4 addresses (private ones in service and evidence text), colon-separated MAC addresses, full-form IPv6 addresses, serial numbers, a scanned service's Set-Cookie values, and any field named in `CONFIDENTIAL_KEYWORDS`. From Community 1.0.0 it also masks AWS account IDs found in ARNs and AWS URLs, everywhere they occur and inside resource names (the names are kept), and any other 12-digit number under a neutral label (0.2.57 and earlier send both). **It does not mask** cloud resource names, AWS access-key IDs, hostnames, e-mail addresses and certificate subjects in service text, compressed IPv6 addresses, or dash- and dot-form MAC addresses (measured 2026-10-11) — inspect `scan_response_ai_payload.json` before using the AI step on production data. On Pro and Enterprise the intelligence block travels in the system prompt, which the redactor does not see and the payload file does not record; [docs/openai.md](./docs/openai.md) has the details. Admin RAW reports retain full detail for internal review.
756
762
 
757
763
  ```ini
758
764
  # .env
@@ -784,9 +790,10 @@ nsauditor-ai scan --host 192.168.1.0/24 --plugins all \
784
790
  ```
785
791
 
786
792
  - **Scheduling** with configurable intervals and concurrency control
787
- - **Change detection** — each host's scan is compared with that host's previous line in `scan_history.jsonl` and printed as a `[ScanHistory]` line: new, removed and changed services, and the findings delta
788
- - **Webhook alerts** — a JSON POST, retried up to twice 1 s apart on a 5xx or network error, not on a 4xx. ⚠️ **In this release the webhook does NOT fire on a service, version or finding change:** the cycle comparison it is gated on finds no services in the scan results it is handed, so its `=== Delta Report ===` shows no change for any host. The gate opens only when a cycle scanned a different set of hosts from the cycle before (in practice, one cut short by stopping the loop); an alert is then posted for each host whose services carry a flag at or above `--alert-severity` (at `info`, every service counts). Read changes from the `[ScanHistory]` lines.
789
- - **SSRF protection** — private, loopback, and cloud metadata addresses blocked at the scan entry point and inside `sendWebhook()`. `NSA_ALLOW_ALL_HOSTS=1` lifts the scan-entry guard for local network auditing — on the CLI the whole guard, loopback and metadata included; over MCP, the check of what a host name resolves to — and never the webhook guard
793
+ - **Change detection** — each host's scan is compared with that host's previous line in `scan_history.jsonl` and printed as a `[ScanHistory]` line: new, removed and changed services, the findings delta, and — since 1.3.0 — each service-check finding that appeared, cleared or was first observed, and each one that could not be compared, with the reason (a check that did not run this time is never read as cleared). The first scan after upgrading says the service checks were not compared, because the earlier line did not record them
794
+ - **Webhook alerts** — a JSON POST, retried up to twice 1 s apart on a 5xx or network error, not on a 4xx. Each cycle is compared with the one before, host by host, and printed as an `=== Delta Report ===`. A host has CHANGED when a service appeared, went or changed, its finding count moved or could not be compared, a service check appeared, cleared or could not be compared, or the two cycles could not be compared at all. An alert is posted for each changed host carrying a finding at or above `--alert-severity` — one detail per finding with its own grade, from the table `--fail-on` reads (at `info`, every finding counts; a host with none gets no alert). The first cycle establishes the baseline and does not alert, whatever it finds; `--alert-every-cycle` alerts every host carrying such a finding on every cycle, the first included. A host whose scan failed is reported on stdout and does not alert in this release.
795
+ - **What watch alerts cover** — `--watch` alerts cover network-scan findings; cloud-plugin findings and compliance control status do not alert — use the drift file and its notification on a scheduled one-shot scan (Enterprise: `--notify-webhook`). `--alert-payload minimal` drops the host, the service and the description from each alert; `full` stays the default. The start-up line names the webhook's host and never its URL: a Slack or Teams webhook URL carries its own credential
796
+ - **SSRF protection** — private, loopback, and cloud metadata addresses blocked at the scan entry point and inside `sendWebhook()`, which connects to the address its own check resolved, so a webhook name that answers differently between the check and the connect is not followed (with `NODE_USE_ENV_PROXY` and a proxy configured, Node sends the webhook through that proxy by name and the proxy resolves it — the pin then covers only the hop to the proxy; since CE 1.0.0 a scanned host and each redirect hop are pinned the same way — see the MCP security note), and at every host a scanned target names: the webapp detector re-checks and pins each redirect hop (at most 5) and sends your `HTTP_EXTRA_HEADERS` only to the origin you named, the UPnP scanner fetches a device description only from the device that answered, and the OpenSearch scanner does not follow redirects — the one exception is the DNS-security audit's zone-transfer attempt, off by default (see the MCP security note). `NSA_ALLOW_ALL_HOSTS=1` lifts the scan-entry guard for local network auditing — on the CLI the whole guard, loopback and metadata included; over MCP, the private-range check only, so loopback, link-local and metadata stay refused — and never the webhook guard
790
797
  - **Scan history** stored in `scan_history.jsonl` in the output directory (`--out`, default `out/`; 7-day retention in CE)
791
798
 
792
799
  ---
@@ -797,7 +804,7 @@ Configuration is entirely environment-based — a `.env` file, `--env <file>`, o
797
804
 
798
805
  **→ [Configuration reference](./docs/configuration.md)** — every environment variable, its default and its effect.
799
806
 
800
- The two that change behaviour most: `NSAUDITOR_OFFLINE_ONLY=1` forbids outbound CVE lookups and reads a local NVD store instead (reporting an explicit coverage gap in the scan rather than a silent clean; in Enterprise 1.2.0 that gap reaches no compliance control, so a control only the missing CVE rows would fail can read PASS), and `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / a local Ollama endpoint enable the opt-in AI analysis pass.
807
+ The two that change behaviour most: `NSAUDITOR_OFFLINE_ONLY=1` forbids outbound CVE lookups and reads a local NVD store instead (reporting an explicit coverage gap in the scan rather than a silent clean; from Enterprise 1.3.0 that gap fails, as an evidence gap, the compliance controls the CVE rows map to), and `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / a local Ollama endpoint enable the opt-in AI analysis pass.
801
808
 
802
809
  ## Developing Plugins
803
810
 
@@ -903,6 +910,7 @@ Tests use Node.js built-in `--test` runner with the `assert` module — no exter
903
910
  | SYN scan requires root | Run with `sudo` or use TCP connect scanner (plugin 003) instead |
904
911
  | Webhook URL rejected | Private/loopback/cloud metadata blocked by SSRF guard. `NSA_ALLOW_ALL_HOSTS` does not lift this guard: use a public webhook URL |
905
912
  | EE plugins not loading | Verify `@nsasoft/nsauditor-ai-ee` is installed and license key is set |
913
+ | An Enterprise package that fails to load (e.g. installed beside a Community older than its `peerDependencies` floor) | Since CE 0.2.56 it is named on stderr and recorded on the scan's conclusion; before that it left its plugins running while the scan skipped its intelligence, analysis-agent and compliance stages without a word. Install the Community version Enterprise requires |
906
914
 
907
915
  ---
908
916
 
@@ -936,7 +944,7 @@ Commonly requested plugins: RDP, VNC, SMTP/POP3/IMAP, MySQL/PostgreSQL/MSSQL/Mon
936
944
 
937
945
  For the full technical architecture, see [ARCHITECTURE.md](docs/architecture.md).
938
946
 
939
- **Tech stack:** Node.js 20+ · ES Modules (.mjs) · OpenAI + Anthropic SDKs · Node.js built-in test runner · MCP stdio transport
947
+ **Tech stack:** Node.js 24+ · ES Modules (.mjs) · OpenAI + Anthropic SDKs · Node.js built-in test runner · MCP stdio transport
940
948
 
941
949
  **Design patterns:** Factory (PluginManager.create) · Strategy (orchestrated/legacy execution) · Context (shared state) · Adapter (plugin conclude()) · Guard Clause (requirement gating) · Capability gating (CE/Pro/EE) · Semaphore (concurrency control) · Delta (scan history diff) · Boundary Guard (SSRF/injection protection) · Finding Queue (structured intermediate format) · Parallel Agents (concurrent specialized analysis)
942
950
 
@@ -950,7 +958,7 @@ NSAuditor AI is built on a **Zero Data Exfiltration (ZDE)** architecture:
950
958
  - **No data processing.** Nsasoft US LLC never sees, stores, or processes your scan results.
951
959
  - **AI is opt-in.** External AI calls use your own API keys. Redaction runs locally first, on by default (`OPENAI_REDACT=false` turns it off).
952
960
  - **License validation is offline.** JWT signature verified locally with an embedded public key.
953
- - **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 in the scan rather than a silent clean when the store cannot answer (in Enterprise 1.2.0 that gap reaches no compliance control, so a control only the missing CVE rows would fail can read PASS). 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, the opt-in RFC 3161 timestamping path (`NSAUDITOR_TSA_URL`, no default ever), and the AWS KMS signing path that ships but is not yet wired — 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).
961
+ - **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 in the scan rather than a silent clean when the store cannot answer (from Enterprise 1.3.0 that gap fails, as an evidence gap, the compliance controls the CVE rows map to). 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, the opt-in RFC 3161 timestamping path (`NSAUDITOR_TSA_URL`, no default ever), and the AWS KMS signing path that ships but is not yet wired — 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).
954
962
 
955
963
  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.
956
964
 
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { main } from '../cli.mjs';
3
- main().catch((err) => { console.error(err?.stack || err); process.exit(1); });
2
+ // The command npm installs. It imports main(), so cli.mjs is never the entry point here: a rejection goes to cli.mjs's
3
+ // own exitOnMainRejection, the one copy of the exit-code mapping (a flag refusal exits 2; anything else exits 1).
4
+ import { main, exitOnMainRejection } from '../cli.mjs';
5
+ main().catch((err) => exitOnMainRejection(err));