nsauditor-ai-agent-skill 0.2.55 → 1.1.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.
package/README.md CHANGED
@@ -13,7 +13,11 @@ Works with **Claude Code**, **Claude Desktop**, **Cursor**, **Windsurf**, **VS C
13
13
 
14
14
  ## Current release
15
15
 
16
- **0.2.55** (2026-10-07) — paired with **Enterprise 1.3.0 / Community 0.2.57**. ⚠️ **A floor bump: requires Community >= 0.2.57** (it was >= 0.2.56 — Enterprise 1.3.0 imports names that first ship in Community 0.2.57; below it Enterprise's core does not load while its plugins still do — on Community 0.2.56 as on 0.2.55 — so the scan skips its intelligence, analysis-agent and compliance stages: Community 0.2.56 names that on stderr, and 0.2.55 or older says nothing, the shape measured for Enterprise 1.2.0 and 1.3.0). New in this release's teaching: the not-comparable bucket also catches a CVE-mapper or service-agent row on a TCP port whose service the other run could not identify, and an analysis agent's or the CVE mapper's row when a plugin it reads was left out of a scan; after a CVE lookup that failed, the controls the prior CVE rows failed are now held failed; the exposure and service agents' titles moved, so their rows read `identity-basis-changed` across the upgrade; the DNS-posture audit (060) declines an IP-address target; the TLS certificate audit (040) grades an IP target's DNS-only name mismatch LOW and reads IP SANs; and two measured limits — a scan made before Enterprise 1.3.0 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; and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the 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 can read PASS — include the port scanner (003). See [CHANGELOG.md](https://github.com/nsasoft/nsauditor-ai-agent-skill/blob/main/CHANGELOG.md).
16
+ **1.1.0** (2026-10-26) — paired with **Enterprise 2.1.0 / Community 1.1.0**. ⚠️ **NOT a Community floor bump: requires Community >= 1.0.0, as 1.0.0 did** (Enterprise 2.1.0, like 2.0.0, imports a name that first ships in Community 1.0.0; below it Enterprise's core does not load while its plugins still do — on Community 0.2.57 as on 0.2.56 — so the scan skips its intelligence, analysis-agent and compliance stages: Community 0.2.56 and later name that on stderr when Enterprise was installed on its own or after Community, and 0.2.55 or older says nothing, the shape measured for Enterprise 2.0.0 and 2.1.0; naming an older Community (below 1.0.0) and Enterprise in ONE `npm install -g` command is different: npm nests a second Community, the newest that satisfies the peer floor, under Enterprise; Enterprise's stages then run against that copy's unverified licence and are skipped with no message and no compliance pack (measured for Enterprise 2.1.0 with Community 0.2.55–0.2.57, npm 11.19). Install the paired versions, or Community first). ⚠️ **Install Node.js 24.22.1 or newer before upgrading** (`engines.node >=24.22.1` in Enterprise 2.1.0 and Community 1.1.0; 2.0.0 and 1.0.0 declared `>=24.0.0`): on an older runtime npm prints an `EBADENGINE` warning and installs anyway, while the air-gap `install.sh` refuses. Node.js 24.22.1 or newer is the supported runtime — the October 2026 Node.js security release for the 24 line. The cloud inventories the scan could not read in full — GCP instances, Azure virtual machines, the AWS EC2 feed of the CIS hardened-image census — are evidence gaps, not empty projects or subscriptions, and a listed AWS region the account has not enabled is skipped and recorded as not enabled, not reported as missing trails or as a permission to grant, wherever the account's enabled regions can be read. Through the MCP server (Community 1.1.0), a host scan stops at a total time limit by default and names the plugins it did not run, a response saved with `NSA_MCP_RESPONSE_DIR` keeps its request beside it for `verify-call`, and a CRITICAL, HIGH or evidence-gap title in `scan_cloud` says when its finding holds more than it shows. New in this release's teaching, in `SKILL.md`'s header items (h)–(l): an unread census inventory is an evidence gap with its cause as a class, never an empty project or subscription; a region the account has not enabled is reported as not scanned, never as a permission to grant; an Azure scan with no subscription id and a GCP scan with no project or no loadable credentials read down, not audited; a control loses measurement per scanner, with its `sources`. Plugin count unchanged at 29 Enterprise plugins; all eight coverage matrices unchanged. See [CHANGELOG.md](https://github.com/nsasoft/nsauditor-ai-agent-skill/blob/main/CHANGELOG.md).
17
+
18
+ **Prior: 1.0.0** (2026-10-15) — paired with **Enterprise 2.0.0 / Community 1.0.0**. ⚠️ **A floor bump: requires Community >= 1.0.0** (it was >= 0.2.57 — Enterprise 2.0.0 imports a name that first ships in Community 1.0.0; below it Enterprise's core does not load while its plugins still do — on Community 0.2.57 as on 0.2.56 — so the scan skips its intelligence, analysis-agent and compliance stages: Community 0.2.56 and later name that on stderr, and 0.2.55 or older says nothing, the shape measured for Enterprise 2.0.0), and the supported runtime is Node.js 24. Node.js 24 is the supported runtime. Every one-shot compliance scan writes what changed since the previous scan beside its pack — controls to fail or to pass, controls that lost measurement, violations new or cleared, and what could not be measured, never read as cleared — and can notify a webhook, Slack or Teams when a control goes to fail or loses measurement, when a new violation reaches a severity you set, or when the drift could not be compared. A scan connects only to the addresses its host check resolved (Community 1.0.0), the AI payload carries no AWS account ID, and plugin 040's weak-key and weak-signature findings fail the certificate control in six of the eight frameworks.
19
+
20
+ **Prior: 0.2.55** (2026-10-07) — paired with **Enterprise 1.3.0 / Community 0.2.57**. ⚠️ **A floor bump: requires Community >= 0.2.57** (it was >= 0.2.56 — Enterprise 1.3.0 imports names that first ship in Community 0.2.57; below it Enterprise's core does not load while its plugins still do — on Community 0.2.56 as on 0.2.55 — so the scan skips its intelligence, analysis-agent and compliance stages: Community 0.2.56 names that on stderr, and 0.2.55 or older says nothing, the shape measured for Enterprise 1.2.0 and 1.3.0). New in this release's teaching: the not-comparable bucket also catches a CVE-mapper or service-agent row on a TCP port whose service the other run could not identify, and an analysis agent's or the CVE mapper's row when a plugin it reads was left out of a scan; after a CVE lookup that failed, the controls the prior CVE rows failed are now held failed; the exposure and service agents' titles moved, so their rows read `identity-basis-changed` across the upgrade; the DNS-posture audit (060) declines an IP-address target; the TLS certificate audit (040) grades an IP target's DNS-only name mismatch LOW and reads IP SANs; and two measured limits — a scan made before Enterprise 1.3.0 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; and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the 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 can read PASS — include the port scanner (003). See [CHANGELOG.md](https://github.com/nsasoft/nsauditor-ai-agent-skill/blob/main/CHANGELOG.md).
17
21
 
18
22
  **Prior: 0.2.54** (2026-10-04) — paired with **Enterprise 1.2.0 / Community 0.2.56**. ⚠️ **A floor bump: requires Community >= 0.2.56** (Enterprise 1.2.0 imports names that first ship in Community 0.2.56). Its teaching — the not-comparable cases, the compliance hold on a prior finding, how a failed Enterprise load surfaces — is carried forward above; of its two measured limits, 0.2.55 teaches the first closed and the second narrowed.
19
23
 
@@ -170,7 +174,7 @@ When an AI agent loads this skill, it gains:
170
174
  | **Schema knowledge** | Complete data structures for parsing and presenting results |
171
175
  | **CPE construction** | How to map detected services to NVD vulnerability lookups |
172
176
  | **Plugin awareness** | 56 scanner plugins (27 Community + 29 Enterprise) with protocols, ports, capabilities, and multi-framework (SOC 2 · HIPAA §164.312 · NIST CSF 2.0 · PCI DSS v4.0.1 · ISO/IEC 27001:2022 · CIS Controls v8 · GDPR Art. 32 · NIST SP 800-171 Rev 2) substrate-evidence dimensions |
173
- | **Compliance frameworks** | **Eight frameworks, one scan** — SOC 2 (AICPA TSC 2017) · HIPAA Security Rule §164.312 Technical Safeguards (HHS Required/Addressable discipline per control) · NIST CSF 2.0 (Subcategory-level) · PCI DSS v4.0.1 (QSA RoC sub-requirement-level) · ISO/IEC 27001:2022 (per-Annex-A-code, SoA discipline) · CIS Controls v8 (per-Safeguard; Implementation Group IG1/IG2/IG3 cumulative discipline) · GDPR Article 32 (sub-measure-level; **Art. 32 infrastructure substrate only, NOT GDPR compliance**) · NIST SP 800-171 Rev 2 (requirement-level for the matrix, SP 800-171A determination-statement level for evidence; **examine-method evidence substrate for CMMC Level 2 preparation — never a CMMC certification and never a MET/NOT MET verdict**). All via `--compliance all`, or any CSV subset via `--compliance soc2,hipaa,nist-csf,pci-dss,iso-27001,cis-v8,gdpr,nist-800-171`. Zero BAA required for HIPAA — ePHI never leaves customer infrastructure. |
177
+ | **Compliance frameworks** | **Eight frameworks, one scan** — SOC 2 (AICPA TSC 2017) · HIPAA Security Rule §164.312 Technical Safeguards (HHS Required/Addressable discipline per control) · NIST CSF 2.0 (Subcategory-level) · PCI DSS v4.0.1 (QSA RoC sub-requirement-level) · ISO/IEC 27001:2022 (per-Annex-A-code, SoA discipline) · CIS Controls v8 (per-Safeguard; Implementation Group IG1/IG2/IG3 cumulative discipline) · GDPR Article 32 (sub-measure-level; **Art. 32 infrastructure substrate only, NOT GDPR compliance**) · NIST SP 800-171 Rev 2 (requirement-level for the matrix, SP 800-171A determination-statement level for evidence; **examine-method evidence substrate for CMMC Level 2 preparation — never a CMMC certification and never a MET/NOT MET verdict**). All via `--compliance all`, or any CSV subset via `--compliance soc2,hipaa,nist-csf,pci-dss,iso-27001,cis-v8,gdpr,nist-800-171`. No BAA with Nsasoft required for HIPAA — no scan data is ever sent to Nsasoft. |
174
178
  | **Security rules** | ZDE, SSRF protection, redaction, scan authorization requirements |
175
179
  | **Error handling** | License gates, SSRF blocks, timeout resolution, CPE format errors |
176
180
  | **Decision routing** | When to use scan_host vs probe_service vs CLI vs get_vulnerabilities |
@@ -201,7 +205,7 @@ This package provides **knowledge about** NSAuditor AI. To actually **run** scan
201
205
  }
202
206
  }
203
207
  ```
204
- `NSA_ALLOW_ALL_HOSTS: "1"` lets Claude scan your own network — private addresses such as `192.168.x.x` or `10.x.x.x`. It admits private ranges only (RFC 1918, CGNAT `100.64/10`, `fc00::/7`): the server still refuses loopback, link-local and cloud-metadata addresses in any spelling, whether written as the host or returned for a host name — every answer is checked. Only `1`, `true`, `yes` or `on` turn it on. The check does not pin the answer, so a name whose answer changes between the check and the scan (DNS rebinding) is not caught. `install-key` does not print it, so add it yourself, and remove it if you only scan public hosts.
208
+ `NSA_ALLOW_ALL_HOSTS: "1"` lets Claude scan your own network — private addresses such as `192.168.x.x` or `10.x.x.x`. It admits private ranges only (RFC 1918, CGNAT `100.64/10`, `fc00::/7`): the server still refuses loopback, link-local and cloud-metadata addresses in any spelling, whether written as the host or returned for a host name — every answer is checked. Only `1`, `true`, `yes` or `on` turn it on. From Community Edition 1.0.0 the scan connects to the answers the check resolved, so a name whose answer changes between the check and the scan (DNS rebinding) is not followed — except through an HTTP proxy set with `NODE_USE_ENV_PROXY`, which resolves the name itself, or over an idle connection opened earlier to the same name, for as long as it stays idle in the pool (a few seconds by default; a server's Keep-Alive hint holds it longer); an older Community Edition does not pin the answer. 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 addresses no host check resolved. `install-key` does not print it, so add it yourself, and remove it if you only scan public hosts.
205
209
 
206
210
  > ⚠️ **`NSA_MCP_AUTH_KEY` is REQUIRED — the server refuses to start without it.** Generate one with
207
211
  > `nsauditor-ai mcp install-key`, then put the SAME value in the `env` block above. Without it the MCP
package/SKILL.md CHANGED
@@ -15,7 +15,11 @@ description: >
15
15
 
16
16
  # NSAuditor AI — Agent Skill
17
17
 
18
- > **Version:** <!-- nsa:derived id="skill-version" -->0.2.55<!-- /nsa:derived --> (knowledge current as of **EE <!-- nsa:derived id="ee-version" -->1.3.0<!-- /nsa:derived -->** · **requires CE ≥ <!-- nsa:derived id="ce-floor" -->0.2.57<!-- /nsa:derived -->**) — **1.3.0: A finding on a port, region or producer a scan did not measure is not counted as fixed, and with SLA tracking on the control it failed is held FAILED — including the prior CVE rows on a service whose lookup failed, the CVE mapper's and the service agent's rows on a TCP port whose service the scan could not identify, and an analysis agent's or the CVE mapper's rows when a plugin they read was left out of the scan or did not complete. Two measured limits: a scan made before EE 1.3.0 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 — the report's Basis cell says so on the row; and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the 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 can read PASS — include the port scanner (003).** Teach these first, and never report a row in either limit as fixed, or its control as passing. (a) **The NOT-COMPARABLE bucket catches four more cases.** A UDP-transport finding whose port did not answer in the other run (for a CVE row, also one that answered without identifying its service) is `port-not-measured`, listed with a `/udp` suffix; a CVE row whose LOOKUP failed in the other run is `evidence-gap` — the mapper's own `[COVERAGE GAP] <gapClass> — <protocol>/<service>` record is a coverage gap that opened or cleared, never an exposure; and a finding of an analysis agent that DID NOT RUN (`[COVERAGE GAP] AGENT NOT RUN — <agent> did not run: <cause class>`) is `evidence-gap`, while that agent's controls fail closed in every framework; and a CVE row that VANISHED — present in the baseline, absent now — while the SAME program and version answered on its port in both runs is `vulnerability-data-changed` — the mapper attributes on program and version alone, so the vulnerability data moved (NVD's answer, a cached answer's age, an offline store), not the estate: never present it as a fix, and in MTTR it is not a closed finding. Neither the delta's rule nor MTTR's reaches a port that answers with two different identified programs or versions, and MTTR's does not reach a prior row that recorded no program or version. A CVE that APPEARED on an unchanged service stays NEW with the basis note "the service is unchanged, the vulnerability data is not" — new knowledge about an old service, not a new exposure on the estate. Every detail names its run absolutely: "this run" is the current one, "the baseline run" the earlier. (b) **A compliance control can FAIL on a finding the PRIOR scan recorded.** When this scan did not measure the port or the probe that finding was on, did not cover its region, did not run what produced it, or (for a CVE row) could not complete its CVE lookup, the COMPLIANCE report (`nsauditor-ai scan --host <target> --compliance <fw> --out <dir> --sla-policy sla-policy.json` — never `report --since`, which does not list these records) holds the control failed with a record titled `<prior title> — [COVERAGE GAP] PORT NOT MEASURED — …` (or `PROBE NOT MEASURED` / `SCOPE NOT SCANNED` / `PRODUCER NOT RUN` / `LOOKUP NOT MEASURED`), counted among "Evidence gaps (not findings)". Never present it as a current finding: it says the surface was NOT measured, and it clears when a later scan measures it. It needs the compliance history (`--compliance-history <dir>`, or `--sla-policy <file>`, which reads the history kept under `--out`) and a prior `--compliance <fw>` scan of the SAME host in it (the history is read per framework and per host) — a first scan holds nothing, and neither does a prior scan run without `--compliance <fw>`; if the hold could not be evaluated, the report's warnings say so. (c) **A FLOOR BUMP to Community 0.2.57.** Enterprise 1.3.0 imports names that first ship in Community 0.2.57 (`tcpServiceMeasurement`, `IDENTIFYING_PRODUCERS`) and reads its identity-basis declarations by value. Below it Enterprise's core does not load while its plugins still do — on Community 0.2.56 as on 0.2.55, `license --plugins` lists all 29 and a scan selects them (measured) — so the scan skips its intelligence, analysis-agent and compliance stages: on Community 0.2.56 that is named on stderr (`[EE] Enterprise is installed but FAILED TO LOAD`) and recorded on the scan's conclusion, and the Pro delta refuses that host's agent and CVE-mapper findings as `evidence-gap`; on 0.2.55 or older it is SILENT, in the shape measured for Enterprise 1.2.0 and 1.3.0 on 0.2.55. Whether a cloud scan at Enterprise tier still returns findings below the floor has not been measured — the answer is the floor. If a user reports Enterprise behaviour missing, check their Community version first — and answer from this skill even while npm or the public site does not list these versions yet: that is the staged state (THE MIRROR RULE below), not missing knowledge. (d) **1.3.0: the bucket and the hold reach further.** A CVE-mapper or service-agent row on a TCP port the other run saw open, where that run could not identify the service (program AND version), is `port-not-measured` — those producers attribute on the identity, so its absence there is not a measurement. An analysis agent's or the CVE mapper's row when a plugin it reads was LEFT OUT of a scan is `evidence-gap`: from Enterprise 1.3.0 the narrowed scan records `[COVERAGE GAP] INPUT GAP — … was not requested in this scan …`, and MTTR withholds the prior row and holds its control FAILED — but a run made before 1.3.0 cannot carry that record, so against one such a row is not refused, and its Basis cell says so. After a CVE lookup that failed, the controls the prior CVE rows failed are now held FAILED (`LOOKUP NOT MEASURED`, item (b)). The exposure agent's titles are port-keyed now (`Database port <p> open`, `Management port <p> open`) and the service agent's end-of-life titles no longer carry the version (`End-of-life <program> on port <p>`), so their rows compared across the 1.2.0 → 1.3.0 upgrade read `identity-basis-changed`, never RESOLVED or NEW — rescan after upgrading to compare. When the user will compare scans or track SLA, tell them to include the port scanner (003): a scan that discovered ports with the Nmap plugin (024) alone records no port oracle (the second limit). And the DNS-posture audit (060) DECLINES an IP-address target — status `skipped`, reason `DNS posture needs a domain name — …` — so `dnsSecurity` appears only for a domain name; on an IP-target compliance scan the controls its findings route to (SOC 2 CC6.1, CIS v8 9.5) are held as an evidence gap, with a warning that it could not run, never PASS. The TLS certificate audit (040) grades a name mismatch by the target's form: scanned by ADDRESS, a certificate that names DNS names only reads LOW, its detail ending `the certificate names DNS names only (…); a client connecting by address sees a name mismatch, expected where the service is reached by name`, so it no longer trips `--fail-on high` on its own; a DNS-name target the certificate does not name, a certificate naming a DIFFERENT address and one with no subjectAltName stay HIGH. A certificate with NO subjectAltName — many consumer routers serve one, naming the device only in its CN — says so: `the certificate carries no subjectAltName — modern clients reject it for any name (CN … is not checked)`, because a modern client checks no name in it. 040 also reads IP SANs now (Node prints them `IP Address:`), so a certificate naming the scanned address raises no mismatch, and a name mismatch on a chain the CA store verified no longer adds `ca_not_trusted`. A self-signed certificate still raises `self_signed` HIGH, so a router scanned by address can still fail `--fail-on high`. 040's key-size and signature checks fire on real servers from Community 0.2.57 (they had read fields Node never sets): an RSA key under 2048 bits is `weak_rsa_key` HIGH (CRITICAL under 1024), an EC key under 256 bits `weak_ec_key` HIGH, a CA-issued certificate signed with SHA-1 or MD5 `weak_signature` HIGH, and an issued intermediate signed that way `chain_weak_signature` MEDIUM. A self-signed certificate's own signature is not graded — no client verifies it — and the `self_signed` finding names the algorithm instead. A key of neither type (Ed25519), or a runtime that cannot read signature algorithms (Node 20; Node 24 reads them), records `not assessed` on `certAudit`, never a pass. So a router serving a 1024-bit key now also shows `weak_rsa_key` HIGH. These grades are recorded, not yet routed to a compliance control. The teaching that follows is 0.2.53's, carried forward and still current. — the delta release: the product can now answer *what changed since the last scan*. Teach five things: (1) **`nsauditor-ai report --from <dir> --format executive --since <runId|prior>`** (`--format` is REQUIRED; `report` refuses without it) (Pro/Enterprise) compares two runs and reports new / resolved / severity-changed findings. (2) **NEVER present a `resolved` count as remediation on its own** — the report carries a NOT-COMPARABLE bucket with a reason per finding (host not scanned · the producing plugin did not run, ERRORED or TIMED OUT on that host, or cannot be identified · an evidence gap such as AccessDenied · **a PORT that was not measured on that host — a check that could not complete its connection, or a port that STOPPED ANSWERING between the two runs (a port that went quiet is not a fix)** · **the two runs COVERED DIFFERENT SCOPES — a narrower `--aws-region`, a different Azure subscription or GCP project — so that surface was not looked at, which is NOT the same as clean** · framework enumeration moved · **the PRODUCER CHANGED WHAT IT NAMES between the two releases**), and a finding that vanished for any of those reasons was not fixed. ⚠️ **THE LAST ONE ARRIVED IN EE 1.1.0 AND IT FIRES ON AN UPGRADE ACROSS 1.1.0.** FOURTEEN producers changed what identifies a finding at EE 1.1.0 — thirteen plugins that had left the object empty, named the REGION, recorded no region the report reads, carried a HIGH sentence that had to be corrected (1110, the effective-decrypt auditor), or (1023, the Zero Trust checker) now name the port a finding is about and keep the open-port count out of its title, and one analysis AGENT whose findings are keyed on their TITLE because they carry no object field at all. A comparison that straddles that upgrade reports those rows NOT-COMPARABLE instead of differencing them, so one surface under two keys is never read as a fix plus a fresh exposure. **So the first `report --since` a user runs across the 1.1.0 upgrade will legitimately show a LARGE not-comparable bucket — that is the declaration working, not a broken report.** Derive the reason list from `NOT_COMPARABLE_REASONS` in Community's `utils/scan_delta.mjs` rather than from this sentence; it is held in equality with what the engine emits, and this sentence is prose that has disagreed with it before. If a user asks what got fixed, read the NOT-COMPARABLE list before answering. **Two conditions refuse the whole comparison rather than bucketing findings**: the two runs straddle a product boundary where a reported number changed meaning, and the two runs ran at DIFFERENT LICENCE TIERS — the set of producers that ran differs, so a whole producer's findings would read as remediation. ⚠️ **AND ONE OF THOSE REASONS IS DECLARED RATHER THAN CHECKED: framework-enumeration movement is NOT EVALUATED in this edition**, because a run record carries no framework enumeration. The report says so as a limit on every comparison and each row's basis reads `framework enumeration: not evaluated`. Never tell a user that framework movement was ruled out. (3) **Integrity is a hash chain, not a signature — and say WHAT is sealed.** At finalize the run record AND each written host's findings files are digested over their exact persisted bytes, with the per-file digests sealed inside the record, which is itself digested and chained through `prevDigest`. So an altered **findings file** — which is what the comparison actually reads — is detected and NAMED, not merely an altered index; a file that did not exist at seal time is sealed as an explicit absence, so adding one is detected too. **BOTH runs are verified, not only the baseline.** An altered or unmeasurable run on either side REFUSES the comparison outright, and the refusal names which side. A file that cannot be READ is reported as unmeasurable rather than as tampering, and a record written before per-host sealing keeps its verdict while its reason says the findings files were not covered. The guarantee is tamper-EVIDENT against corruption and unsophisticated edits, **NOT tamper-proof against host-level access and NOT non-repudiation**. Do not describe it as file-integrity monitoring or a change-detection mechanism: PCI DSS `11.5.1` / `11.5.2` / `11.6.1` are enumerated OUT OF SCOPE in the shipped matrix. The code ships in Community; the capability is licence-gated to Pro/Enterprise. ⚠️ **`--since` REQUIRES COMMUNITY 0.2.55 OR LATER — AND ENTERPRISE 1.3.0 REQUIRES 0.2.57 (item (c) above).** The peer floor moved to 0.2.55 at EE 1.1.0 because **Enterprise CALLS the new Community code** — the per-plugin run statuses the Zero Trust checker reads before it will assert a posture, and the run-record fields `report --since` compares. The earlier floor said otherwise on the grounds that Enterprise did not call it; that stopped being true at EE 1.1.0. If a user reports the flag is unrecognised, check their Community version first. (4) **A `kms:Decrypt on Resource:*` HIGH from 1110 is NEVER downgraded on a live run.** 1110 reads KMS key policies and grants in the configured region only, and one region cannot show that NO key trusts a principal — a key in another region may — so the HIGH→INFO downgrade it used to apply is withheld on every shipped path. The reason is in that plugin's run summary as `summary.kmsDowngrade` (`reason` `single-region-enumeration` for a complete read; `access-denied` · `per-key-error` · `keys-truncated` · `grants-truncated` for an incomplete one, which also emits an evidence-gap row). Never tell a user a surviving HIGH was checked against every key, and never read a missing downgrade as a scanner fault. Its grant finding (`kms-grant-decrypt-no-identity-grant`, MEDIUM) likewise covers keys in the configured region only. (5) **The NVD store and response cache MOVED at EE 1.1.0 and every run records where they are.** They used to be a `.nvd-cache` folder in whatever directory the scan started from; they now resolve, from any directory, to `NVD_CACHE_DIR`, else `NSAUDITOR_NVD_CACHE_DIR`, else `~/.nsauditor/nvd_cache`. Only `feed import` (of a feed you downloaded yourself) takes `--cache-dir`; a scan does not read that flag, so it reads a store imported with `--cache-dir <d>` only when that chain resolves to `<d>` (e.g. `NVD_CACHE_DIR=<d>`). The MCP `get_vulnerabilities` cache reads `NSAUDITOR_NVD_CACHE_DIR`, never `NVD_CACHE_DIR`. An old `.nvd-cache` folder is NOT read, and the scan says so. If a user reports that offline CVE matching stopped after upgrading, that is the cause: move the folder once or set `NVD_CACHE_DIR` to it. Never read the resulting coverage gaps as a clean host. Plugin count UNCHANGED at <!-- nsa:derived id="plugins:ee" -->29<!-- /nsa:derived -->; <!-- nsa:derived id="matrix-movement" -->all eight coverage matrices unchanged since EE 1.2.0<!-- /nsa:derived -->; Community <!-- nsa:derived id="ce-version" -->0.2.57<!-- /nsa:derived --> (published with this skill).
18
+ > **Version:** <!-- nsa:derived id="skill-version" -->1.1.0<!-- /nsa:derived --> (knowledge current as of **EE <!-- nsa:derived id="ee-version" -->2.1.0<!-- /nsa:derived -->** · **requires CE ≥ <!-- nsa:derived id="ce-floor" -->1.0.0<!-- /nsa:derived -->**) — **2.1.0: Node.js 24.22.1 or newer is the supported runtime — the October 2026 Node.js security release for the 24 line. The cloud inventories the scan could not read in full — GCP instances, Azure virtual machines, the AWS EC2 feed of the CIS hardened-image census — are evidence gaps, not empty projects or subscriptions, and a listed AWS region the account has not enabled is skipped and recorded as not enabled, not reported as missing trails or as a permission to grant, wherever the account's enabled regions can be read. Through the MCP server (Community 1.1.0), a host scan stops at a total time limit by default and names the plugins it did not run, a response saved with `NSA_MCP_RESPONSE_DIR` keeps its request beside it for `verify-call`, and a CRITICAL, HIGH or evidence-gap title in `scan_cloud` says when its finding holds more than it shows.** Teach these first: never read a cloud inventory the scan could not read in full as an empty project, subscription or region; never tell a user to grant the audit role access to a region the account has not enabled; never present a violation the drift file lists as could-not-measure as cleared; and never tell a user that a webhook URL a logged `--watch` run printed is safe. Items (h)–(l) are 2.1.0's; (a)–(g), carried forward from 2.0.0 and 1.3.0, still hold. (h) **A cloud inventory the scan could not read in full is an evidence gap (Enterprise 2.1.0).** The CIS hardened-image census (CIS v8 Safeguards 4.1, 4.2 and 4.6) read as complete when its feed came up short: a GCP instance list, an Azure VM list or an AWS EC2 region that failed, was denied or was cut off read exactly as a project, subscription or region with no machines. Each producer now records one constant evidence gap that fails those three Safeguards — `gcp-compute-inventory-unreadable-evidence-gap` (1021, beside `gcp-compute-inventory-evidence-gap` for zones the API named unreachable), `azure-vm-image-inventory-evidence-gap` (1022), `aws-ec2-image-inventory-evidence-gap` (1210) — with the cause as a class in `details.censusLoss` (`unreachable-zones`, `page-lost`, `sdk-not-loaded`, `image-metadata-unread`, `service-off`, `scope-unresolved`, `deadline`), never error text. The machines read before a failure are kept, and one whose image could not be read is counted apart (`imageUnread`), never as an image that did not match. The CIS report's `cisHardenedImage.partial` names each short cloud and its cause, its per-cloud figures name every census cloud ("not scanned this run" where its scanner did not run), and a failing Safeguard row reads "observed (census partial)" or "observed (control failing)", never ✅. No other framework holds a control over the census, so each other framework's pack lists the gap among the gaps routed to no control. Over MCP, `scan_cloud` lists these gaps in its evidence-gap section without the cause, which is in the compliance pack only. The census reads machine images only: no plugin inventories containers, so never offer a container or Docker Hub image as CIS hardened-image evidence. Stated limit: while such a gap stands, `report --since` sets aside that plugin's rows, so a new CRITICAL from the same plugin does not read as new in that comparison. (i) **An AWS region the account has not enabled (Enterprise 2.1.0).** An `all`-regions scan (`--aws-region all`, or `regions: ["all"]` over MCP) walks the regions the account has enabled, for every AWS plugin; it used to send fifteen of the nineteen plugins that use the lookup to the partition's whole list. A LISTED region the account has not enabled is not called: the shared region walker emits one LOW row for it, "Region '<region>' could not be scanned — the region is not enabled for this account (ec2:DescribeRegions). …", an evidence gap, never a clean result (so a plugin's several rows for that region become one, which is not a regression), S3's access-point walk says "ListAccessPoints was not attempted in <region>: the region is not enabled for this account …", and a previous run's rows there read not comparable, never fixed; CloudTrail (1040) records it in `details.regionsNotEnabled` and raises its zero-trails CRITICAL only when it read at least one region; the EC2 auditor (1210) puts it in the census gap as `service-off`. Tell the user to enable the region or drop it from the list — no grant can satisfy it, so never tell them to grant the audit role access there. A single-region or listed scan also names the enabled regions it did not scan (`regionsKnownButNotScanned`). Stated limit: when `ec2:DescribeRegions` is denied the enabled set is unknown, so a listed region is scanned and read as before and an `all` scan walks the partition's list (`resolveError` names the cause); a region named by `AWS_REGION` rather than listed is not covered by this change. (j) **A cloud with nothing to address is reported down (Enterprise 2.1.0).** With `AZURE_SUBSCRIPTION_ID` unset or blank the four Azure plugins (1022, 1220, 1221, 1222), with no GCP project id (`GCP_PROJECT_ID`, `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID`) or no Google credentials that can be loaded the GCP security audit (1021), now return `up: false` with the cause ("… no API call was made"): the provider does not read audited and its controls fail closed as not measured. The fix is to set the variable or the credentials, never a role grant. Before 2.1.0 a GCP scan with no project read audited, and one with no credentials made the CLI exit with status 1. (k) **The MCP server (Community 1.1.0).** `scan_host` stops at `SCAN_HOST_WALL_MS` (default 50000; `0` = off) and names the plugins it did not run (`callWall`, the `scan_host` section); with `NSA_MCP_RESPONSE_DIR` set the server saves each response with its request beside it (`<call_id>.request.json`), and the operator checks both with `verify-call --response … --request …` (the Receipts note); a `scan_cloud` CRITICAL, HIGH or evidence-gap title ending `(+N more via get_findings)` is one of that finding's N+1 entries, not the whole finding — drill with `get_findings` by that line's `plugin` and `severity`, and read the entries, which include evidence statements as well as defects, before counting; and a `get_findings` category filter that matches nothing returns `categoriesInScan` (both in the `scan_cloud` notes below). (l) **A scanner that did not run, and lost measurement (Enterprise 2.1.0).** A control loses measurement per SCANNER: a control that already carried one gap now loses measurement again when another scanner's gap arrives, and each `controls.lostMeasurement` entry, in the drift file and the notification, names its `sources` (the Slack and Teams line names them). `--notify-severity` governs new violations only: a control that went to fail or lost measurement always notifies, and a new evidence gap is listed but is not a trigger of its own. Stated limit: the judgement is per scanner, not per scope, so a scanner whose gap on a control WIDENS does not lose measurement there again. When a scanner reports itself down, every control it evidences still carries an evidence-gap row, whose "Why this violates" text is now one sentence: the scanner that evidences this control did not run. A Vanta push's description now says which rows are evidence gaps; the outcome it sends is unchanged (failed), and Drata and Secureframe pushes carry no such line yet. (a) **The drift file and its notification (Enterprise 2.0.0).** Every one-shot compliance scan writes `scan_drift_<fw>.json` beside its pack (never inside `--watch`): controls to fail or to pass, controls that lost measurement, violations new or cleared. "Previous" is the run that started immediately before in the same `--out` (the run `report --since prior` names), else the newest prior pack for the host in `--compliance-history`. A violation is cleared only when this scan looked for it and it was gone; one whose scanner did not run or reported itself down, or whose port or region was not measured, is could-not-measure, with its reason. A TLS check whose handshake failed in transport on a port the port scanner saw open is could-not-measure (Community 1.0.0 records the error class), except when another port of the same scan completed a TLS handshake: then the earlier TLS finding on the failed port can still read as cleared. The notification is opt-in — `--notify-webhook <url>` (public `https` only), `--notify-format generic|slack|teams`, `--notify-severity <sev>` (default `high`), `--notify-payload minimal|full` (default `minimal`) — and is sent when a control goes to fail or loses measurement, whatever the threshold, or a new violation is at or above `--notify-severity`; a drift that could not be compared always notifies. A timeout or a 5xx is retried twice, so a receiver that took a request and then failed can get it up to three times, and a hanging endpoint costs up to about 32 s per framework. The `--watch` alert is separate: `--alert-payload minimal|full`, default `full`, and its banner now prints the webhook host only — if a user ran `--watch` with a Slack or Teams URL where stdout is logged, tell them to rotate that URL. (b) **The resolved-address pin (Community 1.0.0).** A scan connects only to the addresses its host check resolved — CLI and MCP, the target and each redirect hop — except through an HTTP proxy set with `NODE_USE_ENV_PROXY` (which resolves the name itself) or over an idle connection opened earlier to the same name, for as long as it stays idle in the pool (a few seconds by default; a server's Keep-Alive hint holds it longer); an older Community does not pin. 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 addresses no host check resolved. (c) **A FLOOR BUMP to Node.js 24.22.1; the Community floor stays 1.0.0.** Enterprise 2.1.0 and Community 1.1.0 declare `engines.node >=24.22.1` (2.0.0 and 1.0.0 declared `>=24.0.0`). On an older runtime npm prints an `EBADENGINE` warning and installs anyway — it does not refuse — while the air-gap `install.sh` refuses before installing anything, comparing the whole version (a host on 24.12 is refused). Below 24.22.1 a scanned host that answers with a crafted Brotli body reaches the decoder CVE-2026-111497 names, because Community's web-application detector, its UPnP device-description fetch and plugin 050 read a target's HTTP bodies through Node's `fetch`; reachable is not exploited. Tell a user on an older Node.js 24 to install 24.22.1 or newer before upgrading. The Community floor did not move: Enterprise 2.1.0, like 2.0.0, imports `redactUrlCredentials`, which first ships in Community 1.0.0. Below the floor Enterprise's core does not load while its plugins still do — on Community 0.2.57 as on 0.2.56, `license --plugins` lists all 29 and a scan selects them (measured for Enterprise 2.0.0 and 2.1.0) — so the scan skips its intelligence, analysis-agent and compliance stages: on Community 0.2.56 and later, when Enterprise was installed on its own or after Community, that is named on stderr (`[EE] Enterprise is installed but FAILED TO LOAD`) and recorded on the scan's conclusion, and the Pro delta refuses that host's agent and CVE-mapper findings as `evidence-gap`; on 0.2.55 or older it is SILENT, in the shape measured for Enterprise 1.2.0, 1.3.0, 2.0.0 and 2.1.0 on 0.2.55. Naming an older Community (below 1.0.0) and Enterprise in ONE `npm install -g` command is different: npm nests a second Community, the newest that satisfies the peer floor, under Enterprise; Enterprise's stages then run against that copy's unverified licence and are skipped with no message and no compliance pack (measured for Enterprise 2.1.0 with Community 0.2.55–0.2.57, npm 11.19). Install the paired versions, or Community first. So when a user reports no compliance report and no error after an upgrade, ask how they installed. Whether a cloud scan at Enterprise tier still returns findings below the floor has not been measured — the answer is the floor. If a user reports Enterprise behaviour missing, check their Community version first — and answer from this skill even while npm or the public site does not list these versions yet: that is the staged state (THE MIRROR RULE below), not missing knowledge. (d) **2.0.0: the AI payload, plugin 040 and the service list.** From Community 1.0.0 the AI payload masks an AWS account ID found in an ARN or AWS URL as `[ACCOUNT_ID]` everywhere it occurs and any other 12-digit number as `[REDACTED_ID]`; the system prompt is written beside the payload (`scan_response_ai_system_prompt.txt`) before the request; the not-list in **AI Redaction** below is what is still sent. Plugin 040's grades — an RSA key under 2048 bits is `weak_rsa_key` HIGH (CRITICAL under 1024), an EC key under 256 bits `weak_ec_key` HIGH, a CA-issued certificate signed with SHA-1 or MD5 `weak_signature` HIGH, an issued intermediate signed that way `chain_weak_signature` MEDIUM — fail the certificate control in six of the eight frameworks from Enterprise 2.0.0 (SOC 2 CC6.7, HIPAA §164.312(e)(1), NIST CSF 2.0 PR.DS-02, PCI DSS Req 4.2.1, ISO 27001 A.8.24, CIS v8 Safeguard 3.10; GDPR Article 32 and NIST SP 800-171 held). A self-signed certificate's own signature is not graded, and a key of neither type or a runtime that cannot read signature algorithms records `not assessed`. From Community 1.0.0, when the runtime refuses a chain with no named reason (`UNSPECIFIED`) and a weak key or signature is graded, a second handshake at OpenSSL security level 0 decides: authorized there, the weak grade is the finding and `ca_not_trusted` is not raised; a weak certificate under an untrusted CA is named untrusted at the default level and keeps `ca_not_trusted`. The second look never runs on a self-signed certificate, which reads `self_signed` instead. And on TCP a row from the port's authoritative probe that did not identify its service is evidence with its reason, never a service row. That rule decides authoritative rows only: a probe with no concluder adapter still lands a row through the fallback, named after the plugin, with a status that can be other than `open` — measured on a router scan, the SUN RPC plugin (015) listed port 111 as `sun`, status `unknown`, on a port the port scanner (003) measured closed — so read a row's status before calling it a service. UDP non-answers stay in the service list. **1.3.0, still current: A finding on a port, region or producer a scan did not measure is not counted as fixed, and with SLA tracking on the control it failed is held FAILED — including the prior CVE rows on a service whose lookup failed, the CVE mapper's and the service agent's rows on a TCP port whose service the scan could not identify, and an analysis agent's or the CVE mapper's rows when a plugin they read was left out of the scan or did not complete. Two measured limits: a scan made before EE 1.3.0 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 — the report's Basis cell says so on the row; and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the 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 can read PASS — include the port scanner (003).** Never report a row in either of its two limits as fixed, or its control as passing. (e) **The NOT-COMPARABLE bucket catches four more cases.** A UDP-transport finding whose port did not answer in the other run (for a CVE row, also one that answered without identifying its service) is `port-not-measured`, listed with a `/udp` suffix; a CVE row whose LOOKUP failed in the other run is `evidence-gap` — the mapper's own `[COVERAGE GAP] <gapClass> — <protocol>/<service>` record is a coverage gap that opened or cleared, never an exposure; and a finding of an analysis agent that DID NOT RUN (`[COVERAGE GAP] AGENT NOT RUN — <agent> did not run: <cause class>`) is `evidence-gap`, while that agent's controls fail closed in every framework; and a CVE row that VANISHED — present in the baseline, absent now — while the SAME program and version answered on its port in both runs is `vulnerability-data-changed` — the mapper attributes on program and version alone, so the vulnerability data moved (NVD's answer, a cached answer's age, an offline store), not the estate: never present it as a fix, and in MTTR it is not a closed finding. Neither the delta's rule nor MTTR's reaches a port that answers with two different identified programs or versions, and MTTR's does not reach a prior row that recorded no program or version. A CVE that APPEARED on an unchanged service stays NEW with the basis note "the service is unchanged, the vulnerability data is not" — new knowledge about an old service, not a new exposure on the estate. Every detail names its run absolutely: "this run" is the current one, "the baseline run" the earlier. (f) **A compliance control can FAIL on a finding the PRIOR scan recorded.** When this scan did not measure the port or the probe that finding was on, did not cover its region, did not run what produced it, or (for a CVE row) could not complete its CVE lookup, the COMPLIANCE report (`nsauditor-ai scan --host <target> --compliance <fw> --out <dir> --sla-policy sla-policy.json` — never `report --since`, which does not list these records) holds the control failed with a record titled `<prior title> — [COVERAGE GAP] PORT NOT MEASURED — …` (or `PROBE NOT MEASURED` / `SCOPE NOT SCANNED` / `PRODUCER NOT RUN` / `LOOKUP NOT MEASURED`), counted among "Evidence gaps (not findings)". Never present it as a current finding: it says the surface was NOT measured, and it clears when a later scan measures it. It needs the compliance history (`--compliance-history <dir>`, or `--sla-policy <file>`, which reads the history kept under `--out`) and a prior `--compliance <fw>` scan of the SAME host in it (the history is read per framework and per host) — a first scan holds nothing, and neither does a prior scan run without `--compliance <fw>`; if the hold could not be evaluated, the report's warnings say so. (g) **1.3.0: the bucket and the hold reach further.** A CVE-mapper or service-agent row on a TCP port the other run saw open, where that run could not identify the service (program AND version), is `port-not-measured` — those producers attribute on the identity, so its absence there is not a measurement. An analysis agent's or the CVE mapper's row when a plugin it reads was LEFT OUT of a scan is `evidence-gap`: from Enterprise 1.3.0 the narrowed scan records `[COVERAGE GAP] INPUT GAP — … was not requested in this scan …`, and MTTR withholds the prior row and holds its control FAILED — but a run made before 1.3.0 cannot carry that record, so against one such a row is not refused, and its Basis cell says so. After a CVE lookup that failed, the controls the prior CVE rows failed are now held FAILED (`LOOKUP NOT MEASURED`, item (f)). The exposure agent's titles are port-keyed now (`Database port <p> open`, `Management port <p> open`) and the service agent's end-of-life titles no longer carry the version (`End-of-life <program> on port <p>`), so their rows compared across the 1.2.0 → 1.3.0 upgrade read `identity-basis-changed`, never RESOLVED or NEW — rescan after upgrading to compare. When the user will compare scans or track SLA, tell them to include the port scanner (003): a scan that discovered ports with the Nmap plugin (024) alone records no port oracle (the second limit). And the DNS-posture audit (060) DECLINES an IP-address target — status `skipped`, reason `DNS posture needs a domain name — …` — so `dnsSecurity` appears only for a domain name; on an IP-target compliance scan the controls its findings route to (SOC 2 CC6.1, CIS v8 9.5) are held as an evidence gap, with a warning that it could not run, never PASS. The TLS certificate audit (040) grades a name mismatch by the target's form: scanned by ADDRESS, a certificate that names DNS names only reads LOW, its detail ending `the certificate names DNS names only (…); a client connecting by address sees a name mismatch, expected where the service is reached by name`, so it no longer trips `--fail-on high` on its own; a DNS-name target the certificate does not name, a certificate naming a DIFFERENT address and one with no subjectAltName stay HIGH. A certificate with NO subjectAltName — many consumer routers serve one, naming the device only in its CN — says so: `the certificate carries no subjectAltName — modern clients reject it for any name (CN … is not checked)`, because a modern client checks no name in it. 040 also reads IP SANs now (Node prints them `IP Address:`), so a certificate naming the scanned address raises no mismatch, and a name mismatch on a chain the CA store verified no longer adds `ca_not_trusted`. A self-signed certificate still raises `self_signed` HIGH, so a router scanned by address can still fail `--fail-on high`. 040's key-size and signature checks fire on real servers from Community 0.2.57 (they had read fields Node never sets): an RSA key under 2048 bits is `weak_rsa_key` HIGH (CRITICAL under 1024), an EC key under 256 bits `weak_ec_key` HIGH, a CA-issued certificate signed with SHA-1 or MD5 `weak_signature` HIGH, and an issued intermediate signed that way `chain_weak_signature` MEDIUM. A self-signed certificate's own signature is not graded — no client verifies it — and the `self_signed` finding names the algorithm instead. A key of neither type (Ed25519), or a runtime that cannot read signature algorithms (Node 22 or older; Node 24 reads them), records `not assessed` on `certAudit`, never a pass. So a router serving a 1024-bit key now also shows `weak_rsa_key` HIGH. From Enterprise 2.0.0 they fail the certificate control — item (d). The teaching that follows is 0.2.53's, carried forward and still current. — the delta release: the product can now answer *what changed since the last scan*. Teach five things: (1) **`nsauditor-ai report --from <dir> --format executive --since <runId|prior>`** (`--format` is REQUIRED; `report` refuses without it) (Pro/Enterprise) compares two runs and reports new / resolved / severity-changed findings. (2) **NEVER present a `resolved` count as remediation on its own** — the report carries a NOT-COMPARABLE bucket with a reason per finding (host not scanned · the producing plugin did not run, ERRORED or TIMED OUT on that host, or cannot be identified · an evidence gap such as AccessDenied · **a PORT that was not measured on that host — a check that could not complete its connection, or a port that STOPPED ANSWERING between the two runs (a port that went quiet is not a fix)** · **the two runs COVERED DIFFERENT SCOPES — a narrower `--aws-region`, a different Azure subscription or GCP project — so that surface was not looked at, which is NOT the same as clean** · framework enumeration moved · **the PRODUCER CHANGED WHAT IT NAMES between the two releases**), and a finding that vanished for any of those reasons was not fixed. ⚠️ **THE LAST ONE ARRIVED IN EE 1.1.0 AND IT FIRES ON AN UPGRADE ACROSS 1.1.0.** FOURTEEN producers changed what identifies a finding at EE 1.1.0 — thirteen plugins that had left the object empty, named the REGION, recorded no region the report reads, carried a HIGH sentence that had to be corrected (1110, the effective-decrypt auditor), or (1023, the Zero Trust checker) now name the port a finding is about and keep the open-port count out of its title, and one analysis AGENT whose findings are keyed on their TITLE because they carry no object field at all. A comparison that straddles that upgrade reports those rows NOT-COMPARABLE instead of differencing them, so one surface under two keys is never read as a fix plus a fresh exposure. **So the first `report --since` a user runs across the 1.1.0 upgrade will legitimately show a LARGE not-comparable bucket — that is the declaration working, not a broken report.** Derive the reason list from `NOT_COMPARABLE_REASONS` in Community's `utils/scan_delta.mjs` rather than from this sentence; it is held in equality with what the engine emits, and this sentence is prose that has disagreed with it before. If a user asks what got fixed, read the NOT-COMPARABLE list before answering. **Two conditions refuse the whole comparison rather than bucketing findings**: the two runs straddle a product boundary where a reported number changed meaning, and the two runs ran at DIFFERENT LICENCE TIERS — the set of producers that ran differs, so a whole producer's findings would read as remediation. ⚠️ **AND ONE OF THOSE REASONS IS DECLARED RATHER THAN CHECKED: framework-enumeration movement is NOT EVALUATED in this edition**, because a run record carries no framework enumeration. The report says so as a limit on every comparison and each row's basis reads `framework enumeration: not evaluated`. Never tell a user that framework movement was ruled out. (3) **Integrity is a hash chain, not a signature — and say WHAT is sealed.** At finalize the run record AND each written host's findings files are digested over their exact persisted bytes, with the per-file digests sealed inside the record, which is itself digested and chained through `prevDigest`. So an altered **findings file** — which is what the comparison actually reads — is detected and NAMED, not merely an altered index; a file that did not exist at seal time is sealed as an explicit absence, so adding one is detected too. **BOTH runs are verified, not only the baseline.** An altered or unmeasurable run on either side REFUSES the comparison outright, and the refusal names which side. A file that cannot be READ is reported as unmeasurable rather than as tampering, and a record written before per-host sealing keeps its verdict while its reason says the findings files were not covered. The guarantee is tamper-EVIDENT against corruption and unsophisticated edits, **NOT tamper-proof against host-level access and NOT non-repudiation**. Do not describe it as file-integrity monitoring or a change-detection mechanism: PCI DSS `11.5.1` / `11.5.2` / `11.6.1` are enumerated OUT OF SCOPE in the shipped matrix. The code ships in Community; the capability is licence-gated to Pro/Enterprise. ⚠️ **`--since` REQUIRES COMMUNITY 0.2.55 OR LATER — AND ENTERPRISE 2.0.0 REQUIRES 1.0.0 (item (c) above).** The peer floor moved to 0.2.55 at EE 1.1.0 because **Enterprise CALLS the new Community code** — the per-plugin run statuses the Zero Trust checker reads before it will assert a posture, and the run-record fields `report --since` compares. The earlier floor said otherwise on the grounds that Enterprise did not call it; that stopped being true at EE 1.1.0. If a user reports the flag is unrecognised, check their Community version first. (4) **A `kms:Decrypt on Resource:*` HIGH from 1110 is NEVER downgraded on a live run.** 1110 reads KMS key policies and grants in the configured region only, and one region cannot show that NO key trusts a principal — a key in another region may — so the HIGH→INFO downgrade it used to apply is withheld on every shipped path. The reason is in that plugin's run summary as `summary.kmsDowngrade` (`reason` `single-region-enumeration` for a complete read; `access-denied` · `per-key-error` · `keys-truncated` · `grants-truncated` for an incomplete one, which also emits an evidence-gap row). Never tell a user a surviving HIGH was checked against every key, and never read a missing downgrade as a scanner fault. Its grant finding (`kms-grant-decrypt-no-identity-grant`, MEDIUM) likewise covers keys in the configured region only. (5) **The NVD store and response cache MOVED at EE 1.1.0 and every run records where they are.** They used to be a `.nvd-cache` folder in whatever directory the scan started from; they now resolve, from any directory, to `NVD_CACHE_DIR`, else `NSAUDITOR_NVD_CACHE_DIR`, else `~/.nsauditor/nvd_cache`. Only `feed import` (of a feed you downloaded yourself) takes `--cache-dir`; a scan does not read that flag, so it reads a store imported with `--cache-dir <d>` only when that chain resolves to `<d>` (e.g. `NVD_CACHE_DIR=<d>`). The MCP `get_vulnerabilities` cache reads `NSAUDITOR_NVD_CACHE_DIR`, never `NVD_CACHE_DIR`. An old `.nvd-cache` folder is NOT read, and the scan says so. If a user reports that offline CVE matching stopped after upgrading, that is the cause: move the folder once or set `NVD_CACHE_DIR` to it. Never read the resulting coverage gaps as a clean host. Plugin count UNCHANGED at <!-- nsa:derived id="plugins:ee" -->29<!-- /nsa:derived -->; <!-- nsa:derived id="matrix-movement" -->all eight coverage matrices unchanged since EE 2.0.0<!-- /nsa:derived -->; Community <!-- nsa:derived id="ce-version" -->1.1.0<!-- /nsa:derived --> (published with this skill).
19
+ >
20
+ > **Prior: 1.0.0** (post-EE-2.0.0 · **requires CE ≥ 1.0.0**) — **2.0.0: Node.js 24 is the supported runtime. Every one-shot compliance scan writes what changed since the previous scan beside its pack — controls to fail or to pass, controls that lost measurement, violations new or cleared, and what could not be measured, never read as cleared — and can notify a webhook, Slack or Teams when a control goes to fail or loses measurement, when a new violation reaches a severity you set, or when the drift could not be compared. A scan connects only to the addresses its host check resolved (Community 1.0.0), the AI payload carries no AWS account ID, and plugin 040's weak-key and weak-signature findings fail the certificate control in six of the eight frameworks.** Its teaching — the drift file and its notification (a), the resolved-address pin (b), the AI payload and plugin 040 (d) — still holds and is carried forward in the header above; its runtime sentence is superseded by the Node.js 24.22.1 floor, item (c).
21
+ >
22
+ > **Prior: 0.2.55** (post-EE-1.3.0 · **requires CE ≥ 0.2.57**) — the 1.3.0 release: what a scan did not measure is not counted as fixed, with two measured limits. Its register, its limits and its teaching still hold and are carried forward in the header above; its Community 0.2.57 floor is superseded by the 1.0.0 floor, item (c).
19
23
  >
20
24
  > **Prior: 0.2.54** (post-EE-1.2.0 · **requires CE ≥ 0.2.56**) — **1.2.0: A finding on a port, region or producer a scan did not measure is not counted as fixed, and with SLA tracking on the control it failed is held FAILED. Two measured limits in this release: after a CVE lookup that failed, the prior CVE rows are not counted as fixed, but the controls they failed are not held FAILED and can read PASS; and when two compared scans ran different `--plugins` sets, a row an analysis agent or the CVE mapper derived from a plugin only one of them requested can read RESOLVED or NEW. When the later scan left the plugin out, the row also counts as closed in MTTR and its control can read PASS. Keep `--plugins` identical between compared scans.** Measured then: on Community 0.2.55, Community still loads the Enterprise plugins, and `license --plugins` reports Enterprise 1.2.0 `(loaded)`, but the scan skips its intelligence, analysis-agent and compliance stages without a word. Its other teaching — the NOT-COMPARABLE cases (a), the hold (b) and how a failed Enterprise load surfaces — is carried forward in the header above.
21
25
  >
@@ -159,6 +163,14 @@ skill teaches you how to operate it via MCP tools and CLI.
159
163
  NSAuditor AI exposes tools via Model Context Protocol (stdio transport). Available tools
160
164
  depend on the license tier (Community / Pro / Enterprise).
161
165
 
166
+ > **Receipts.** Every tool response ends with a `── MCP call receipt ──` footer and a `call_id`. You cannot verify a
167
+ > response yourself, and you cannot save one — never call a response verified. The operator checks one with
168
+ > `nsauditor-ai mcp verify-call <call_id> --response <file>`, which needs the WHOLE response; when the client showed it
169
+ > truncated, tell them about `NSA_MCP_RESPONSE_DIR` (below): with it set, the server saves each response as
170
+ > `<call_id>.txt` and its request as `<call_id>.request.json`, and the footer says "A copy of this response was saved
171
+ > as <call_id>.txt, and its request as <call_id>.request.json, in the directory NSA_MCP_RESPONSE_DIR names." only
172
+ > when it did. The operator checks both with `--response <file> --request <file>`; you cannot read either file.
173
+
162
174
  ### Community Edition Tools (always available)
163
175
 
164
176
  #### `scan_host`
@@ -170,9 +182,16 @@ order (discovery → service probes → OS detection → result fusion).
170
182
  | `host` | string | ✅ | — | Target hostname or IP address |
171
183
 
172
184
  Every plugin runs under the server's `PLUGIN_TIMEOUT_MS` (default 30000), including a plugin that
173
- declares a longer budget of its own; there is no per-call timeout. A plugin that runs out of time
185
+ declares a longer budget of its own; the call takes no timeout argument. A plugin that runs out of time
174
186
  reads `timeout` in the result's `manifest` — its surface was NOT measured, which is never a clean
175
- result.
187
+ result. **The whole call stops at `SCAN_HOST_WALL_MS`** (default 50000; `0` = off), because a client may end a
188
+ tool call at a limit of its own (Claude Desktop 3.x at about 60 s) and return nothing. When it stops, the response's
189
+ `callWall.reached` is `true`, `callWall.notStarted` and `callWall.cutShort` list the plugins that did not measure
190
+ (in the order they would have run), a `note` says the scan is partial, and the `markdown` opens with the same. **Say
191
+ so in your answer: name those plugins and say the result is partial — never present the services found as the host's
192
+ whole posture.** The plugins that run late (the TLS Scanner 011, the TLS certificate audit 040) are the first to be
193
+ lost; for a complete scan the operator runs the CLI, or raises `SCAN_HOST_WALL_MS` in a client that does not cut the
194
+ call.
176
195
 
177
196
  ⚠️ **What `scan_host` returns, and what it does NOT.** Services, the OS, and the flags the service checks leave on
178
197
  each service record: weak SSH algorithms, an SNMP default community, weak TLS protocols / ciphers and a self-signed
@@ -186,9 +205,11 @@ IP-address target 060 DECLINES — its `manifest` entry reads `skipped` with the
186
205
  `DNS-security audit (060) not tested`, and nothing lands — so DNS posture is unknown, never clean. Anonymous FTP
187
206
  login, zone transfer and the SMB null session are tested only when the MCP server's environment enables them
188
207
  (`FTP_CHECK_ANON=true`; `DNS_CHECK_AXFR=true` with `DNS_AXFR_DOMAIN`; `SMB_NULL_SESSION=true`) — all off by default.
189
- A null `anonymousLogin`, `axfrAllowed`, `nullSessionAllowed` or `dangerousMethods` means NOT TESTED, never "not
190
- allowed" or "none": `anonymousLoginTested`, `axfrTested` and `nullSessionTested` are `true` when measured, else the
191
- reason (`opt-in-off`, `no-domain`, `no-answer`). With the Enterprise package, the zero-trust assessment (1023) reaches
208
+ 060's own zone-transfer attempt against the domain's nameservers runs only with `DNS_AUDIT_AXFR_CHECK=true`, also off
209
+ by default, and it records only an open transfer (`axfr_open` in `dnsSecurity`) — so its absence is never a measured
210
+ "closed". A null `anonymousLogin`, `axfrAllowed`, `nullSessionAllowed` or `dangerousMethods` means NOT TESTED, never
211
+ "not allowed" or "none": `anonymousLoginTested`, `axfrTested` and `nullSessionTested` are `true` when measured, else
212
+ the reason (`opt-in-off`, `no-domain`, `no-answer`). With the Enterprise package, the zero-trust assessment (1023) reaches
192
213
  the conclusion only as one score line in its `evidence`, not its per-dimension findings. It does NOT look up CVEs and
193
214
  does NOT run the Enterprise analysis agents or exploit intelligence (those run in the CLI scan's enrichment). So
194
215
  `Security findings: 0` in its `markdown` is NOT a clean verdict and not a statement that the host has no known
@@ -205,7 +226,7 @@ analysis-agent findings, and plugins 040 and 060 had recorded 2 and 3 HIGH findi
205
226
  response then. Those audit entries land on the records now and are counted; the CVEs and the agent findings still are
206
227
  not.
207
228
 
208
- **Returns:** `{ host, conclusion, manifest[], pluginsRan, markdown }` — `conclusion.result` is the fused
229
+ **Returns:** `{ host, conclusion, manifest[], pluginsRan, markdown, callWall{ limitMs, reached, notStarted[], cutShort[] } }` (plus a `note` when `callWall.reached`) — `conclusion.result` is the fused
209
230
  record (`summary` is a one-line string; `host`, `services[]`, `evidence[]`), and `manifest[]`
210
231
  is every plugin's `{ id, name, status, reason, duration_ms }`; `pluginsRan` is the number of `manifest[]` entries
211
232
  whose status is `ran`. There is no `findings` array. See
@@ -288,7 +309,7 @@ Run a single plugin against a specific host:port for deep-dive investigation.
288
309
  | 014 | NetBIOS/SMB Scanner | SMB/NetBIOS enumeration, null sessions |
289
310
  | 040 | TLS Certificate & Cipher Auditor | Certificate chain, expiry, weak ciphers |
290
311
  | 050 | TRIBE v2 Neural API Security Probe | Debug leaks, stack traces, CORS misconfig on a TRIBE v2 API |
291
- | 060 | DNS Security Auditor | SPF/DKIM/DMARC, DNSSEC, zone transfer |
312
+ | 060 | DNS Security Auditor | SPF/DKIM/DMARC, DNSSEC, zone transfer (opt-in: `DNS_AUDIT_AXFR_CHECK`) |
292
313
 
293
314
  ---
294
315
 
@@ -332,11 +353,11 @@ These tools return a license upgrade prompt on CE installations:
332
353
  | Tool | Tier | Purpose |
333
354
  |------|------|---------|
334
355
  | `scan_cloud` | Enterprise | Audit one or more cloud accounts (AWS / GCP / Azure) for security & compliance posture using the server-configured credentials. No network host needed. Input: `{ providers?: ("aws"\|"gcp"\|"azure")[], regions?: string[] }` — **pass only the cloud(s) the user names** (`providers:["aws"]` for "audit my AWS account"); omit `providers` only when the user asks to audit ALL clouds. Use this (not `scan_host`) when the user asks to "audit my AWS account", "audit my AWS and Azure accounts", or "check my cloud compliance". CE/Pro callers get an upgrade message. **`regions` (AWS only)** — AWS region codes (e.g. `["us-east-1","eu-west-1"]`) or `["all"]`. **Default — omit `regions` (MOST requests):** a plain "audit my AWS account", a "quick check", or any request that names no region AND does not explicitly ask for all/every/whole-account/complete/full coverage → **OMIT `regions`** (the auditors that take their region from the client audit the server-configured `AWS_REGION` and no other; the three that enumerate their own region list — CloudTrail trail discovery (1040), GuardDuty/Inspector (1200), EC2 instances (1210) — still attempt every enabled region; do NOT fan out or batch). Omitting does NOT scan all regions with every auditor. **Specific regions:** when the user names region(s), pass exactly those. **All regions — ONLY on an explicit "all regions / every region / whole account / full coverage" request:** use the discover-then-batch approach in the region-scope note below — do NOT pass a single `["all"]` call and rely on it (it fans every regional plugin across all enabled regions and usually EXCEEDS the host's MCP tool-call timeout, e.g. Claude Desktop's, returning nothing). Unknown region codes are rejected before the scan runs (the WHOLE call fails — fix the region and re-call; never silently drop it). |
335
- | `get_findings` | Enterprise | Drill into the findings of the MOST RECENT `scan_cloud` scan — a per-provider, **per-session** cache (NOT live state; cleared when the MCP server restarts). Input: `{ scanId?, provider?, plugin?, severity?, category?, cursor?, limit? }`. Use it AFTER `scan_cloud` when the summary's **category rollup** shows a category you want to expand to specific resources, or when you need the FULL untruncated text of a finding. Pass the **`scanId` from the `scan_cloud` summary footer** + the `provider`; filter by `category`/`severity`/`plugin`; paginate with `cursor`/`limit` (server-capped at 20 — follow `nextCursor`). If you get a **"re-run scan_cloud"** error the cache was cleared or superseded — **re-run `scan_cloud`, do NOT retry `get_findings`**. CE/Pro callers get the same upgrade message as `scan_cloud`. |
356
+ | `get_findings` | Enterprise | Drill into the findings of the MOST RECENT `scan_cloud` scan — a per-provider, **per-session** cache (NOT live state; cleared when the MCP server restarts). Input: `{ scanId?, provider?, plugin?, severity?, category?, cursor?, limit? }`. Use it AFTER `scan_cloud` when the summary's **category rollup** shows a category you want to expand to specific resources, or when you need the FULL untruncated text of a finding. Pass the **`scanId` from the `scan_cloud` summary footer** + the `provider`; filter by `category`/`severity`/`plugin`; paginate with `cursor`/`limit` (server-capped at 20 — follow `nextCursor`). If you get a **"re-run scan_cloud"** error the cache was cleared or superseded — **re-run `scan_cloud`, do NOT retry `get_findings`**. **A `category` filter that returns `total: 0` also returns `categoriesInScan`** — every category this scan holds, with its `total` and the `plugins` that emitted it, whatever `plugin` or `severity` you passed — and a `note`. Read both: if the category is listed, it exists in this scan at another severity or from another plugin, so say which; if it is not listed, say this scan had no finding in that category — **never say the release does not, or cannot, emit it, nor that it may belong to a later or different release** — nothing in the response tells you that. CE/Pro callers get the same upgrade message as `scan_cloud`. |
336
357
 
337
- > **Interpreting `scan_cloud` results — never report a false clean:** read **`findingsSummary`** for the findings — it maps each provider to `counts` (per-severity totals) and a `findings` list of the CRITICAL/HIGH items (`{severity, plugin, title}`); report those. A cloud was effectively audited only if it appears in `auditedProviders`. If the result has `audited: false`, any `notes` entries, or `pluginsRan: 0`, the cloud was **NOT** audited (no plugins, missing credentials, or skipped) — report the gap explicitly; an empty result is **not** a clean pass. Do not infer "clean" from an empty `findingsSummary` when the cloud is not in `auditedProviders`. **Beyond CRITICAL/HIGH, `findingsSummary[provider].rollup` groups the remaining findings by `category` with counts (count-descending) across **MEDIUM, LOW *and* INFO** — and the INFO tier is not optional reading: every deferred-scope boundary is INFO, and an evidence gap carries its own finding's severity, which can be any tier up to HIGH, so read every tier. ⚠️ This sentence said "MEDIUM + LOW" until EE 0.39.0, which was FALSE against the summariser's own three rollup buckets, and false in the direction that hides the tier a reader most needs. Report every tier that occurs; these are actionable too: a category like `sqs-age-alarm-missing` or `*-public` is a real gap, not noise, and reporting only CRITICAL/HIGH while the rollup is non-empty is itself a false clean. To enumerate the specific resources behind a rollup category, or to read a finding's full untruncated text, call `get_findings` with the `scanId` from the summary footer + the `category`.**
358
+ > **Interpreting `scan_cloud` results — never report a false clean:** read **`findingsSummary`** for the findings — it maps each provider to `counts` (per-severity totals) and a `findings` list of the CRITICAL/HIGH items (`{severity, plugin, title}`); report those. A title that ends `(+N more via get_findings)` is one of that finding's N+1 entries, not the whole finding: call `get_findings` with the `scanId`, that line's `plugin` and `severity`, and read the row whose `resource` begins the title — its `text` holds every entry. The entries include evidence statements as well as defects, so read them before counting anything. A cloud was effectively audited only if it appears in `auditedProviders`. If the result has `audited: false`, any `notes` entries, or `pluginsRan: 0`, the cloud was **NOT** audited (no plugins, missing credentials, or skipped) — report the gap explicitly; an empty result is **not** a clean pass. Do not infer "clean" from an empty `findingsSummary` when the cloud is not in `auditedProviders`. **Beyond CRITICAL/HIGH, `findingsSummary[provider].rollup` groups the remaining findings by `category` with counts (count-descending) across **MEDIUM, LOW *and* INFO** — and the INFO tier is not optional reading: every deferred-scope boundary is INFO, and an evidence gap carries its own finding's severity, which can be any tier up to HIGH, so read every tier. ⚠️ This sentence said "MEDIUM + LOW" until EE 0.39.0, which was FALSE against the summariser's own three rollup buckets, and false in the direction that hides the tier a reader most needs. Report every tier that occurs; these are actionable too: a category like `sqs-age-alarm-missing` or `*-public` is a real gap, not noise, and reporting only CRITICAL/HIGH while the rollup is non-empty is itself a false clean. To enumerate the specific resources behind a rollup category, or to read a finding's full untruncated text, call `get_findings` with the `scanId` from the summary footer + the `category`.**
338
359
 
339
- > **Reporting `scan_cloud` region scope — never overstate coverage:** Report the regions you ACTUALLY scanned, derived from the `regions` you **passed** — NOT from the findings. If you OMITTED `regions`, the auditors that take their region from the client scanned only the server-default region (`AWS_REGION`) — say that, and that the account's OTHER enabled regions were NOT covered by them (offer to re-run for all regions). The three auditors that enumerate their own region list — CloudTrail trail discovery (1040), GuardDuty/Inspector (1200), EC2 instances (1210) — attempt every enabled region when `regions` is omitted. Say one of them covered every region ONLY if it `ran` and carries no multi-region evidence gap (a single-region fallback, or errored or denied regions); any region such a gap names was NOT audited, say so. 1040's CloudWatch-alarm and AWS Config checks cover the configured region only. Never call the whole scan single-region when these three ran. **Never escalate a single-region or "quick" request into a multi-region scan.** Do NOT claim "all regions" / "every region" / "across N regions" for the scan as a whole because those three list per-region findings: their coverage is NOT evidence the other auditors ran outside the region(s) you passed. When you DO pass `regions`, their region-scoped checks follow the list you passed.
360
+ > **Reporting `scan_cloud` region scope — never overstate coverage:** Report the regions you ACTUALLY scanned, derived from the `regions` you **passed** — NOT from the findings. If you OMITTED `regions`, the auditors that take their region from the client scanned only the server-default region (`AWS_REGION`) — say that, and that the account's OTHER enabled regions were NOT covered by them (offer to re-run for all regions). The three auditors that enumerate their own region list — CloudTrail trail discovery (1040), GuardDuty/Inspector (1200), EC2 instances (1210) — attempt every enabled region when `regions` is omitted. Say one of them covered every region ONLY if it `ran` and carries no multi-region evidence gap (a single-region fallback, or errored or denied regions); any region such a gap names was NOT audited, say so. 1040's CloudWatch-alarm and AWS Config checks cover the configured region only. Never call the whole scan single-region when these three ran. **Never escalate a single-region or "quick" request into a multi-region scan.** Do NOT claim "all regions" / "every region" / "across N regions" for the scan as a whole because those three list per-region findings: their coverage is NOT evidence the other auditors ran outside the region(s) you passed. When you DO pass `regions`, their region-scoped checks follow the list you passed — except a listed region the account has not enabled (from Enterprise 2.1.0, wherever `ec2:DescribeRegions` can be read): no plugin calls it, and its row reads "Region '<region>' could not be scanned — the region is not enabled for this account", an evidence gap. Report that region as NOT scanned because it is not enabled, never as audited, clean or missing a permission; `["all"]` walks the regions the account has enabled.
340
361
 
341
362
  > **Full all-region coverage — discover then batch** (use ONLY when the user explicitly asked for all/every/whole-account/complete/full region coverage; NEVER for a plain or "quick" request — those stay on the `regions` default above, with no explicit fan-out): a single `regions:["all"]` call usually exceeds the host's MCP tool-call timeout (e.g. Claude Desktop's) and returns nothing. Reliable pattern: (1) run a default scan (omit `regions`) — its GuardDuty/Inspector findings enumerate every enabled region, giving you the full list while the region-scoped auditors cover the default region; (2) scan the REMAINING regions in small batches (3–5 region codes per `regions:[...]` call) across successive calls until every enabled region is covered; (3) merge and report the TOTAL number of regions actually covered — **count** them, don't guess. If you try `["all"]` and it times out, that result is INCOMPLETE — fall back to batching and continue until complete; never report a timed-out or partial scan as full coverage.
342
363
  > **⚠️ COMPLIANCE IS A CLI SURFACE, NOT AN MCP TOOL.** `compliance_check` is WITHDRAWN — there is no such tool and there never was, and the evidence pack is produced by `nsauditor-ai scan --host <target> --compliance <fw> --out <dir>`, which runs the compliance PHASE and writes `scan_compliance_<framework>.{json,md,html}` plus its attestation and chain-of-custody sidecars. `scan_cloud` **never runs that phase** and maps no finding to any framework, so no MCP call produces a pack. Use `compliance_matrix` to state COVERAGE (what is covered / partial / out of scope); use the CLI to produce EVIDENCE.
@@ -576,18 +597,27 @@ See `references/schemas.md` for complete structures:
576
597
  for a host name — every answer is checked — and refuses a name that does not resolve. Private ranges (RFC 1918,
577
598
  CGNAT `100.64/10`, `fc00::/7`) are refused too unless `NSA_ALLOW_ALL_HOSTS` is `1`, `true`, `yes` or `on`, which
578
599
  admits private ranges only, never loopback, link-local or metadata. It takes one host: a CIDR or URL-shaped string
579
- is refused. The check does not pin the answer, so a name whose answer changes between the check and the scan (DNS
580
- rebinding) is not caught. Set the variable **only** for legitimate local network auditing.
600
+ is refused. From Community Edition 1.0.0 the scan connects to the answers the check resolved, so a name whose answer
601
+ changes between the check and the scan (DNS rebinding) is not followed — except through an HTTP proxy set with
602
+ `NODE_USE_ENV_PROXY`, which resolves the name itself, or over an idle connection opened earlier to the same name, for
603
+ as long as it stays idle in the pool (a few seconds by default; a server's Keep-Alive hint holds it longer); an older
604
+ Community Edition does not pin the answer. One exception, off by default: with `DNS_AUDIT_AXFR_CHECK` set, the
605
+ DNS-security audit (060) opens TCP/53 to each of the scanned domain's nameservers, at addresses no host check resolved.
606
+ Set the variable **only** for legitimate local network auditing.
581
607
 
582
608
  3. **AI Redaction:** with AI analysis on and `OPENAI_REDACT` at its default (on), the scan payload
583
609
  (host, summary, services, evidence) is scrubbed at every tier: the host field becomes
584
610
  `[REDACTED_HOST]`; private IPv4 becomes `[REDACTED_HOST]` in the summary and `[REDACTED_IP]` in
585
611
  services and evidence; public IPv4 becomes `[IP]`; link-local and full-form IPv6 and colon-form
586
612
  MACs (`[MAC]`) are masked; serial numbers become `[REDACTED_HIDDEN]`, as do values under keys
587
- containing a word listed in `CONFIDENTIAL_KEYWORDS` (empty by default). **That payload is NOT
588
- scrubbed of** email addresses, internal hostnames, SNMP community strings, Bearer tokens, AWS keys,
589
- file paths or compressed IPv6 (`2001:db8::1`); ports, service names, versions and other evidence
590
- text go as they are. At Pro and above, the findings block added to the prompt is also masked by
613
+ containing a word listed in `CONFIDENTIAL_KEYWORDS` (empty by default). From Community Edition 1.0.0
614
+ an AWS account ID found in an ARN or an `amazonaws.com/<id>/…` URL becomes `[ACCOUNT_ID]` everywhere
615
+ it occurs, and any other 12-digit number `[REDACTED_ID]`; the system prompt is written beside the
616
+ payload (`scan_response_ai_system_prompt.txt`) before the request. **That payload is NOT scrubbed
617
+ of** email addresses, internal hostnames, SNMP community strings, Bearer tokens, AWS access-key IDs,
618
+ Azure subscription IDs, cloud resource names (an account ID inside one is masked from 1.0.0), file
619
+ paths or compressed IPv6 (`2001:db8::1`); ports, service names, versions and other evidence text go
620
+ as they are. At Pro and above, the findings block added to the prompt is also masked by
591
621
  default: target hosts, private IPv4, emails, MACs, internal hostnames and `community=<value>`
592
622
  strings (a community quoted in a finding title is not); AWS keys, Bearer tokens and file paths
593
623
  only with `NSA_AI_REDACT_LEVEL=strict`. With Ollama at its default localhost URL, the AI call
@@ -619,9 +649,9 @@ See `references/schemas.md` for complete structures:
619
649
  | `AI_PROVIDER` | openai | `openai` · `claude` · `ollama` |
620
650
  | `OPENAI_API_KEY` | — | OpenAI API key (or `keychain:OPENAI_API_KEY`) |
621
651
  | `ANTHROPIC_API_KEY` | — | Claude/Anthropic API key |
622
- | `OPENAI_MODEL` | gpt-4o-mini | OpenAI model name |
652
+ | `OPENAI_MODEL` | gpt-6-luna (Community Edition 1.0.0 and later; gpt-4o-mini before) | OpenAI model name |
623
653
  | `ANTHROPIC_MODEL` | claude-sonnet-4-6 | Anthropic model name (used when `AI_PROVIDER=claude`) |
624
- | `OPENAI_REDACT` | true | On the CLI, masks the host, most IP addresses, colon-form MACs and serials in the AI scan payload — NOT emails, internal hostnames, Bearer tokens or AWS keys (see **AI Redaction** above) |
654
+ | `OPENAI_REDACT` | true | On the CLI, masks the host, most IP addresses, colon-form MACs, serials and (from Community Edition 1.0.0) AWS account IDs in the AI scan payload — NOT emails, internal hostnames, Bearer tokens or AWS access keys (see **AI Redaction** above) |
625
655
  | `CONFIDENTIAL_KEYWORDS` | unset — no keyword scrub | Comma-separated substrings, e.g. `password,token,secret`: on the CLI, while `OPENAI_REDACT` is on, any AI-payload key containing one (any case) has its value replaced with `[REDACTED_HIDDEN]`. Unset, a key such as `password` is not masked for its name (serial-number keys — `serial`, `serialNumber`, `sn` — are masked either way) |
626
656
  | `NSAUDITOR_LICENSE_KEY` | — | Pro/Enterprise JWT license key |
627
657
  | `COMPLIANCE_GRC_PROVIDER` | — | **Enterprise** — opt-in scan-time GRC push: `vanta`, `drata`, or `secureframe`. Needs `COMPLIANCE_GRC_TOKEN`; optional `COMPLIANCE_GRC_BASE_URL` / `COMPLIANCE_GRC_CONTROL_MAP` / `COMPLIANCE_GRC_REDACTION` (`off`/`hash`/`remove`, default `off`). Finding content is **NOT redacted by default**: with `off`, a Vanta push carries finding text and `host:port` targets in the clear; `hash` or `remove` scrubs them. Token never serialized. Early-access; live-tenant validation not yet complete. |
@@ -629,7 +659,10 @@ See `references/schemas.md` for complete structures:
629
659
  | `SMB_NULL_SESSION` | false | Allow SMB null session probe |
630
660
  | `FTP_CHECK_ANON` | false | Let the FTP check try an anonymous login — without it no scan reports anonymous FTP |
631
661
  | `DNS_CHECK_AXFR` / `DNS_AXFR_DOMAIN` | false / — | Let the DNS check try a zone transfer of that zone — without both, `axfrAllowed` stays null (not tested) |
662
+ | `DNS_AUDIT_AXFR_CHECK` | false | Let the DNS-security audit (060) try a zone transfer of the scanned domain against each of its nameservers (`1`, `true`, `yes` or `on`). It connects to each nameserver's own address, outside the scan's host check, and records only an open transfer |
632
663
  | `ENABLE_SYN_SCAN` | false | Enable Nmap TCP SYN scanning (requires root) |
664
+ | `SCAN_HOST_WALL_MS` | 50000 | MCP server only: the time limit for the whole `scan_host` call (`0` = off), so a client that ends tool calls at about 60 s still gets a disclosed partial — read `callWall` |
665
+ | `NSA_MCP_RESPONSE_DIR` | unset (off) | MCP server only: an ABSOLUTE directory where the server saves each tool response, exactly as returned, as `<call_id>.txt`, with the call's request (tool and arguments, verbatim) beside it as `<call_id>.request.json`, both or neither (directory `0700`, files `0600`, never overwriting), so `verify-call` works on a response the client truncated and can check what the call asked for (`--request`). Set in the client config's `env` block, then fully quit and reopen the client. The files hold scan results and are never removed by the server |
633
666
 
634
667
  ### Plugin-Specific Timeouts
635
668
 
@@ -684,7 +717,7 @@ Run `install-key` once per machine, and use the `NSA_MCP_AUTH_KEY` value in the
684
717
  }
685
718
  ```
686
719
 
687
- `NSA_ALLOW_ALL_HOSTS: "1"` lets Claude scan your own network — private addresses such as `192.168.x.x` or `10.x.x.x`. It admits private ranges only (RFC 1918, CGNAT `100.64/10`, `fc00::/7`): the server still refuses loopback, link-local and cloud-metadata addresses in any spelling, whether written as the host or returned for a host name — every answer is checked. Only `1`, `true`, `yes` or `on` turn it on. The check does not pin the answer, so a name whose answer changes between the check and the scan (DNS rebinding) is not caught. `install-key` does not print it, so add it yourself, and remove it if you only scan public hosts.
720
+ `NSA_ALLOW_ALL_HOSTS: "1"` lets Claude scan your own network — private addresses such as `192.168.x.x` or `10.x.x.x`. It admits private ranges only (RFC 1918, CGNAT `100.64/10`, `fc00::/7`): the server still refuses loopback, link-local and cloud-metadata addresses in any spelling, whether written as the host or returned for a host name — every answer is checked. Only `1`, `true`, `yes` or `on` turn it on. From Community Edition 1.0.0 the scan connects to the answers the check resolved, so a name whose answer changes between the check and the scan (DNS rebinding) is not followed — except through an HTTP proxy set with `NODE_USE_ENV_PROXY`, which resolves the name itself, or over an idle connection opened earlier to the same name, for as long as it stays idle in the pool (a few seconds by default; a server's Keep-Alive hint holds it longer); an older Community Edition does not pin the answer. 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 addresses no host check resolved. `install-key` does not print it, so add it yourself, and remove it if you only scan public hosts.
688
721
 
689
722
  `PLUGIN_TIMEOUT_MS` bounds each plugin `scan_host` runs, not the call: the plugins run one after
690
723
  another, so the call takes roughly the sum of their times. On one router, a `scan_host` call timed out
@@ -752,6 +785,7 @@ The CLI's report tags what it finds with MITRE techniques (the `scan_host` tool
752
785
  | `scan_conclusion_raw.json` | JSON | Full unredacted scan data (admin) |
753
786
  | `scan_conclusion_raw.html` | HTML | Admin dashboard with filters |
754
787
  | `scan_response_ai_payload.json` | JSON | Redacted payload sent to AI |
788
+ | `scan_response_ai_system_prompt.txt` | Text | System prompt sent to AI, the Pro/Enterprise intelligence block included (from Community 1.0.0) |
755
789
  | `scan_response_ai.html` | HTML | Styled report with CVE links, severity badges |
756
790
  | `scan_response_ai.txt` | Markdown | AI vulnerability assessment (text) |
757
791
  | SARIF | JSON | CI/CD integration (GitHub Advanced Security, Azure DevOps) |
@@ -144,10 +144,10 @@ new, removed and changed services; the findings delta; each service-check findin
144
144
  not be compared). The webhook alerts a host whose scan changed since the previous cycle and that carries a finding
145
145
  at or above --alert-severity — one detail per finding, each with its own grade; the first cycle sets the baseline and
146
146
  alerts nobody, and --alert-every-cycle alerts every such host on every cycle. At high that is anonymous FTP login or a
147
- DNS zone transfer (both off unless FTP_CHECK_ANON=true / DNS_CHECK_AXFR=true + DNS_AXFR_DOMAIN are set), an MCP
148
- server finding, an SNMP default community, an expired certificate, an SMB null session (off unless
149
- SMB_NULL_SESSION=true) or a HIGH entry of the 040 / 050 / 060 audits; Enterprise's CVE and analysis-agent findings
150
- never trigger it.
147
+ DNS zone transfer (off unless FTP_CHECK_ANON=true / DNS_CHECK_AXFR=true + DNS_AXFR_DOMAIN — or
148
+ DNS_AUDIT_AXFR_CHECK=true for 060's attempt — are set), an MCP server finding, an SNMP default community, an expired
149
+ certificate, an SMB null session (off unless SMB_NULL_SESSION=true) or a HIGH entry of the 040 / 050 / 060 audits;
150
+ Enterprise's CVE and analysis-agent findings never trigger it.
151
151
 
152
152
  Each scan appends one line per host to scan_history.jsonl in the output directory (--out, default out/)."
153
153
  ```
@@ -180,7 +180,8 @@ Each scan appends one line per host to scan_history.jsonl in the output director
180
180
  ✅ DKIM selector found (google)
181
181
  ✅ DMARC policy set to 'reject' (strongest setting)
182
182
  ⚠ DNSSEC not enabled (recommended for integrity)
183
- ✅ Zone transfer not reported open (060 tries AXFR only against the domain's nameservers)
183
+ ℹ Zone transfer NOT TESTED — 060 tries one only with DNS_AUDIT_AXFR_CHECK=true and records only an open
184
+ transfer, so `axfr: []` is never a pass
184
185
  Note: 060's MX checks flag a missing MX, a null MX, and an MX that is a CNAME or an IP address — it does not test
185
186
  STARTTLS / TLS on the mail servers.
186
187
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai-agent-skill",
3
- "version": "0.2.55",
3
+ "version": "1.1.0",
4
4
  "description": "AI Agent Skill for NSAuditor AI — gives any AI coding agent built-in knowledge of NSAuditor's MCP tools, schemas, plugins, and security audit workflows.",
5
5
  "keywords": [
6
6
  "nsauditor",
@@ -50,7 +50,7 @@
50
50
  ".claude-plugin/"
51
51
  ],
52
52
  "peerDependencies": {
53
- "nsauditor-ai": ">=0.2.57"
53
+ "nsauditor-ai": ">=1.0.0"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "nsauditor-ai": {
@@ -167,7 +167,7 @@ LLMNR poisoning risk).
167
167
  |----|------|-------|---------|
168
168
  | 040 | TLS Certificate & Cipher Auditor | 443, 465, 587, 636, 853, 993, 995, 8443, 8883, 9443 | Deep TLS audit: cert chain, expiry, self-signed, weak ciphers |
169
169
  | 050 | TRIBE v2 Neural API Security Probe | 8080 | Debug endpoints, stack traces in error bodies, leaking headers, CORS misconfiguration, unauthenticated routes and unexpected HTTP methods on a TRIBE v2 API. In a scan it runs only when TCP 8080 is open, so a scan never checks a website's 80/443 |
170
- | 060 | DNS Security Auditor | 53 | SPF/DKIM/DMARC validation, DNSSEC, zone transfer, MX security, CAA |
170
+ | 060 | DNS Security Auditor | 53 | SPF/DKIM/DMARC validation, DNSSEC, zone transfer (opt-in: `DNS_AUDIT_AXFR_CHECK`), MX security, CAA |
171
171
 
172
172
  ### Deep-Audit Plugin Details
173
173
 
@@ -186,8 +186,14 @@ fields Node never sets): an RSA key under 2048 bits is `weak_rsa_key` HIGH (CRIT
186
186
  bits `weak_ec_key` HIGH, a CA-issued certificate signed with SHA-1 or MD5 `weak_signature` HIGH, and an issued
187
187
  intermediate signed that way `chain_weak_signature` MEDIUM. A self-signed certificate's own signature is not graded —
188
188
  no client verifies it — and `self_signed` names the algorithm. A key of neither type (Ed25519), or a runtime that cannot
189
- read signature algorithms (Node 20; Node 24 reads them), records `not assessed` on `certAudit`. These grades are not yet
190
- routed to a compliance control.
189
+ read signature algorithms (Node 22 or older; Node 24 reads them), records `not assessed` on `certAudit`. From Enterprise 2.0.0 the
190
+ four grades fail the certificate control in SOC 2 (CC6.7), HIPAA (§164.312(e)(1)), NIST CSF 2.0 (PR.DS-02), PCI DSS (Req
191
+ 4.2.1), ISO 27001 (A.8.24) and CIS v8 (Safeguard 3.10) — where 040's certificate-validity findings route; GDPR Article 32
192
+ and NIST SP 800-171 are held. From Community 1.0.0, when the runtime refuses the chain with no named reason
193
+ (`UNSPECIFIED`) and a weak key or signature is graded, a second handshake at OpenSSL security level 0 decides whether the
194
+ chain is trusted: if it is, `ca_not_trusted` is not raised and the weak grade is the finding; a weak certificate under an
195
+ untrusted CA is named untrusted at the default level and keeps `ca_not_trusted`. The second look never runs on a
196
+ self-signed certificate, which reads `self_signed` instead.
191
197
 
192
198
  **050 — TRIBE v2 Neural API Security Probe:** Probes a TRIBE v2 API: `TRIBE_API_HOST` / `TRIBE_API_PORT` /
193
199
  `TRIBE_API_SCHEME` set the target; otherwise it is the scan host, on 8080 in a scan (or the port `probe_service`
@@ -203,7 +209,8 @@ not check a website's 80/443.
203
209
  - **DKIM:** Checks for DKIM selector records
204
210
  - **DMARC:** Validates DMARC policy (reject/quarantine/none)
205
211
  - **DNSSEC:** Checks for DNSSEC signing and validation chain
206
- - **Zone Transfer (AXFR):** Tests if zone transfer is allowed (security risk)
212
+ - **Zone Transfer (AXFR):** opt-in (`DNS_AUDIT_AXFR_CHECK=true`, off by default) — tries a transfer of the scanned
213
+ domain at each nameserver's own address, outside the scan's host check; records only an open transfer
207
214
  - **MX:** Flags a missing MX, a null MX, and an MX that points at a CNAME or an IP address — no STARTTLS / TLS check
208
215
  - **CAA Records:** Checks Certificate Authority Authorization records
209
216
 
@@ -260,7 +267,7 @@ not check a website's 80/443.
260
267
  | ID | Name | Tier | Purpose | SOC 2 Controls |
261
268
  |----|------|------|---------|----------------|
262
269
  | 1020 | AWS Cloud Scanner | Enterprise | S3 bucket hardening (PAB, encryption, versioning, Object Lock, MFA Delete) + object-access audit trail as a JOIN of server access logging AND CloudTrail S3 data-event coverage — a bucket a trail records is not a gap; any uncertain answer keeps the finding | CC7.1 / C1.1 / C1.2 / A1.2 |
263
- | 1021 | GCP Cloud Scanner | Enterprise | Firewall rule audit + IAM bindings + Storage bucket public-access | CC6.1 / CC6.6 / C1.1 |
270
+ | 1021 | GCP Cloud Scanner | Enterprise | Firewall rule audit + IAM bindings + Storage bucket public-access + Compute image inventory (feeds CIS v8 4.1 / 4.2 / 4.6 only) | CC6.1 / CC6.6 / C1.1 |
264
271
  | 1022 | Azure Cloud Scanner | Enterprise | NSG rule analysis + RBAC role assignments + Storage account hardening | CC6.1 / CC6.3 / CC6.6 / C1.1 / A1.2 |
265
272
  | 1023 | Zero Trust Checker | Enterprise | Segmentation + encryption + identity + lateral movement scoring (reads OBSERVED open ports) | CC6.1 / CC6.2 / CC6.6 / CC6.7 |
266
273
  | 1024 | GCP Cloud Storage Auditor (EE 0.6.8) | Enterprise | Multi-cloud parity sister of plugin 1020 AWS S3. 6 dimensions: bucket-level IAM public bindings (allUsers = CRITICAL, allAuthenticatedUsers = HIGH); Uniform Bucket-Level Access enforcement (closes legacy bucket-ACL false-PASS class); Object Versioning; Bucket Lock retention policy (SEC 17a-4 / FINRA 4511 WORM-alignment); CMEK via Cloud KMS (four-tier custody ladder); bucket-level access logging. NEW SDK dep `@google-cloud/storage`. | CC6.1 / CC6.6 / CC7.1 / C1.1 / C1.2 / A1.2 |
@@ -286,7 +293,7 @@ not check a website's 80/443.
286
293
  | 1210 | AWS EC2 Instance Auditor (**NEW EE 0.13.1** — first new plugin since 1200; plugin count 24 → 25; the AWS producer for CIS-Hardened-Image detection + EC2 instance-level + EBS-encryption coverage) | Enterprise | Audits EC2 instances (orthogonal to plugin 1170 which audits the SG perimeter policy). **Multi-region** via ec2:DescribeRegions (single-region fallback emits an evidence-gap). Dimensions: **IMDSv1 enabled** (CC6.1 — MEDIUM when an IAM instance profile is attached / LOW without; **IMDSv2 hop-limit > 1** re-opens container credential theft) + **EBS volume unencrypted** (C1.1 + CIS 3.11 — HIGH; resolves every attached BlockDeviceMappings volume via DescribeVolumes) + **account default-EBS-encryption disabled** (C1.1 preventive — GetEbsEncryptionByDefault) + **public-IP exposure** incl. secondary-ENI/EIP + IPv6 GUA (CC6.6 substrate, INFO) + **instance-store (ephemeral) volume** evidence-gap. **DIM 4 — AMI inventory** → result.cisImageInventory (the producer feed that makes CIS-Hardened-Image detection LIVE on Safeguards 4.1/4.2/4.6; Azure 1022 + GCP 1021 feed the same contract). Conservative classifier: LOW + evidenceGap on indeterminate metadata; AccessDenied → INFO + evidenceGap (never silent-PASS); terminated/shutting-down instances skipped. Survived 3 review rounds / 5 adversarial skill lenses (network-security-audit + CIS + IAM-effective-permissions + soc2-evidence + cloud-plugin-false-negatives). `@aws-sdk/client-ec2` reused. | CC6.1 / C1.1 / CC6.6 |
287
294
  | 1220 | Azure Storage Account Data-Protection Auditor (**NEW EE 0.13.2** — Move C-2; **+2 dims EE 0.13.3** — Move C-2.1; plugin count 25 → 26; first dedicated Azure auditor beyond the multi-purpose 1022 scanner) | Enterprise | Audits the Azure Storage Account data-protection surface across **7 dims** — **orthogonal to plugin 1022** which owns the network-exposure dims (no double-emission; mirrors the AWS 1020 S3-scanner + 1120 S3-lifecycle two-plugin split). (1) **HTTPS-only transit** (`enableHttpsTrafficOnly` — HIGH on plaintext HTTP; CC6.7) + (2) **minimum TLS version** (< TLS1_2 → MEDIUM; CC6.7) + (3) **Shared Key authorization** (`allowSharedKeyAccess` — bypasses Azure AD identity; CC6.1; Azure field-default discipline: absent = ENABLED, never silent-PASS) + (4) **infrastructure (double) encryption** (`requireInfrastructureEncryption`; C1.1) + (5) **encryption key source incl. CMK reachability + rotation** (`encryption.keyVaultProperties` — keySource=Microsoft.Keyvault PASS only when the key is currently resolvable + auto-rotating; a disabled/revoked/version-pinned CMK degrades, not silent-PASS; C1.1) + (6) **blob recoverability** (soft-delete + versioning via the secondary `blobServices.getServiceProperties` path — A1.2; EE 0.13.3) + (7) **per-container anonymous public access** (account-toggle-aware via `blobContainers.list` — `publicAccess=Blob/Container` + allowBlobPublicAccess=true → HIGH effective exposure; C1.1; EE 0.13.3). Conservative classifier: indeterminate field / AccessDenied / secondary-path failure → evidence-gap (never silent-PASS); `for await` pagination. Single-subscription scope surfaced explicitly. Built + reviewed through the cloud-plugin-false-negatives lens. `@azure/arm-storage` + `@azure/identity`. | CC6.7 / CC6.1 / C1.1 / A1.2 |
288
295
  | 1221 | Azure NSG Perimeter Auditor (**NEW EE 0.14.0** — Move C-2.2; **UDP lane EE 0.14.1**; plugin count 26 → 27; the Azure analog of AWS plugin 1170) | Enterprise | A CC6.6 network-segmentation perimeter auditor for Azure Network Security Groups — **non-overlapping-by-depth with plugin 1022's flat per-rule NSG lint** (no double-emission of a verdict; mirrors the AWS 1023-observed / 1170-declared two-plugin precedent). Evaluates each NSG's inbound rules in **Azure priority order** (first match wins; DenyAllInbound default): (1) **all-protocol (`*`) public Allow** — every port reachable from the public internet + (2) **public-source (`*`/`0.0.0.0/0`/`Internet`) to a restricted TCP port** (SSH/RDP/MSSQL/MySQL/Postgres/Redis/Memcached/MongoDB/Elasticsearch/CouchDB/SMB/WinRM/Oracle/Docker/Kubelet) + (3) **`::/0` IPv6-wildcard to a restricted port** (the dimension 1022's flat lint misses) + **(2u/3u) public-source / `::/0` to a restricted UDP service** (DNS/TFTP/rpcbind/NTP/NetBIOS/SNMP/CLDAP/IKE/Syslog/IPMI/OpenVPN/MSSQL-Monitor/SSDP/IPsec-NAT-T/Memcached — closes the public-UDP false negative, **EE 0.14.1**) + (4) **public→non-restricted port** INFO substrate + (5) **PASS substrate** when no public restricted exposure. **Attachment-aware severity** via the read-only `nsg.subnets[]`/`networkInterfaces[]` back-references — attached → CRITICAL (effective exposure); orphaned → MEDIUM (latent). Per-transport effective priority/deny-override resolution + port-range expansion + `0.0.0.0/1` split-range coverage + service-tag/ASG-source normalization (VirtualNetwork/AzureLoadBalancer/ASG = not public). Conservative classifier: denied/indeterminate listAll → evidence-gap; one malformed NSG degrades per-resource (never aborts the scan); `for await` pagination; single-subscription scope explicit. Built + reviewed through the cloud-plugin-false-negatives lens (EE 0.14.0 SHIP-WITH-FOLDS 2 LOW; EE 0.14.1 UDP lane SHIP-WITH-FOLDS 2 MED + 1 LOW, all folded). `@azure/arm-network` + `@azure/identity`. | CC6.6 |
289
- | 1222 | Azure Key Vault Deep Auditor (**NEW EE 0.15.0** — Move C-2.3; plugin count 27 → 28; the third dedicated Azure auditor after 1220 storage + 1221 NSG; **hotfixed EE 0.15.1** — Dim-3 SDK-shape + Dim-4 inherited-admin severity re-tune) | Enterprise | The deep Key Vault auditor — the KV analog of how 1221 deepens 1022's flat NSG dim; **orthogonal to plugin 1022's vault-property dims** (purge/soft-delete/network-ACL/RBAC-mode) — no double-emission of a verdict. Enumerates each vault's **keys, role assignments, and diagnostic settings** across 4 dims: (1) key **auto-rotation policy** — keys with no `rotationPolicy` / no rotate-action → finding + (2) key **expiry** — keys with no `expires` (or already-expired), with epoch-s/ms/Date/string coercion (closes the ms-overflow false-clean) + (3) **diagnostic logging → Log Analytics** — vaults with no `AuditEvent`→workspace diagnostic setting; reads `@azure/arm-monitor`'s `diagnosticSettings.list()` correctly as an `await`-ed `{value:[]}` collection object (NOT a paged async-iterator — the EE 0.15.1 H-1 fix; pre-fix the dim always threw and degraded to a non-functional evidence-gap), soft-degrades to an evidence-gap if the SDK is absent + (4) **privileged-access depth** — RBAC `roleAssignments` (admin/data-plane/**scope-aware severity**: vault-scoped control-plane god roles + Key Vault Administrator at any scope → HIGH; inherited subscription/management-group-scope Owner/User-Access-Administrator → MEDIUM, inherited Contributor → LOW — the EE 0.15.1 H-2 re-tune that stops flagging the ubiquitous inherited-admin reality as HIGH on every RBAC vault) + legacy `accessPolicies` breadth (export + wide-crypto permissions). **Secret/cert expiry is a deliberate data-plane scope boundary** (explicit caveat — the control-plane mgmt API does not expose data-plane object expiry). Conservative classifier: indeterminate field / AccessDenied / arm-monitor absent → evidence-gap; one malformed vault degrades per-resource (never aborts the scan); single-subscription scope explicit. Findings route across every shipped framework (SOC 2 CC6.3/C1.1/CC6.1/CC7.2 · HIPAA §164.312(a)(2)(iv)/(b)/(a)(1) · NIST CSF PR.DS-01/DE.CM-09/PR.AA-05 · PCI DSS 3.5.1/10.2.1/7.2.1 · ISO 27001 A.8.24/A.8.15/A.5.15+A.8.2 · CIS v8 3.11/8.2/5.4 · GDPR Art.32(1)(b)-confidentiality + Art.32(1)(b)-integrity · NIST SP 800-171 3.1.2/3.1.4/3.1.5/3.1.7/3.3.1/3.5.3/3.13.10) — every coverage matrix UNCHANGED at that release. Built + reviewed through the cloud-plugin-false-negatives lens (SHIP-WITH-FOLDS 0C/3H/2M/4L; 4 folded). `@azure/arm-keyvault` + `@azure/arm-monitor` + `@azure/arm-authorization` + `@azure/identity`. | CC6.3 / C1.1 / CC6.1 / CC7.2 |
296
+ | 1222 | Azure Key Vault Deep Auditor (**NEW EE 0.15.0** — Move C-2.3; plugin count 27 → 28; the third dedicated Azure auditor after 1220 storage + 1221 NSG; **hotfixed EE 0.15.1** — Dim-3 SDK-shape + Dim-4 inherited-admin severity re-tune) | Enterprise | The deep Key Vault auditor — the KV analog of how 1221 deepens 1022's flat NSG dim; **orthogonal to plugin 1022's vault-property dims** (purge/soft-delete/network-ACL/RBAC-mode) — no double-emission of a verdict. Enumerates each vault's **keys, role assignments, and diagnostic settings** across 4 dims: (1) key **auto-rotation policy** — keys with no `rotationPolicy` / no rotate-action → finding + (2) key **expiry** — keys with no `expires` (or already-expired), with epoch-s/ms/Date/string coercion (closes the ms-overflow false-clean) + (3) **diagnostic logging → Log Analytics** — vaults with no `AuditEvent`→workspace diagnostic setting; reads `@azure/arm-monitor` 8's `diagnosticSettings.list()` by iterating every page of its paged async-iterator (EE 2.1.0 — monitor 7 returned an `await`-ed `{value:[]}` collection, which the EE 0.15.1 H-1 fix read; a return of any other shape is an evidence gap, never an empty list), soft-degrades to an evidence-gap if the SDK cannot be loaded + (4) **privileged-access depth** — RBAC `roleAssignments` (admin/data-plane/**scope-aware severity**: vault-scoped control-plane god roles + Key Vault Administrator at any scope → HIGH; inherited subscription/management-group-scope Owner/User-Access-Administrator → MEDIUM, inherited Contributor → LOW — the EE 0.15.1 H-2 re-tune that stops flagging the ubiquitous inherited-admin reality as HIGH on every RBAC vault) + legacy `accessPolicies` breadth (export + wide-crypto permissions). **Secret/cert expiry is a deliberate data-plane scope boundary** (explicit caveat — the control-plane mgmt API does not expose data-plane object expiry). Conservative classifier: indeterminate field / AccessDenied / arm-monitor absent → evidence-gap; one malformed vault degrades per-resource (never aborts the scan); single-subscription scope explicit. Findings route across every shipped framework (SOC 2 CC6.3/C1.1/CC6.1/CC7.2 · HIPAA §164.312(a)(2)(iv)/(b)/(a)(1) · NIST CSF PR.DS-01/DE.CM-09/PR.AA-05 · PCI DSS 3.5.1/10.2.1/7.2.1 · ISO 27001 A.8.24/A.8.15/A.5.15+A.8.2 · CIS v8 3.11/8.2/5.4 · GDPR Art.32(1)(b)-confidentiality + Art.32(1)(b)-integrity · NIST SP 800-171 3.1.2/3.1.4/3.1.5/3.1.7/3.3.1/3.5.3/3.13.10) — every coverage matrix UNCHANGED at that release. Built + reviewed through the cloud-plugin-false-negatives lens (SHIP-WITH-FOLDS 0C/3H/2M/4L; 4 folded). `@azure/arm-keyvault` + `@azure/arm-monitor` + `@azure/arm-authorization` + `@azure/identity`. | CC6.3 / C1.1 / CC6.1 / CC7.2 |
290
297
  | 1230 | AWS DocumentDB Auditor (**NEW EE 0.41.0** — plugin count 28 → 29; owns the `docdb` engine on the shared RDS control plane, closing the measured false clean where an unencrypted DocumentDB cluster read CLEAN under the RDS auditor's Aurora-only cluster filter) | Enterprise | Seven dimensions: storage encryption at rest + KMS custody classification (customer-alias PASS / AWS-managed MEDIUM / key-UUID and foreign-account custody stay evidence gaps); `tls` cluster parameter (disabled = HIGH); `audit_logs` parameter + CloudWatch `audit` export (generating-but-unexported is its own MEDIUM); automated backup retention vs the 7-day floor; deletion protection; replica/AZ availability topology under the CLUSTER lens (never the per-instance Multi-AZ flag — that is RDS semantics and does not apply); manual-snapshot `restore`-attribute probes (`all` = publicly restorable CRITICAL; external account ids = HIGH). Non-docdb engines counted and skipped; DocumentDB Elastic clusters declared deferred. Paired 1140 change: docdb/neptune engine-filtered with counted skips + a standing disclosure — a Neptune estate is explicitly UNAUDITED. | CC6.1 / CC6.6 / CC7.2 / C1.1 / A1.2 |
291
298
 
292
299
  ---
@@ -74,7 +74,8 @@ Complete data structures for all MCP tool inputs and outputs.
74
74
  }
75
75
  ],
76
76
  "pluginsRan": 1,
77
- "markdown": "# NSAuditor AI Scan Report …"
77
+ "markdown": "# NSAuditor AI Scan Report …",
78
+ "callWall": { "limitMs": 50000, "reached": false, "notStarted": [], "cutShort": [] }
78
79
  }
79
80
  ```
80
81
 
@@ -85,6 +86,11 @@ Complete data structures for all MCP tool inputs and outputs.
85
86
  `error`. A `timeout` or `error` means that surface was **NOT measured** — never read it as clean.
86
87
  - `pluginsRan` is the number of `manifest[]` entries whose status is `ran` — in this example, one of the two entries
87
88
  shown. It counts plugins, never ports or results.
89
+ - `callWall` is on every response: the call's total time limit (`SCAN_HOST_WALL_MS`, `limitMs` 0 when off) and
90
+ whether it was `reached`. When it was, `notStarted` (plugins the limit reached before they started) and `cutShort`
91
+ (plugins stopped at it) list, in the order they would have run, plugins whose `manifest[]` entries read `timeout`;
92
+ a `note` field and the head of `markdown` say the scan is partial. Never read such a result as the host's whole
93
+ posture.
88
94
  - There is **no `findings` array** in a `scan_host` result, and no `techniques`: the CLI adds ATT&CK
89
95
  techniques to its own report, and this tool does not.
90
96
 
@@ -113,6 +119,7 @@ interface ServiceRecord {
113
119
  anonymousLogin?: boolean | null; // FTP anonymous login detected — tested only with FTP_CHECK_ANON=true; null = NOT TESTED
114
120
  anonymousLoginTested?: true | string | null; // true when measured, else why not: "opt-in-off" · "no-answer"
115
121
  axfrAllowed?: boolean | null; // DNS zone transfer allowed — tested only with DNS_CHECK_AXFR=true + DNS_AXFR_DOMAIN; null = NOT TESTED
122
+ // (060's own attempt, DNS_AUDIT_AXFR_CHECK, reports as an axfr_open entry of dnsSecurity, not here)
116
123
  axfrTested?: true | string | null; // true when measured, else why not: "opt-in-off" · "no-domain" · "no-answer"
117
124
  community?: string | null; // SNMP DEFAULT community accepted: "public" or "private"; a custom SNMP_COMMUNITY string is never recorded
118
125
  communityCustom?: boolean; // true when a custom SNMP_COMMUNITY string answered — not a finding
@@ -396,8 +403,8 @@ The file carries one `note` result per service (an open port is inventory) plus
396
403
  finding — the same findings, at the same grades, that `--fail-on` and the Markdown report read
397
404
  (`references/workflows.md` §4), the TLS, SNMP, MCP, certificate, SMB null-session and 040 / 050 / 060 audit results
398
405
  included — and no CVE or analysis-agent results. Anonymous FTP, zone transfer and the SMB null session appear only when
399
- `FTP_CHECK_ANON` / `DNS_CHECK_AXFR` / `SMB_NULL_SESSION` enabled those checks. The rule ids of the four flags SARIF
400
- graded before Community 0.2.57 are unchanged.
406
+ `FTP_CHECK_ANON` / `DNS_CHECK_AXFR` (or `DNS_AUDIT_AXFR_CHECK`, for 060's attempt) / `SMB_NULL_SESSION` enabled those
407
+ checks. The rule ids of the four flags SARIF graded before Community 0.2.57 are unchanged.
401
408
 
402
409
  #### SARIF Severity Mapping
403
410
 
@@ -418,7 +425,8 @@ Compatible with: GitHub Advanced Security, Azure DevOps, SonarQube, and other SA
418
425
 
419
426
  When `OPENAI_REDACT=true` (default), the CLI's AI stage (`AI_ENABLED=true`) redacts the scan payload it sends —
420
427
  `host`, `host_os_hint`, `summary`, `services`, `evidence` — with Community's built-in redactor, at every tier
421
- (the Pro-tier external-redactor hook is installed by neither package):
428
+ (from Community 1.0.0 there is no external-redactor hook at all and `_meta.redactor` reads `builtin`; earlier releases
429
+ carried a Pro-tier hook that neither package installed):
422
430
 
423
431
  | Data | Becomes |
424
432
  |------|---------|
@@ -431,12 +439,14 @@ When `OPENAI_REDACT=true` (default), the CLI's AI stage (`AI_ENABLED=true`) reda
431
439
  | MAC addresses written with colons (`aa:bb:cc:dd:ee:ff`); hyphen and dotted forms pass unchanged | `[MAC]` |
432
440
  | Serial numbers (text `Serial:` / `Serial=`; keys `serial`, `serialNumber`, `sn`, in any case) | `[REDACTED_HIDDEN]` |
433
441
  | Values under keys containing a `CONFIDENTIAL_KEYWORDS` entry (unset by default: no key scrub) | `[REDACTED_HIDDEN]` |
442
+ | An AWS account ID in an ARN's account field or an `amazonaws.com/<id>/…` URL — there and everywhere else it occurs, inside resource names too (Community 1.0.0+) | `[ACCOUNT_ID]` |
443
+ | Any other 12-digit number (Community 1.0.0+) | `[REDACTED_ID]` |
434
444
 
435
445
  Keys dropped entirely: `ip6`, `deviceWebPage`, `deviceWebPageInstruction`, `hardwareVersion`, `firmwareVersion`.
436
446
 
437
447
  ⚠️ **NOT redacted in that payload:** email addresses, internal hostnames (`.local`, `.corp`, `.internal`), the SNMP
438
- `community` field, Bearer tokens, AWS access keys and file paths — nor ports, service names, versions, or banner and
439
- evidence prose beyond the rows above. `OPENAI_REDACT` has no strict level. `CONFIDENTIAL_KEYWORDS` works by KEY
448
+ `community` field, Bearer tokens, AWS access-key IDs, Azure subscription IDs, cloud resource names themselves and file
449
+ paths — nor ports, service names, versions, or banner and evidence prose beyond the rows above. `OPENAI_REDACT` has no strict level. `CONFIDENTIAL_KEYWORDS` works by KEY
440
450
  (`community` blanks the `community` field, not the same value inside banner or evidence text). With Ollama at its
441
451
  default localhost URL the payload does not leave the host.
442
452
 
@@ -131,7 +131,7 @@ grade a finding differently:
131
131
 
132
132
  | Finding in the scan's conclusion | `--fail-on` grade | SARIF level |
133
133
  |---|---|---|
134
- | anonymous FTP login · DNS zone transfer — tested only with `FTP_CHECK_ANON=true` / `DNS_CHECK_AXFR=true` + `DNS_AXFR_DOMAIN`, both off by default | critical | error |
134
+ | anonymous FTP login · DNS zone transfer — tested only with `FTP_CHECK_ANON=true` / `DNS_CHECK_AXFR=true` + `DNS_AXFR_DOMAIN` (060's attempt against the domain's nameservers: `DNS_AUDIT_AXFR_CHECK=true`), all off by default | critical | error |
135
135
  | MCP server reachable without authentication · MCP server lists its tools without authentication | critical | error |
136
136
  | MCP server over cleartext HTTP · MCP server on a deprecated protocol version | high | error |
137
137
  | SNMP default community (`public` / `private`) | high | error |
@@ -151,7 +151,8 @@ clean host.
151
151
 
152
152
  ```bash
153
153
  # Scan with SARIF output and severity gating. --fail-on high exits 1 on any graded finding at high or above (table
154
- # above); anonymous FTP and zone transfer are graded only with FTP_CHECK_ANON=true / DNS_CHECK_AXFR=true + DNS_AXFR_DOMAIN.
154
+ # above); anonymous FTP and zone transfer are graded only with FTP_CHECK_ANON=true / DNS_CHECK_AXFR=true + DNS_AXFR_DOMAIN
155
+ # (060's attempt against the domain's nameservers: DNS_AUDIT_AXFR_CHECK=true).
155
156
  nsauditor-ai scan --host $TARGET \
156
157
  --plugins all \
157
158
  --output-format sarif \
@@ -173,6 +174,7 @@ SARIF file.
173
174
  ```yaml
174
175
  - name: Security Scan
175
176
  env: # both checks are off by default; without these --fail-on high never sees anonymous FTP or a zone transfer
177
+ # (DNS_AUDIT_AXFR_CHECK, also off by default, is 060's own attempt against the scanned domain's nameservers)
176
178
  FTP_CHECK_ANON: "true"
177
179
  DNS_CHECK_AXFR: "true"
178
180
  DNS_AXFR_DOMAIN: example.com # the zone to try transferring
@@ -223,18 +225,21 @@ NSA_ALLOW_ALL_HOSTS=1 nsauditor-ai scan --host 192.168.1.0/24 --plugins all \
223
225
  `--alert-every-cycle` alerts every host carrying such a finding on every cycle, the first included. Stdout prints
224
226
  each host's change under `=== Delta Report ===`, and `No significant changes detected.` only when no host changed.
225
227
  - The findings are the service-check findings `--fail-on` and the reports grade (§4): anonymous FTP login or a DNS
226
- zone transfer (critical; both off unless `FTP_CHECK_ANON=true` / `DNS_CHECK_AXFR=true` + `DNS_AXFR_DOMAIN` are
227
- set), the MCP server checks, an SNMP default community, an expired certificate, an SMB null session (off unless
228
- `SMB_NULL_SESSION=true`), weak SSH algorithms and weak TLS protocols / ciphers, dangerous HTTP methods where an Allow
229
- header was read, a self-signed certificate, and each 040 / 050 / 060 audit entry at its own grade. Enterprise's CVE
230
- rows and analysis-agent findings are not among them, so they never trigger an alert.
228
+ zone transfer (critical; off unless `FTP_CHECK_ANON=true` / `DNS_CHECK_AXFR=true` + `DNS_AXFR_DOMAIN` — or
229
+ `DNS_AUDIT_AXFR_CHECK=true` for 060's attempt — are set), the MCP server checks, an SNMP default community, an
230
+ expired certificate, an SMB null session (off unless `SMB_NULL_SESSION=true`), weak SSH algorithms and weak TLS
231
+ protocols / ciphers, dangerous HTTP methods where an Allow header was read, a self-signed certificate, and each 040 /
232
+ 050 / 060 audit entry at its own grade. Enterprise's CVE rows and analysis-agent findings are not among them, so they
233
+ never trigger an alert.
231
234
  - A host whose scan failed, or whose summary is missing on either side, is reported not comparable on stdout — never
232
235
  "No changes", never "removed" — and does not alert in this release.
233
236
  - Watch mode scans each distinct host once per cycle: a repeated `--host` entry, or one also inside a listed CIDR, is
234
237
  dropped, and the banner says how many.
235
- - The payload is not redacted: the host and its findings go in the clear to the URL you supply.
238
+ - The payload is not redacted. Under the default `--alert-payload full`, the host and its findings go in the clear to the
239
+ URL you supply; `--alert-payload minimal` (Community 1.0.0) drops the host, the service and the description, and sends
240
+ a count, the alert severity and each finding's port, protocol and severity.
236
241
 
237
- **Webhook Payload** (the change itself is NOT in it: `details` carries one entry per finding at or above
242
+ **Webhook Payload**, the default `full` (the change itself is NOT in it: `details` carries one entry per finding at or above
238
243
  `--alert-severity` — `description` is the finding's title and `severity` its own grade, lower-case; a finding with no
239
244
  port, such as a domain's DNS-security entry that landed in the conclusion's evidence, carries `port` and `protocol`
240
245
  null. The top-level `severity` is the `--alert-severity` value):
@@ -283,6 +288,7 @@ AI_ENABLED=true AI_PROVIDER=openai OPENAI_API_KEY=sk-... OPENAI_REDACT=true NSA_
283
288
  | `scan_conclusion_raw.json` | Full unredacted data (admin reference) |
284
289
  | `scan_conclusion_raw.html` | Admin HTML dashboard with filters |
285
290
  | `scan_response_ai_payload.json` | Redacted payload sent to AI |
291
+ | `scan_response_ai_system_prompt.txt` | System prompt sent to AI, the Pro/Enterprise intelligence block included (from Community 1.0.0) |
286
292
  | `scan_response_ai.html` | Styled HTML report with CVE links, severity badges |
287
293
  | `scan_response_ai.txt` | AI vulnerability assessment in markdown |
288
294
  | `scan_run_<runId>.json` | **Per-run scan record** — written at the ROOT of `--out` (one level above the per-host folders) at run start, appended per host, finalized at the end. It is what `report` is built from, and what lets a report cover STATE its coverage (hosts requested / written / reachable) instead of implying it. On the free tier these are pruned after a retention window. |
@@ -322,7 +328,7 @@ port or probe not measured, a CVE lookup that failed, an agent that did not run,
322
328
  changing under the same program and version — as RESOLVED. `report --since` reads each of those as
323
329
  NOT COMPARABLE with its reason, refuses the comparison outright when either run's chain is altered or cannot be
324
330
  measured (naming which side), and says what it did not evaluate.
325
- A finding on a port, region or producer a scan did not measure is not counted as fixed, and with SLA tracking on the control it failed is held FAILED — including the prior CVE rows on a service whose lookup failed, the CVE mapper's and the service agent's rows on a TCP port whose service the scan could not identify, and an analysis agent's or the CVE mapper's rows when a plugin they read was left out of the scan or did not complete. Two measured limits: a scan made before EE 1.3.0 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 — the report's Basis cell says so on the row; and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the 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 can read PASS — include the port scanner (003).
331
+ A finding on a port, region or producer a scan did not measure is not counted as fixed, and with SLA tracking on the control it failed is held FAILED — including the prior CVE rows on a service whose lookup failed, the CVE mapper's and the service agent's rows on a TCP port whose service the scan could not identify, and an analysis agent's or the CVE mapper's rows when a plugin they read was left out of the scan or did not complete. Two measured limits: a scan made before EE 1.3.0 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 — the report's Basis cell says so on the row; and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the 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 can read PASS — include the port scanner (003). An Enterprise 2.0.0 limit beside these: a TLS check whose handshake failed in transport on a port the port scanner saw open is NOT COMPARABLE (Community 1.0.0 records the error class), except when another port of the same scan completed a TLS handshake: then the earlier TLS finding on the failed port can still read RESOLVED.
326
332
 
327
333
  Read from its output:
328
334
  - New findings (unexpected exposure) — it compares FINDINGS, not services: a newly exposed service appears only if