nsauditor-ai-agent-skill 0.2.54 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.54** (2026-10-04) — paired with **Enterprise 1.2.0 / Community 0.2.56**. ⚠️ **A floor bump: requires Community >= 0.2.56** (it was >= 0.2.55 — Enterprise 1.2.0 imports names that first ship in Community 0.2.56; below it Enterprise's plugins load, but the scan skips its intelligence, analysis-agent and compliance stages without a word). New in this release's teaching: the not-comparable bucket also catches a UDP port that did not answer, a CVE lookup that failed, an analysis agent that did not run, and a CVE the vulnerability data stopped attributing while the same program and version still answered; a compliance control can FAIL on a finding the prior scan recorded when this scan did not measure the port or probe it was on, cover its region, or run what produced it — a `<prior title> — [COVERAGE GAP] …` record, never a current finding; how a failed Enterprise load surfaces from Community 0.2.56; and two measured limits — after a CVE lookup that failed, the control its CVE rows failed can still read PASS; and when a later scan left out a plugin the earlier one requested, a row an analysis agent or the CVE mapper derived from it can read fixed, and its control PASS; when the earlier scan left the plugin out instead, such a row can read NEW. Keep `--plugins` identical between compared scans. Plugin counts and all eight coverage matrices are unchanged since Enterprise 1.1.0. The 0.2.53 teaching is carried forward.
16
+ **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.
17
+
18
+ **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).
19
+
20
+ **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.
17
21
 
18
22
  **Prior: 0.2.53** — paired with **Enterprise 1.1.0 / Community 0.2.55**. ⚠️ **A floor bump: requires Community >= 0.2.55** (it was >= 0.2.49 — Enterprise 1.1.0 calls code that first ships in Community 0.2.55). The skill now INSTALLS itself: `npx nsauditor-ai-agent-skill install` copies it into `~/.claude/skills/` for Claude Code, and `npx nsauditor-ai-agent-skill build-zip --out ~/Desktop` builds the Claude Desktop upload zip. Claude Code users can instead add it as a plugin marketplace — `/plugin marketplace add nsasoft/nsauditor-ai-agent-skill`. ⚠️ A marketplace reaches **Claude Code only**; Claude Desktop accepts skills only as an uploaded zip, and skills do not sync between surfaces. Knowledge updated for the Pro cross-run delta (`report --since`) that ships in Community 0.2.55.
19
23
 
@@ -168,7 +172,7 @@ When an AI agent loads this skill, it gains:
168
172
  | **Schema knowledge** | Complete data structures for parsing and presenting results |
169
173
  | **CPE construction** | How to map detected services to NVD vulnerability lookups |
170
174
  | **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 |
171
- | **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. |
175
+ | **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. |
172
176
  | **Security rules** | ZDE, SSRF protection, redaction, scan authorization requirements |
173
177
  | **Error handling** | License gates, SSRF blocks, timeout resolution, CPE format errors |
174
178
  | **Decision routing** | When to use scan_host vs probe_service vs CLI vs get_vulnerabilities |
@@ -199,7 +203,7 @@ This package provides **knowledge about** NSAuditor AI. To actually **run** scan
199
203
  }
200
204
  }
201
205
  ```
202
- `NSA_ALLOW_ALL_HOSTS: "1"` lets Claude scan your own network — private addresses such as `192.168.x.x` or `10.x.x.x` — because it turns off the MCP server's check of the address a host name resolves to. The server still refuses a host written as `localhost`, `127.x.x.x`, `::1`, `169.254.x.x` or `metadata.google.internal`, but no longer a name that resolves to a loopback or cloud-metadata address, or a spelling such as `::ffff:127.0.0.1`. `install-key` does not print it, so add it yourself, and remove it if you only scan public hosts.
206
+ `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.
203
207
 
204
208
  > ⚠️ **`NSA_MCP_AUTH_KEY` is REQUIRED — the server refuses to start without it.** Generate one with
205
209
  > `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.54<!-- /nsa:derived --> (knowledge current as of **EE <!-- nsa:derived id="ee-version" -->1.2.0<!-- /nsa:derived -->** · **requires CE ≥ <!-- nsa:derived id="ce-floor" -->0.2.56<!-- /nsa:derived -->**) — **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.** 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, or did not run what produced it, 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`), 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.56, because Enterprise's index would not LOAD below it.** Enterprise 1.2.0 imports names that first ship in Community 0.2.56. On Community 0.2.55 or older that load failure is SILENT — 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; from 0.2.56 it is named on stderr (`[EE] Enterprise is installed but FAILED TO LOAD`) and the Pro delta refuses that host's agent and CVE-mapper findings as `evidence-gap`. 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. 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.2.0 REQUIRES 0.2.56 (item (c) above): on 0.2.55 Enterprise 1.2.0's plugins load, but the scan skips its intelligence, analysis-agent and compliance stages without a word.** 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.1.0<!-- /nsa:derived -->; Community <!-- nsa:derived id="ce-version" -->0.2.56<!-- /nsa:derived --> (published with this skill).
18
+ > **Version:** <!-- nsa:derived id="skill-version" -->1.0.0<!-- /nsa:derived --> (knowledge current as of **EE <!-- nsa:derived id="ee-version" -->2.0.0<!-- /nsa:derived -->** · **requires CE ≥ <!-- nsa:derived id="ce-floor" -->1.0.0<!-- /nsa:derived -->**) — **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.** Teach these first: 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. (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 Community 1.0.0.** Enterprise 2.0.0 imports `redactUrlCredentials`, which first ships in Community 1.0.0, and its supported runtime is Node.js 24. 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) — so the scan skips its intelligence, analysis-agent and compliance stages: on Community 0.2.56 and later 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 and 2.0.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) **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 1.3.0<!-- /nsa:derived -->; Community <!-- nsa:derived id="ce-version" -->1.0.0<!-- /nsa:derived --> (published with this skill).
19
+ >
20
+ > **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).
21
+ >
22
+ > **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.
19
23
  >
20
24
  > **Prior: 0.2.53** (post-EE-1.1.0 · **requires CE ≥ 0.2.55**) — the delta release: `report --since` compares two runs, the NOT-COMPARABLE bucket carries a reason per finding, and integrity is a hash chain over the run record and each host's findings files. Its teaching is carried forward in the header above.
21
25
  >
@@ -173,30 +177,42 @@ reads `timeout` in the result's `manifest` — its surface was NOT measured, whi
173
177
  result.
174
178
 
175
179
  ⚠️ **What `scan_host` returns, and what it does NOT.** Services, the OS, and the flags the service checks leave on
176
- each service record: weak SSH algorithms, SNMP community, weak TLS protocols / ciphers and a self-signed certificate,
177
- and the MCP server checks — `mcpAnonymousAccess`, `mcpAnonymousToolList`, `mcpCleartextTransport`,
178
- `mcpDeprecatedProtocol`, `mcpInspectorExposed`. Anonymous FTP login and zone transfer appear only when the MCP server's
179
- environment enables those checks (`FTP_CHECK_ANON=true`; `DNS_CHECK_AXFR=true` with `DNS_AXFR_DOMAIN`) — both are off
180
- by default, so a missing `anonymousLogin` or a null `axfrAllowed` means NOT TESTED, never "not allowed". It RUNS, but
181
- does NOT return, the HTTP probe's dangerous-methods result (006), the NetBIOS/SMB scanner's null session and shares
182
- (014), the TLS-certificate, debug-endpoint and DNS-security auditors (040 / 050 / 060) — use `probe_service` (Pro)
183
- with that plugin for those — and, with the Enterprise package, the zero-trust assessment (1023). So no
184
- `dangerousMethods` on a service means NOT MEASURED. It does NOT look up CVEs and does NOT run the Enterprise
185
- analysis agents or exploit intelligence (those run in the CLI scan's enrichment). So `Security findings: 0` in its `markdown` is
186
- NOT a clean verdict and not a statement that the host has no known vulnerabilities — and that count does not include
187
- the MCP server flags either: read `mcp*` off each service record. Look CVEs up with `get_vulnerabilities` (Pro) on
180
+ each service record: weak SSH algorithms, an SNMP default community, weak TLS protocols / ciphers and a self-signed
181
+ certificate, and the MCP server checks — `mcpAnonymousAccess`, `mcpAnonymousToolList`, `mcpCleartextTransport`,
182
+ `mcpDeprecatedProtocol`, `mcpInspectorExposed`. Since Community 0.2.57 the records also carry the HTTP probe's methods
183
+ (006: `dangerousMethods`, with `methodsTested` false when no Allow header was read), the NetBIOS/SMB null-session check
184
+ (014: `nullSessionAllowed` and `shares`), the TLS-certificate audit (040: `certAudit`) and the debug-endpoint audit
185
+ (050: `tribeHealth`, only when TCP 8080 is open). For a domain-name target the DNS-security audit (060: `dnsSecurity`)
186
+ lands on the 53/udp record when the scan found a 53/udp service, otherwise in the conclusion's `evidence`; for an
187
+ IP-address target 060 DECLINES — its `manifest` entry reads `skipped` with the reason, the `markdown` says
188
+ `DNS-security audit (060) not tested`, and nothing lands — so DNS posture is unknown, never clean. Anonymous FTP
189
+ login, zone transfer and the SMB null session are tested only when the MCP server's environment enables them
190
+ (`FTP_CHECK_ANON=true`; `DNS_CHECK_AXFR=true` with `DNS_AXFR_DOMAIN`; `SMB_NULL_SESSION=true`) — all off by default.
191
+ 060's own zone-transfer attempt against the domain's nameservers runs only with `DNS_AUDIT_AXFR_CHECK=true`, also off
192
+ by default, and it records only an open transfer (`axfr_open` in `dnsSecurity`) — so its absence is never a measured
193
+ "closed". A null `anonymousLogin`, `axfrAllowed`, `nullSessionAllowed` or `dangerousMethods` means NOT TESTED, never
194
+ "not allowed" or "none": `anonymousLoginTested`, `axfrTested` and `nullSessionTested` are `true` when measured, else
195
+ the reason (`opt-in-off`, `no-domain`, `no-answer`). With the Enterprise package, the zero-trust assessment (1023) reaches
196
+ the conclusion only as one score line in its `evidence`, not its per-dimension findings. It does NOT look up CVEs and
197
+ does NOT run the Enterprise analysis agents or exploit intelligence (those run in the CLI scan's enrichment). So
198
+ `Security findings: 0` in its `markdown` is NOT a clean verdict and not a statement that the host has no known
199
+ vulnerabilities: that count is the service-check findings above — the MCP server flags and the 040 / 050 / 060 audit
200
+ entries included — and nothing else. Look CVEs up with `get_vulnerabilities` (Pro) on
188
201
  each service whose `cpe` names a concrete version (a `*` version is not a lookup for what is deployed). A service with
189
202
  a program and version but `cpe: null` — a program outside the scanner's CPE table; on the measured router, the
190
203
  firmware's HTTP server — gets NO lookup from either path: build a CPE for it (`references/workflows.md`), or report its
191
204
  CVE coverage as unknown, never as clean. With the Enterprise package and a Pro or Enterprise licence,
192
205
  `nsauditor-ai scan --host <host>` runs the CVE mapper and the agents, and joins exploit intelligence when a KEV / EPSS
193
- store is configured. Measured on a router: the Markdown `scan_host` uses, rendered over the CLI scan's own conclusion
194
- for that host, read `Security findings: 0` while that CLI scan carried 16 CVEs and 5 analysis-agent findings, and
195
- plugins 040 and 060 had recorded 2 and 3 HIGH findings that never reach `scan_host`'s response.
206
+ store is configured. Measured on a router in an earlier release: the Markdown `scan_host` uses, rendered over the CLI
207
+ scan's own conclusion for that host, read `Security findings: 0` while that CLI scan carried 16 CVEs and 5
208
+ analysis-agent findings, and plugins 040 and 060 had recorded 2 and 3 HIGH findings that did not reach `scan_host`'s
209
+ response then. Those audit entries land on the records now and are counted; the CVEs and the agent findings still are
210
+ not.
196
211
 
197
212
  **Returns:** `{ host, conclusion, manifest[], pluginsRan, markdown }` — `conclusion.result` is the fused
198
213
  record (`summary` is a one-line string; `host`, `services[]`, `evidence[]`), and `manifest[]`
199
- is every plugin's `{ id, name, status, reason, duration_ms }`. There is no `findings` array. See
214
+ is every plugin's `{ id, name, status, reason, duration_ms }`; `pluginsRan` is the number of `manifest[]` entries
215
+ whose status is `ran`. There is no `findings` array. See
200
216
  `references/schemas.md`.
201
217
 
202
218
  **Example:**
@@ -276,7 +292,7 @@ Run a single plugin against a specific host:port for deep-dive investigation.
276
292
  | 014 | NetBIOS/SMB Scanner | SMB/NetBIOS enumeration, null sessions |
277
293
  | 040 | TLS Certificate & Cipher Auditor | Certificate chain, expiry, weak ciphers |
278
294
  | 050 | TRIBE v2 Neural API Security Probe | Debug leaks, stack traces, CORS misconfig on a TRIBE v2 API |
279
- | 060 | DNS Security Auditor | SPF/DKIM/DMARC, DNSSEC, zone transfer |
295
+ | 060 | DNS Security Auditor | SPF/DKIM/DMARC, DNSSEC, zone transfer (opt-in: `DNS_AUDIT_AXFR_CHECK`) |
280
296
 
281
297
  ---
282
298
 
@@ -398,8 +414,11 @@ dispositions to know:
398
414
  a **partial** requirement; its rationale reads the finding as corroborating substrate that
399
415
  applies where the operator asserts the endpoint serves CUI-bearing sessions. (EE 0.32.7 cut
400
416
  the routing to SOC 2 alone; the 800-171 rule arrived with that framework in EE 0.40.0.)
401
- **In this release it does not fire:** the check reads a `headers` field that no shipped plugin sets on a
402
- service record, so the absence of this finding says nothing about whether HSTS is set.
417
+ **It fires on port 443 only, where the HTTP probe (006) received the HTTPS response:** since Community 0.2.57 that
418
+ probe keeps the `Strict-Transport-Security` header on its port-443 HTTPS record, and the CLI scan's crypto agent
419
+ (Enterprise package, Pro or Enterprise licence) reads it there. On any other port, over plain HTTP, or where no HTTPS
420
+ response arrived (a certificate the client rejects needs `--insecure-https`), the header is not read, so the absence
421
+ of this finding says nothing about whether HSTS is set there.
403
422
  - **Aggregate open-port count** and the **opportunistic-STARTTLS / port-inferred cleartext**
404
423
  variants route to **SOC 2 only** — each is a breadth heuristic or a self-declared
405
424
  *unverifiable* observation, not a per-transmission determination.
@@ -473,9 +492,10 @@ live total and marks each Enterprise plugin `✓ active` or `✗ requires: <tier
473
492
 
474
493
  The Community set groups roughly as service probes, host/network discovery, and intelligence /
475
494
  meta plugins, plus three deep-audit Community plugins (040 TLS Certificate & Cipher Auditor, 050
476
- TRIBE v2 Neural API Security Probe, 060 DNS Security Auditor): `scan_host` RUNS them on every tier (050 only when
477
- TCP 8080 is open) but does NOT return their findings, so over MCP the only route to their results is
478
- `probe_service`, which needs Pro.
495
+ TRIBE v2 Neural API Security Probe, 060 DNS Security Auditor): `scan_host` runs them on every tier (050 only when
496
+ TCP 8080 is open) and returns their findings on the records — `certAudit`, `tribeHealth`, `dnsSecurity` (060's only for
497
+ a domain-name target, in the conclusion's evidence when the scan found no 53/udp service; it declines an IP address);
498
+ `probe_service` (Pro) runs one of them against one port.
479
499
 
480
500
  > A per-plugin list used to be duplicated here and drifted: it claimed **18** Enterprise plugins
481
501
  > while enumerating **15**, against **28** on disk. One catalog, in `references/plugins.md`.
@@ -485,13 +505,13 @@ TCP 8080 is open) but does NOT return their findings, so over MCP the only route
485
505
  See `references/workflows.md` for detailed multi-step patterns:
486
506
 
487
507
  1. **Full Security Audit** — list_plugins → scan_host → get_vulnerabilities per service (over MCP: no analysis
488
- agents, no exploit intelligence, no 040 / 050 / 060 findings — the CLI scan runs the agents and exploit intelligence)
508
+ agents and no exploit intelligence — the CLI scan runs them)
489
509
  2. **Targeted Service Investigation** — probe_service(pluginId) → get_vulnerabilities
490
510
  3. **Subnet Discovery** — CLI: `nsauditor-ai scan --host <CIDR> --parallel 10`
491
511
  4. **CI/CD Pipeline** — SARIF output with `--fail-on` severity gating
492
- 5. **Continuous Monitoring (CTEM)** — `--watch --interval <min> --webhook-url <url>` (its webhook does NOT fire
493
- on a service, version or finding change in this release; read changes from the `[ScanHistory]` lines —
494
- `references/workflows.md` §5)
512
+ 5. **Continuous Monitoring (CTEM)** — `--watch --interval <min> --webhook-url <url>` (its webhook alerts a host
513
+ whose scan changed and that carries a finding at or above `--alert-severity`; the first cycle sets the baseline
514
+ and alerts nobody — `references/workflows.md` §5)
495
515
  6. **AI-Powered Report** — Scan with AI provider (OpenAI/Claude/Ollama) + redaction
496
516
 
497
517
  ### Decision Tree: Which Tool to Use
@@ -504,17 +524,21 @@ User wants to...
504
524
  ├── Check a specific service/port → probe_service (Pro)
505
525
  ├── Look up CVEs for software version → get_vulnerabilities (Pro)
506
526
  ├── See available plugins → list_plugins
507
- ├── Audit TLS certificates → probe_service (Pro) with plugin 040 (scan_host RUNS it but does not return its findings)
508
- ├── Check DNS security (SPF/DKIM/DMARC) → probe_service (Pro) with plugin 060 (scan_host RUNS it but does not return its findings)
509
- ├── Detect debug leaks / CORS issues → probe_service (Pro) with plugin 050 (scan_host RUNS it but does not return its findings)
527
+ ├── Audit TLS certificates → probe_service (Pro) with plugin 040, one port — or scan_host:
528
+ │ `certAudit` on each port 040 audited
529
+ ├── Check DNS security (SPF/DKIM/DMARC) → probe_service (Pro) with plugin 060 — or scan_host on the domain name:
530
+ │ `dnsSecurity` (on the 53/udp record, else in the conclusion's evidence)
531
+ ├── Detect debug leaks / CORS issues → probe_service (Pro) with plugin 050 — or scan_host: `tribeHealth`,
532
+ │ only when TCP 8080 is open
510
533
  ├── Scan a subnet → CLI with --parallel (not MCP)
511
534
  ├── Set up continuous monitoring → CLI with --watch (not MCP)
512
- │ (its webhook does NOT fire on a service change in this release — workflows.md §5)
535
+ │ (its webhook alerts a changed host carrying a finding at or above
536
+ │ --alert-severity; never on the first cycle — workflows.md §5)
513
537
  ├── Compare two scans → CLI (Pro): report --from <dir> --format executive --since prior
514
538
  │ (not MCP; NEVER by hand — a by-hand diff reads a finding that
515
- │ vanished unmeasured as fixed; report --since does too for an
516
- │ agent or CVE row derived from a plugin the later run left out
517
- │ (the header's measured limits) — keep --plugins identical)
539
+ │ vanished unmeasured as fixed; report --since refuses it, but
540
+ │ the header's two measured limits still apply — compare runs
541
+ │ made by Enterprise 1.3.0 or later, and include the port scanner 003)
518
542
  ├── State framework COVERAGE → compliance_matrix (any tier)
519
543
  └── Produce a compliance EVIDENCE PACK → CLI with --compliance (not MCP)
520
544
  ```
@@ -551,21 +575,32 @@ See `references/schemas.md` for complete structures:
551
575
  by default. Never tell a user "nothing leaves", and never suggest enabling an opt-in path without
552
576
  the user's decision.
553
577
 
554
- 2. **SSRF Protection:** The MCP server refuses targets written as loopback (127.x, ::1),
555
- link-local (169.254.x, fe80:) or a cloud metadata name, and while `NSA_ALLOW_ALL_HOSTS` is unset
556
- it also checks the address a host name resolves to. `NSA_ALLOW_ALL_HOSTS=1` turns that check off,
557
- so a name that resolves to a loopback or metadata address then gets through; set it **only** for
558
- legitimate local network auditing.
578
+ 2. **SSRF Protection:** The MCP server refuses loopback, unspecified, link-local and cloud-metadata addresses
579
+ (`169.254.169.254`, `100.100.100.200`, `fd00:ec2::254`) in any spelling, whether written as the target or returned
580
+ for a host name — every answer is checked — and refuses a name that does not resolve. Private ranges (RFC 1918,
581
+ CGNAT `100.64/10`, `fc00::/7`) are refused too unless `NSA_ALLOW_ALL_HOSTS` is `1`, `true`, `yes` or `on`, which
582
+ admits private ranges only, never loopback, link-local or metadata. It takes one host: a CIDR or URL-shaped string
583
+ is refused. From Community Edition 1.0.0 the scan connects to the answers the check resolved, so a name whose answer
584
+ changes between the check and the scan (DNS rebinding) is not followed — except through an HTTP proxy set with
585
+ `NODE_USE_ENV_PROXY`, which resolves the name itself, or over an idle connection opened earlier to the same name, for
586
+ 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
587
+ Community Edition does not pin the answer. One exception, off by default: with `DNS_AUDIT_AXFR_CHECK` set, the
588
+ DNS-security audit (060) opens TCP/53 to each of the scanned domain's nameservers, at addresses no host check resolved.
589
+ Set the variable **only** for legitimate local network auditing.
559
590
 
560
591
  3. **AI Redaction:** with AI analysis on and `OPENAI_REDACT` at its default (on), the scan payload
561
592
  (host, summary, services, evidence) is scrubbed at every tier: the host field becomes
562
593
  `[REDACTED_HOST]`; private IPv4 becomes `[REDACTED_HOST]` in the summary and `[REDACTED_IP]` in
563
594
  services and evidence; public IPv4 becomes `[IP]`; link-local and full-form IPv6 and colon-form
564
595
  MACs (`[MAC]`) are masked; serial numbers become `[REDACTED_HIDDEN]`, as do values under keys
565
- containing a word listed in `CONFIDENTIAL_KEYWORDS` (empty by default). **That payload is NOT
566
- scrubbed of** email addresses, internal hostnames, SNMP community strings, Bearer tokens, AWS keys,
567
- file paths or compressed IPv6 (`2001:db8::1`); ports, service names, versions and other evidence
568
- text go as they are. At Pro and above, the findings block added to the prompt is also masked by
596
+ containing a word listed in `CONFIDENTIAL_KEYWORDS` (empty by default). From Community Edition 1.0.0
597
+ an AWS account ID found in an ARN or an `amazonaws.com/<id>/…` URL becomes `[ACCOUNT_ID]` everywhere
598
+ it occurs, and any other 12-digit number `[REDACTED_ID]`; the system prompt is written beside the
599
+ payload (`scan_response_ai_system_prompt.txt`) before the request. **That payload is NOT scrubbed
600
+ of** email addresses, internal hostnames, SNMP community strings, Bearer tokens, AWS access-key IDs,
601
+ Azure subscription IDs, cloud resource names (an account ID inside one is masked from 1.0.0), file
602
+ paths or compressed IPv6 (`2001:db8::1`); ports, service names, versions and other evidence text go
603
+ as they are. At Pro and above, the findings block added to the prompt is also masked by
569
604
  default: target hosts, private IPv4, emails, MACs, internal hostnames and `community=<value>`
570
605
  strings (a community quoted in a finding title is not); AWS keys, Bearer tokens and file paths
571
606
  only with `NSA_AI_REDACT_LEVEL=strict`. With Ollama at its default localhost URL, the AI call
@@ -590,16 +625,16 @@ See `references/schemas.md` for complete structures:
590
625
 
591
626
  | Variable | Default | Purpose |
592
627
  |----------|---------|---------|
593
- | `NSA_ALLOW_ALL_HOSTS` | unset | Set to `1` to scan RFC 1918 private ranges |
628
+ | `NSA_ALLOW_ALL_HOSTS` | unset | `1`, `true`, `yes` or `on` (nothing else) admits private ranges — RFC 1918, CGNAT, `fc00::/7`. Over MCP it never admits loopback, link-local or metadata; on the CLI it lifts the whole scan-entry guard |
594
629
  | `PLUGIN_TIMEOUT_MS` | 30000 | Per-plugin budget. `scan_host` and `probe_service` bind every plugin to it; on the CLI a plugin that declares its own budget outranks it |
595
630
  | `PLUGIN_TIMEOUT_CEILING_MS` | 120000 | Upper bound on any plugin's declared budget — the setting that caps every plugin on the CLI |
596
631
  | `AI_ENABLED` | false | Enable AI analysis |
597
632
  | `AI_PROVIDER` | openai | `openai` · `claude` · `ollama` |
598
633
  | `OPENAI_API_KEY` | — | OpenAI API key (or `keychain:OPENAI_API_KEY`) |
599
634
  | `ANTHROPIC_API_KEY` | — | Claude/Anthropic API key |
600
- | `OPENAI_MODEL` | gpt-4o-mini | OpenAI model name |
635
+ | `OPENAI_MODEL` | gpt-6-luna (Community Edition 1.0.0 and later; gpt-4o-mini before) | OpenAI model name |
601
636
  | `ANTHROPIC_MODEL` | claude-sonnet-4-6 | Anthropic model name (used when `AI_PROVIDER=claude`) |
602
- | `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) |
637
+ | `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) |
603
638
  | `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) |
604
639
  | `NSAUDITOR_LICENSE_KEY` | — | Pro/Enterprise JWT license key |
605
640
  | `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. |
@@ -607,6 +642,7 @@ See `references/schemas.md` for complete structures:
607
642
  | `SMB_NULL_SESSION` | false | Allow SMB null session probe |
608
643
  | `FTP_CHECK_ANON` | false | Let the FTP check try an anonymous login — without it no scan reports anonymous FTP |
609
644
  | `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) |
645
+ | `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 |
610
646
  | `ENABLE_SYN_SCAN` | false | Enable Nmap TCP SYN scanning (requires root) |
611
647
 
612
648
  ### Plugin-Specific Timeouts
@@ -662,7 +698,7 @@ Run `install-key` once per machine, and use the `NSA_MCP_AUTH_KEY` value in the
662
698
  }
663
699
  ```
664
700
 
665
- `NSA_ALLOW_ALL_HOSTS: "1"` lets Claude scan your own network — private addresses such as `192.168.x.x` or `10.x.x.x` — because it turns off the MCP server's check of the address a host name resolves to. The server still refuses a host written as `localhost`, `127.x.x.x`, `::1`, `169.254.x.x` or `metadata.google.internal`, but no longer a name that resolves to a loopback or cloud-metadata address, or a spelling such as `::ffff:127.0.0.1`. `install-key` does not print it, so add it yourself, and remove it if you only scan public hosts.
701
+ `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.
666
702
 
667
703
  `PLUGIN_TIMEOUT_MS` bounds each plugin `scan_host` runs, not the call: the plugins run one after
668
704
  another, so the call takes roughly the sum of their times. On one router, a `scan_host` call timed out
@@ -695,7 +731,8 @@ Add to your MCP configuration with the same command/args pattern.
695
731
 
696
732
  | Error | Cause | Resolution |
697
733
  |-------|-------|-----------|
698
- | SSRF block (`… is not allowed via MCP`) | One message, several causes: the target, or what its name resolves to, is private (RFC 1918, CGNAT `100.64/10`, IPv6 `fc00::/7`), loopback, `0.x`, link-local or metadata; or, while `NSA_ALLOW_ALL_HOSTS` is unset, the name does not resolve | Unresolvable name: check its spelling. Private target: set `NSA_ALLOW_ALL_HOSTS=1` in the MCP server env, then fully quit and relaunch the client. Targets written as `localhost`, `127.x`, `::1`, `0.x`, `169.254.x`, `fe80:` or `metadata.google…` are refused over MCP with or without it; scan those from the CLI, where `NSA_ALLOW_ALL_HOSTS=1` lifts the whole guard |
734
+ | SSRF block (`Scanning loopback, link-local, or metadata addresses is not allowed via MCP`) | The target, or any address its name resolves to, is loopback, unspecified (`0.x`, `::`), link-local or a cloud-metadata address — refused with or without `NSA_ALLOW_ALL_HOSTS` — or is private (RFC 1918, CGNAT `100.64/10`, IPv6 `fc00::/7`) while `NSA_ALLOW_ALL_HOSTS` is not `1`, `true`, `yes` or `on`. The message names loopback for a private target too | Private target: set `NSA_ALLOW_ALL_HOSTS=1` in the MCP server env, then fully quit and relaunch the client. A loopback, link-local or metadata target — written so, or reached through what a name resolves to — is refused over MCP with or without it; scan it from the CLI, where `NSA_ALLOW_ALL_HOSTS=1` lifts the whole guard |
735
+ | Unresolvable host (`Host could not be resolved via MCP: <host>`) | The name does not resolve — with or without `NSA_ALLOW_ALL_HOSTS` — or the string is not one host: a CIDR or URL-shaped string (`10.0.0.0/8`, `host:80`) is refused this way | Check the spelling; pass one host name or IP address; scan a range from the CLI (`--host <CIDR>`) |
699
736
  | License gate (`🔒`) | Pro/Enterprise tool on CE | Upgrade license or use CE alternative |
700
737
  | Plugin timeout (`timeout` in `manifest`) | Network unreachable / slow target | Not measured, never clean. Raise `PLUGIN_TIMEOUT_MS` in the server env, or scan from the CLI, where a plugin that declares its own budget gets it |
701
738
  | No DNS banner | Provider blocks CHAOS/TXT queries | Expected; not all DNS servers expose version |
@@ -729,6 +766,7 @@ The CLI's report tags what it finds with MITRE techniques (the `scan_host` tool
729
766
  | `scan_conclusion_raw.json` | JSON | Full unredacted scan data (admin) |
730
767
  | `scan_conclusion_raw.html` | HTML | Admin dashboard with filters |
731
768
  | `scan_response_ai_payload.json` | JSON | Redacted payload sent to AI |
769
+ | `scan_response_ai_system_prompt.txt` | Text | System prompt sent to AI, the Pro/Enterprise intelligence block included (from Community 1.0.0) |
732
770
  | `scan_response_ai.html` | HTML | Styled report with CVE links, severity badges |
733
771
  | `scan_response_ai.txt` | Markdown | AI vulnerability assessment (text) |
734
772
  | SARIF | JSON | CI/CD integration (GitHub Advanced Security, Azure DevOps) |
@@ -23,10 +23,12 @@ it does NOT look up CVEs, so follow with `get_vulnerabilities` for each service
23
23
  ] } },
24
24
  manifest: [ … ], pluginsRan: …, markdown: "… **Security findings:** 1 (High: 1) …" }
25
25
  There is no findings array: the SNMP flag is `community` on the service record, and the markdown's count is the
26
- flags it counts — anonymous FTP login, zone transfer, SNMP default community, weak TLS protocols / ciphers, weak SSH
27
- algorithms. It does NOT count the MCP server flags (read `mcp*` off the service records yourself), and it holds no CVE
28
- lookup, no analysis-agent finding and no 006 / 014 / 040 / 050 / 060 result. No `anonymousLogin` on a service and a
29
- null `axfrAllowed` mean those checks were NOT RUN (they need FTP_CHECK_ANON / DNS_CHECK_AXFR), not that they passed.
26
+ service-check findings the shared table grades, one per item — anonymous FTP login, zone transfer and the SMB null
27
+ session (each only when its check is enabled), SNMP default community, weak SSH algorithms, weak TLS protocols /
28
+ ciphers, dangerous HTTP methods, a self-signed or expired certificate, the MCP server flags and the 040 / 050 / 060
29
+ audit entries. It holds no CVE lookup and no analysis-agent finding. A null `anonymousLogin` or `axfrAllowed` means
30
+ the check was NOT TESTED (`anonymousLoginTested` / `axfrTested` say why — `opt-in-off` without FTP_CHECK_ANON /
31
+ DNS_CHECK_AXFR), not that it passed.
30
32
 
31
33
  2. get_vulnerabilities({ cpe: "cpe:2.3:a:openbsd:openssh:8.9:p1:*:*:*:*:*:*" })
32
34
  → CVE-2023-38408 (CRITICAL 9.8), CVE-2023-48795 (MEDIUM 5.9)
@@ -138,18 +140,21 @@ start-up if the URL does not resolve or resolves to a private, loopback or link-
138
140
  --alert-severity high
139
141
 
140
142
  This rescans every 15 minutes; a [ScanHistory] line per host compares its scan with the previous one (counts of
141
- new, removed and changed services; the findings delta). In this release the webhook does NOT fire on a service,
142
- version or finding change: only after a cycle that scanned a different set of hosts from the one before (in
143
- practice, one cut short by stopping the loop), and at --alert-severity high only for a host where anonymous FTP
144
- login or a DNS zone transfer succeeded (both checks are off unless FTP_CHECK_ANON=true / DNS_CHECK_AXFR=true +
145
- DNS_AXFR_DOMAIN are set). Read the [ScanHistory] lines for changes.
143
+ new, removed and changed services; the findings delta; each service-check finding that appeared, cleared or could
144
+ not be compared). The webhook alerts a host whose scan changed since the previous cycle and that carries a finding
145
+ at or above --alert-severity — one detail per finding, each with its own grade; the first cycle sets the baseline and
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 (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.
146
151
 
147
152
  Each scan appends one line per host to scan_history.jsonl in the output directory (--out, default out/)."
148
153
  ```
149
154
 
150
155
  ---
151
156
 
152
- ## Example 6: "Audit DNS security for example.com" — `probe_service` (Pro) with plugin 060 (`scan_host` runs 060 but does not return its findings)
157
+ ## Example 6: "Audit DNS security for example.com" — `probe_service` (Pro) with plugin 060 (`scan_host` on the domain returns 060's findings too, as `dnsSecurity`)
153
158
 
154
159
  **Agent reasoning:** Use the DNS Security Auditor plugin for comprehensive DNS assessment.
155
160
 
@@ -175,7 +180,8 @@ Each scan appends one line per host to scan_history.jsonl in the output director
175
180
  ✅ DKIM selector found (google)
176
181
  ✅ DMARC policy set to 'reject' (strongest setting)
177
182
  ⚠ DNSSEC not enabled (recommended for integrity)
178
- ✅ 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
179
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
180
186
  STARTTLS / TLS on the mail servers.
181
187
  ```
@@ -191,10 +197,10 @@ Each scan appends one line per host to scan_history.jsonl in the output director
191
197
  → isError reply: {"error":"Scanning loopback, link-local, or metadata addresses is not allowed via MCP"}
192
198
 
193
199
  2. Agent response:
194
- "NSAuditor AI's MCP server refuses targets written as localhost, 127.x, ::1, 0.x, 169.254.x, fe80: or
195
- metadata.google…, with or without NSA_ALLOW_ALL_HOSTS in its environment; without the variable it also
196
- checks the address a host name resolves to. That limits what an agent can reach when the scanner is
197
- exposed as an MCP tool.
200
+ "NSAuditor AI's MCP server refuses loopback, link-local and cloud-metadata addresses in any spelling — written
201
+ as the target, or returned for a host name, where every answer is checked — with or without NSA_ALLOW_ALL_HOSTS
202
+ in its environment; that variable admits private ranges only. That limits what an agent can reach when the
203
+ scanner is exposed as an MCP tool.
198
204
 
199
205
  To scan your own machine, use the CLI with the guard lifted (without the variable the CLI refuses it too:
200
206
  'Scanning blocked address range is not allowed: 127.0.0.1'):
@@ -250,12 +256,11 @@ carry it.
250
256
  Security Audit Report: 192.168.1.1
251
257
  ═══════════════════════════════════════
252
258
 
253
- Scope: scan_host's service checks + an NVD lookup per service whose cpe names a version. No analysis agents, no
254
- exploit intelligence — for those, run `NSA_ALLOW_ALL_HOSTS=1 nsauditor-ai scan --host 192.168.1.1` with the
255
- Enterprise package and a Pro/Enterprise licence. No TLS-certificate / DNS-security / debug-endpoint / HTTP-method
256
- results — for those, run probe_service (Pro) with 040 / 060 / 050 / 006; the CLI scan's reports do not carry them
257
- either (only its raw per-plugin results in scan_conclusion_raw.json do). Anonymous FTP and zone transfer were not
258
- tested (FTP_CHECK_ANON / DNS_CHECK_AXFR were not set).
259
+ Scope: scan_host's service checks — the TLS-certificate, DNS-security and debug-endpoint audits and the HTTP
260
+ methods included, where those plugins ran — plus an NVD lookup per service whose cpe names a version. No analysis
261
+ agents, no exploit intelligence — for those, run `NSA_ALLOW_ALL_HOSTS=1 nsauditor-ai scan --host 192.168.1.1` with
262
+ the Enterprise package and a Pro/Enterprise licence. Anonymous FTP, zone transfer and the SMB null session were not
263
+ tested (FTP_CHECK_ANON / DNS_CHECK_AXFR / SMB_NULL_SESSION were not set).
259
264
 
260
265
  Host: Linux (Ubuntu 22.04)
261
266
  Services: 4 (SSH, DNS, HTTPS, SNMP)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai-agent-skill",
3
- "version": "0.2.54",
3
+ "version": "1.0.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.56"
53
+ "nsauditor-ai": ">=1.0.0"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "nsauditor-ai": {
@@ -10,7 +10,7 @@ Complete reference of all scanner plugins, organized by category.
10
10
  |----|------|-----------|-------|----------|-------------|
11
11
  | 001 | Ping Checker | ICMP/ARP | — | 10 | — |
12
12
  | 002 | SSH Scanner | TCP | 22 | 50 | host: up, tcp_open: [22] |
13
- | 003 | Port Scanner | TCP/UDP | 50 default (43 TCP + 7 UDP, from `config/services.json`); `--ports` adds more | 30 | host: up |
13
+ | 003 | Port Scanner | TCP/UDP | 50 default (43 TCP + 7 UDP, from `config/services.json`); `--ports` adds more | 30 | — (runs whether or not an earlier plugin marked the host up; a port that answered — open or refused — marks it up) |
14
14
  | 004 | FTP Banner Check | TCP | 21 | 40 | host: up, tcp_open: [21] |
15
15
  | 005 | Host Up Check | ICMP/TCP/UDP | TCP 21, 22, 80, 443, 3389 · UDP 54321 | 20 | host: down (runs only if not yet up) |
16
16
  | 006 | HTTP Probe | TCP | 80, 443 | 60 | host: up, tcp_open: [80, 443] |
@@ -54,7 +54,9 @@ extracts program/version from FTP banner (220 response).
54
54
 
55
55
  **005 — Host Up Check:** Fallback reachability check: it runs only when no earlier plugin has marked the host
56
56
  up (requirements `host: down`). ICMP echo, then a TCP connect to 21, 22, 80, 443 and 3389 (a refused connection
57
- counts as up), then one UDP datagram to high port 54321.
57
+ counts as up), then one UDP datagram to high port 54321, which counts as up only when the host answers it — an ICMP
58
+ port-unreachable or a reply. An error, or no answer within 3 s, is no evidence either way, never "down"; that wait
59
+ adds up to 3 s to a run in which nothing answers on UDP.
58
60
 
59
61
  **006 — HTTP Probe:** Extracts HTTP response headers (`Server`, `X-Powered-By`),
60
62
  detects redirects, extracts server tokens for vendor/program identification.
@@ -165,7 +167,7 @@ LLMNR poisoning risk).
165
167
  |----|------|-------|---------|
166
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 |
167
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 |
168
- | 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 |
169
171
 
170
172
  ### Deep-Audit Plugin Details
171
173
 
@@ -173,7 +175,25 @@ LLMNR poisoning risk).
173
175
  with full certificate chain validation, expiration warnings, and a comprehensive weak
174
176
  cipher inventory. Generates findings for: expired certs, self-signed certs, weak key
175
177
  sizes (<2048-bit RSA), and deprecated cipher suites. *(Missing-HSTS detection is the
176
- network-scan `crypto_agent`'s, not this plugin's.)*
178
+ network-scan `crypto_agent`'s, not this plugin's.)* A hostname mismatch is graded by the target's form (Community 0.2.57): scanned by address against a certificate that
179
+ names DNS names only it is LOW, saying so in its detail; a DNS-name target the certificate does not name, a certificate
180
+ naming a different address, or one with no subjectAltName is HIGH. IP SANs are compared as addresses, and a name
181
+ mismatch on a chain the CA store verified does not also raise `ca_not_trusted`. A certificate with no subjectAltName
182
+ (CN only) stays HIGH and says it carries none. A TLS 1.3 connection has forward secrecy by its protocol (every TLS 1.3
183
+ suite is ephemeral); below TLS 1.3 the negotiated cipher decides, so a TLS 1.2 RSA key exchange keeps
184
+ `no_forward_secrecy` MEDIUM. The key-size and signature checks fire on real servers from Community 0.2.57 (they had read
185
+ fields Node never sets): an RSA key under 2048 bits is `weak_rsa_key` HIGH (CRITICAL under 1024), an EC key under 256
186
+ bits `weak_ec_key` HIGH, a CA-issued certificate signed with SHA-1 or MD5 `weak_signature` HIGH, and an issued
187
+ intermediate signed that way `chain_weak_signature` MEDIUM. A self-signed certificate's own signature is not graded —
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 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.
177
197
 
178
198
  **050 — TRIBE v2 Neural API Security Probe:** Probes a TRIBE v2 API: `TRIBE_API_HOST` / `TRIBE_API_PORT` /
179
199
  `TRIBE_API_SCHEME` set the target; otherwise it is the scan host, on 8080 in a scan (or the port `probe_service`
@@ -189,7 +209,8 @@ not check a website's 80/443.
189
209
  - **DKIM:** Checks for DKIM selector records
190
210
  - **DMARC:** Validates DMARC policy (reject/quarantine/none)
191
211
  - **DNSSEC:** Checks for DNSSEC signing and validation chain
192
- - **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
193
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
194
215
  - **CAA Records:** Checks Certificate Authority Authorization records
195
216
 
@@ -315,4 +336,7 @@ Priority ID Plugin What it adds
315
336
 
316
337
  **Auto-skip rules:** Plugins with unmet `requirements` are automatically skipped. For
317
338
  example, SSH Scanner (requires `tcp_open: [22]`) skips if the Port Scanner didn't find
318
- port 22 open. This avoids wasted probes and reduces scan time.
339
+ port 22 open. This avoids wasted probes and reduces scan time. A UDP port opens a `udp_open` gate (007 on 161) only
340
+ when a row there ANSWERED: the port scanner's `no-response` row, or a "No SNMP response for community …" row, does not
341
+ open it. With no evidence the host is up, a `host: up` plugin's skip reason reads *no evidence the host is up*, never
342
+ "down" — and the port scanner (003), which is not gated, runs either way.
@@ -73,7 +73,7 @@ Complete data structures for all MCP tool inputs and outputs.
73
73
  "duration_ms": 30001
74
74
  }
75
75
  ],
76
- "pluginsRan": 55,
76
+ "pluginsRan": 1,
77
77
  "markdown": "# NSAuditor AI Scan Report …"
78
78
  }
79
79
  ```
@@ -83,6 +83,8 @@ Complete data structures for all MCP tool inputs and outputs.
83
83
  `evidence[]` and, where one could be built, a `cpe` for `get_vulnerabilities`.
84
84
  - `manifest[]` is every plugin's run status: `ran`, `skipped` (with its `reason`), `timeout` or
85
85
  `error`. A `timeout` or `error` means that surface was **NOT measured** — never read it as clean.
86
+ - `pluginsRan` is the number of `manifest[]` entries whose status is `ran` — in this example, one of the two entries
87
+ shown. It counts plugins, never ports or results.
86
88
  - There is **no `findings` array** in a `scan_host` result, and no `techniques`: the CLI adds ATT&CK
87
89
  techniques to its own report, and this tool does not.
88
90
 
@@ -108,10 +110,24 @@ interface ServiceRecord {
108
110
  authoritative: boolean; // Takes precedence over other sources for this port
109
111
 
110
112
  // Optional fields (populated by specific plugins)
111
- anonymousLogin?: boolean; // FTP anonymous login detected — tested only with FTP_CHECK_ANON=true; absent = NOT TESTED
113
+ anonymousLogin?: boolean | null; // FTP anonymous login detected — tested only with FTP_CHECK_ANON=true; null = NOT TESTED
114
+ anonymousLoginTested?: true | string | null; // true when measured, else why not: "opt-in-off" · "no-answer"
112
115
  axfrAllowed?: boolean | null; // DNS zone transfer allowed — tested only with DNS_CHECK_AXFR=true + DNS_AXFR_DOMAIN; null = NOT TESTED
113
- community?: string; // SNMP community accepted, as-is: "public", "private" or a custom SNMP_COMMUNITY value
114
- dangerousMethods?: string[]; // NOT carried onto a scan_host record in this release — the HTTP probe (006) records it, and probe_service with 006 returns it
116
+ // (060's own attempt, DNS_AUDIT_AXFR_CHECK, reports as an axfr_open entry of dnsSecurity, not here)
117
+ axfrTested?: true | string | null; // true when measured, else why not: "opt-in-off" · "no-domain" · "no-answer"
118
+ community?: string | null; // SNMP DEFAULT community accepted: "public" or "private"; a custom SNMP_COMMUNITY string is never recorded
119
+ communityCustom?: boolean; // true when a custom SNMP_COMMUNITY string answered — not a finding
120
+ communitiesTried?: string[] | null; // the communities tried, as labels (a custom string reads "custom")
121
+ dangerousMethods?: string[] | null; // HTTP probe (006): the dangerous methods an Allow header listed; null when methodsTested is false
122
+ methodsTested?: boolean; // 006: true only when an Allow header was read — false is NOT TESTED, never "none"
123
+ allowedMethods?: string[] | null; // 006: every method the Allow header listed
124
+ headers?: object; // 006: { "strict-transport-security" } only, on the port-443 HTTPS record, only where a response arrived
125
+ nullSessionAllowed?: boolean | null; // NetBIOS/SMB (014) — tested only with SMB_NULL_SESSION=true; null = NOT TESTED
126
+ nullSessionTested?: true | string | null; // true when measured, else why not: "opt-in-off" · "no-answer"
127
+ shares?: string[]; // 014: the shares a null session listed
128
+ certAudit?: object; // the TLS-certificate audit (040) of this port — its issues[] graded at 040's own grades
129
+ tribeHealth?: object; // the debug-endpoint audit (050), on the 8080 record — { state, severity, findings[] }
130
+ dnsSecurity?: object; // the DNS-security audit (060) of a DOMAIN-NAME target — here on a 53/udp record, else in the conclusion's evidence[]; absent for an IP-address target, which 060 declines (manifest: skipped, with the reason) — NOT TESTED, never clean
115
131
  weakAlgorithms?: string[]; // SSH weak key-exchange, cipher and MAC names, together in one array (002)
116
132
  weakCiphers?: string[]; // TLS weak ciphers (the negotiated cipher per version)
117
133
  certSelfSigned?: boolean; // TLS certificate is self-signed (011)
@@ -169,7 +185,8 @@ export default {
169
185
  // tcpOpen: Set<number>, // TCP ports found open by the plugins that ran before this one
170
186
  // udpOpen: Set<number>, // UDP ports, likewise
171
187
  // pluginRunStatus: Map, // id -> "ran" | "timeout" | "error" | "skipped" for each earlier plugin
172
- // // dispatched; one skipped on its requirements has no entry
188
+ // // the scan selected — one skipped on its requirements or a missing
189
+ // // capability reads "skipped"; no entry = the plugin was never requested
173
190
  // effectiveTimeoutMs: number, // the budget the manager races this run() against
174
191
  // }
175
192
  return {
@@ -277,7 +294,7 @@ never writes a queue. A CVE row, which carries every field:
277
294
 
278
295
  **Every row:** `id` · `category` · `status` · `title` · `severity` · `cvss` · `target` · `evidence` · `remediation` · `riskScore`
279
296
 
280
- **Most rows:** `description` — the analysis agents' own findings omit it: `auth_agent` · `config_agent` · `crypto_agent` · `exposure_agent` · `service_agent`. The CVE engine's rows and every coverage-gap record carry it.
297
+ **Most rows:** `description` — three analysis agents' own findings omit it: `auth_agent` · `config_agent` · `crypto_agent`. The CVE engine's rows, the exposure agent's rows (since Enterprise 1.3.0: the service its title no longer names), the service agent's rows (since Enterprise 1.3.0: the version its title no longer names) and every coverage-gap record carry it.
281
298
 
282
299
  **CVE-bearing rows, once KEV / EPSS data is loaded:** `kev` · `knownRansomwareCampaignUse` · `kevMatchedCve` · `kevAsOf` · `kevStoreState` · `epssScore` · `epssPercentile` · `epssMatchedCve` · `epssAsOf` · `epssModelVersion` · `epssStoreState` · `exploitPriority` · `exploitPriorityReason`
283
300
 
@@ -358,27 +375,30 @@ the installed Community's. The file is written to the output directory as `scan_
358
375
  "informationUri": "https://github.com/nsasoft/nsauditor-ai",
359
376
  "rules": [
360
377
  { "id": "vsftpd:3.0.5", "shortDescription": { "text": "ftp service detected" },
361
- "helpUri": "https://github.com/nsasoft/nsauditor-ai", "properties": { "severity": "Medium" } },
378
+ "helpUri": "https://github.com/nsasoft/nsauditor-ai", "properties": { "severity": "Info" } },
362
379
  { "id": "ftp-anonymous-login", "shortDescription": { "text": "FTP anonymous login enabled" },
363
380
  "helpUri": "https://github.com/nsasoft/nsauditor-ai", "properties": { "severity": "Critical" } }
364
381
  ]
365
382
  }
366
383
  },
367
384
  "results": [
368
- { "ruleId": "vsftpd:3.0.5", "level": "warning",
385
+ { "ruleId": "vsftpd:3.0.5", "level": "note",
369
386
  "message": { "text": "Service ftp detected on 192.168.1.1:21/tcp. Program: vsftpd. Version: 3.0.5. Status: open" },
370
387
  "locations": [{ "physicalLocation": { "artifactLocation": { "uri": "192.168.1.1" } } }] },
371
388
  { "ruleId": "ftp-anonymous-login", "level": "error",
372
- "message": { "text": "FTP anonymous login is enabled on 192.168.1.1:21. This allows unauthenticated access to the FTP server." },
389
+ "message": { "text": "FTP anonymous login enabled on 192.168.1.1:21/tcp. ftp on 192.168.1.1:21/tcp accepts anonymous authentication." },
373
390
  "locations": [{ "physicalLocation": { "artifactLocation": { "uri": "192.168.1.1" } } }] }
374
391
  ]
375
392
  }]
376
393
  }
377
394
  ```
378
395
 
379
- The file carries one result per service (`warning` when open, `note` otherwise) plus anonymous FTP login, zone transfer
380
- and weak SSH algorithm results — no TLS, SNMP, MCP, CVE or analysis-agent results. Anonymous FTP and zone transfer
381
- appear only when `FTP_CHECK_ANON` / `DNS_CHECK_AXFR` enabled those checks.
396
+ The file carries one `note` result per service (an open port is inventory) plus one result per graded service-check
397
+ finding — the same findings, at the same grades, that `--fail-on` and the Markdown report read
398
+ (`references/workflows.md` §4), the TLS, SNMP, MCP, certificate, SMB null-session and 040 / 050 / 060 audit results
399
+ included — and no CVE or analysis-agent results. Anonymous FTP, zone transfer and the SMB null session appear only when
400
+ `FTP_CHECK_ANON` / `DNS_CHECK_AXFR` (or `DNS_AUDIT_AXFR_CHECK`, for 060's attempt) / `SMB_NULL_SESSION` enabled those
401
+ checks. The rule ids of the four flags SARIF graded before Community 0.2.57 are unchanged.
382
402
 
383
403
  #### SARIF Severity Mapping
384
404
 
@@ -399,7 +419,8 @@ Compatible with: GitHub Advanced Security, Azure DevOps, SonarQube, and other SA
399
419
 
400
420
  When `OPENAI_REDACT=true` (default), the CLI's AI stage (`AI_ENABLED=true`) redacts the scan payload it sends —
401
421
  `host`, `host_os_hint`, `summary`, `services`, `evidence` — with Community's built-in redactor, at every tier
402
- (the Pro-tier external-redactor hook is installed by neither package):
422
+ (from Community 1.0.0 there is no external-redactor hook at all and `_meta.redactor` reads `builtin`; earlier releases
423
+ carried a Pro-tier hook that neither package installed):
403
424
 
404
425
  | Data | Becomes |
405
426
  |------|---------|
@@ -412,12 +433,14 @@ When `OPENAI_REDACT=true` (default), the CLI's AI stage (`AI_ENABLED=true`) reda
412
433
  | MAC addresses written with colons (`aa:bb:cc:dd:ee:ff`); hyphen and dotted forms pass unchanged | `[MAC]` |
413
434
  | Serial numbers (text `Serial:` / `Serial=`; keys `serial`, `serialNumber`, `sn`, in any case) | `[REDACTED_HIDDEN]` |
414
435
  | Values under keys containing a `CONFIDENTIAL_KEYWORDS` entry (unset by default: no key scrub) | `[REDACTED_HIDDEN]` |
436
+ | 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]` |
437
+ | Any other 12-digit number (Community 1.0.0+) | `[REDACTED_ID]` |
415
438
 
416
439
  Keys dropped entirely: `ip6`, `deviceWebPage`, `deviceWebPageInstruction`, `hardwareVersion`, `firmwareVersion`.
417
440
 
418
441
  ⚠️ **NOT redacted in that payload:** email addresses, internal hostnames (`.local`, `.corp`, `.internal`), the SNMP
419
- `community` field, Bearer tokens, AWS access keys and file paths — nor ports, service names, versions, or banner and
420
- evidence prose beyond the rows above. `OPENAI_REDACT` has no strict level. `CONFIDENTIAL_KEYWORDS` works by KEY
442
+ `community` field, Bearer tokens, AWS access-key IDs, Azure subscription IDs, cloud resource names themselves and file
443
+ 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
421
444
  (`community` blanks the `community` field, not the same value inside banner or evidence text). With Ollama at its
422
445
  default localhost URL the payload does not leave the host.
423
446
 
@@ -445,5 +468,10 @@ Every scan appends one line per host to `scan_history.jsonl` in its output direc
445
468
  or resolved findings.
446
469
  - `cloudFindingsCount` is the raw number of findings the plugins returned (evidence gaps and scope
447
470
  statements included); `findingsCount` is the report loader's shaped count, so the raw figure can exceed it.
471
+ - Since Community 0.2.57 a line also carries `flagsBasis` (`"service-flags-v1"`), `hostFlags` and `hostChecks` (a
472
+ domain's DNS-security audit), and each service entry `flags` (the comparison identity of each service-check finding,
473
+ e.g. `"weakAlgorithms:diffie-hellman-group1-sha1"`) and `checks` (per check that applies, `true` when measured, else
474
+ the reason it was not). The `[ScanHistory]` diff reads them: a finding gone reads CLEARED only where its check was
475
+ measured this run, else NOT COMPARED with the reason. The two example lines above were written before them.
448
476
  - Retention: Community keeps **7 days** (older lines are pruned after each scan); Pro and Enterprise keep
449
477
  every line. Neither is configurable.
@@ -8,7 +8,8 @@ Multi-step patterns for common security audit scenarios.
8
8
 
9
9
  The most common MCP workflow: a scan followed by a CVE lookup per service. It is NOT the CLI's full analysis: the
10
10
  Enterprise analysis agents and exploit intelligence run only in `nsauditor-ai scan --host <target>` (Enterprise package +
11
- Pro/Enterprise licence), and `scan_host` does not return the 040 / 050 / 060 auditors' findings (use `probe_service`).
11
+ Pro/Enterprise licence). `scan_host` does return the 040 / 050 / 060 audits (`certAudit`, `tribeHealth`, and
12
+ `dnsSecurity` for a domain-name target — 060 declines an IP address, and the `markdown` says it was not tested).
12
13
 
13
14
  ```
14
15
  Step 1: list_plugins()
@@ -123,25 +124,35 @@ and avoid network congestion. The MCP server processes one scan at a time.
123
124
 
124
125
  ## 4. CI/CD Pipeline Integration
125
126
 
126
- `--fail-on <severity>` reads FOUR service flags, and it is **not computed from the SARIF file** — the two read the same
127
- flags but grade them differently:
127
+ `--fail-on <severity>` reads every finding the shared service-check table grades — one per item (a method, an
128
+ algorithm, a protocol, a cipher, an audit entry), the same findings at the same grades that the Markdown report, the
129
+ SARIF file and the CSV count. It is **not computed from the SARIF file**, but the two read one table, so they cannot
130
+ grade a finding differently:
128
131
 
129
- | Flag in the scan's conclusion | `--fail-on` grade | SARIF level |
132
+ | Finding in the scan's conclusion | `--fail-on` grade | SARIF level |
130
133
  |---|---|---|
131
- | 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 |
132
- | weak SSH algorithms | medium | warning |
133
- | dangerous HTTP methods — read, but they never reach the conclusion in this release (the HTTP probe's result is not carried onto a service record) | medium | warning |
134
- | any service in a concluded scan | info — so `--fail-on info` fails every scan that concludes | `warning` when open, `note` otherwise |
135
-
136
- So on a default scan `--fail-on high` and `--fail-on critical` never exit 1, and `--fail-on medium` fires only on weak
137
- SSH algorithms. Neither the gate nor the SARIF file reads the SNMP default community, weak TLS protocols / ciphers, a
138
- self-signed certificate, the MCP server checks, the TLS-certificate / DNS-security / debug-endpoint auditors,
139
- Enterprise's CVE rows or analysis-agent findings (the Markdown report counts the SNMP community and weak TLS protocols /
140
- ciphers, but not the MCP server checks; none of them gates). Exit 0 is NOT a clean host.
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
+ | MCP server reachable without authentication · MCP server lists its tools without authentication | critical | error |
136
+ | MCP server over cleartext HTTP · MCP server on a deprecated protocol version | high | error |
137
+ | SNMP default community (`public` / `private`) | high | error |
138
+ | SMB null session — tested only with `SMB_NULL_SESSION=true`; graded high provisionally | high | error |
139
+ | expired certificate, on a port the TLS-certificate auditor (040) did not audit | high | error |
140
+ | weak SSH algorithms · weak TLS protocols · weak TLS ciphers, one per item | medium | warning |
141
+ | dangerous HTTP methods, one per method — only where an Allow header was read | medium | warning |
142
+ | self-signed certificate, on a port 040 did not audit · MCP Inspector exposed | medium | warning |
143
+ | each actionable entry of the TLS-certificate, debug-endpoint and DNS-security audits (040 / 050 / 060) | the auditor's own grade | by that grade |
144
+ | any concluded scan; an open service is inventory | info — so `--fail-on info` fails every scan that concludes | `note`, one per service |
145
+
146
+ So a default scan can exit 1 at `--fail-on high` — an SNMP default community, an MCP server finding, an expired
147
+ certificate or a HIGH audit entry is enough — and a pipeline that passed on an earlier Community may fail on findings
148
+ that were always there. One expired certificate is graded once: High where only the TLS scanner (011) saw it,
149
+ Critical where 040 audited it. The gate does not read Enterprise's CVE rows or analysis-agent findings. Exit 0 is NOT a
150
+ clean host.
141
151
 
142
152
  ```bash
143
- # Scan with SARIF output and severity gating. --fail-on high fires only on anonymous FTP / zone transfer:
144
- # set FTP_CHECK_ANON=true and DNS_CHECK_AXFR=true + DNS_AXFR_DOMAIN=<zone> first, or it never exits 1.
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
155
+ # (060's attempt against the domain's nameservers: DNS_AUDIT_AXFR_CHECK=true).
145
156
  nsauditor-ai scan --host $TARGET \
146
157
  --plugins all \
147
158
  --output-format sarif \
@@ -149,8 +160,8 @@ nsauditor-ai scan --host $TARGET \
149
160
  --out ./nsauditor-out
150
161
 
151
162
  # Exit codes:
152
- # 0 = no gated flag at or above the threshold — NOT a clean host
153
- # 1 = a gated flag at or above the threshold
163
+ # 0 = no graded finding at or above the threshold — NOT a clean host
164
+ # 1 = a graded finding at or above the threshold
154
165
  # 2 = unknown severity, or no scan produced a conclusion
155
166
  ```
156
167
 
@@ -162,7 +173,8 @@ SARIF file.
162
173
 
163
174
  ```yaml
164
175
  - name: Security Scan
165
- env: # without these, --fail-on high can never exit 1 (both checks are off by default)
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)
166
178
  FTP_CHECK_ANON: "true"
167
179
  DNS_CHECK_AXFR: "true"
168
180
  DNS_AXFR_DOMAIN: example.com # the zone to try transferring
@@ -184,7 +196,8 @@ SARIF file.
184
196
  ## 5. Continuous Monitoring (CTEM)
185
197
 
186
198
  Watch mode: every host is re-scanned on an interval and each scan is compared with that host's previous one. Its
187
- webhook alert does NOT fire on a service change in this release — read what triggers it before offering it.
199
+ webhook alerts a host whose scan changed and that carries a finding at or above `--alert-severity` — read what
200
+ triggers it before offering it.
188
201
 
189
202
  ```bash
190
203
  # NSA_ALLOW_ALL_HOSTS=1 because the range is private (§3). Replace the webhook URL with your own PUBLIC one: the
@@ -200,24 +213,36 @@ NSA_ALLOW_ALL_HOSTS=1 nsauditor-ai scan --host 192.168.1.0/24 --plugins all \
200
213
  **Features:**
201
214
  - Rescans on configurable interval (minutes; default 60); the first cycle runs at once
202
215
  - Each host's scan is compared with that host's previous line in `scan_history.jsonl` and printed as a
203
- `[ScanHistory]` line: new, removed and changed services, and the findings delta
216
+ `[ScanHistory]` line: new, removed and changed services, the findings delta, and each service-check finding that
217
+ appeared, cleared, was first observed or could not be compared (with the reason)
204
218
  - Scan history appended to `scan_history.jsonl` in the output directory (`--out`, default `out/`), one line per host
205
219
  - CE: 7-day retention; Pro/Enterprise: every line kept (neither is configurable)
206
220
 
207
- **What triggers the webhook — and what does not:**
208
- - A service, version or finding change does NOT. The cycle comparison the alert is gated on finds no services in
209
- the scan results it is handed, so its `=== Delta Report ===` shows no change for any host. The gate opens only
210
- when a cycle scanned a different set of hosts from the cycle before (in practice, a cycle cut short by stopping
211
- the loop), and never on the first cycle. For changes, read the `[ScanHistory]` lines.
212
- - When the gate opens, an alert (a JSON POST) goes out for each host whose services carry a flag at or above
213
- `--alert-severity` (default `high`; at `info` every service counts, flagged or not): anonymous FTP login or DNS
214
- zone transfer (critical; both checks are off unless `FTP_CHECK_ANON=true` / `DNS_CHECK_AXFR=true` +
215
- `DNS_AXFR_DOMAIN` are set) and weak SSH algorithms (medium). Dangerous HTTP methods never reach a service record
216
- (§4) and nothing on the scan path fills a service's CVE list, so at `high` only the two opt-in checks count.
217
- - The payload is not redacted: the host and its flagged services go in the clear to the URL you supply.
218
-
219
- **Webhook Payload** (the change itself is NOT in it; `details` lists the host's flagged services, and each
220
- detail's `severity` is the `--alert-severity` value, not the flag's own grade):
221
+ **What triggers the webhook:**
222
+ - A host alerts when its scan CHANGED since the previous cycle — a service appeared, went or changed, its finding
223
+ count moved, a service check alone changed, or the comparison could not be made — AND it carries at least one
224
+ finding at or above `--alert-severity` (default `high`). The first cycle sets the baseline and alerts nobody;
225
+ `--alert-every-cycle` alerts every host carrying such a finding on every cycle, the first included. Stdout prints
226
+ each host's change under `=== Delta Report ===`, and `No significant changes detected.` only when no host changed.
227
+ - The findings are the service-check findings `--fail-on` and the reports grade (§4): anonymous FTP login or a DNS
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.
234
+ - A host whose scan failed, or whose summary is missing on either side, is reported not comparable on stdout — never
235
+ "No changes", never "removed" — and does not alert in this release.
236
+ - Watch mode scans each distinct host once per cycle: a repeated `--host` entry, or one also inside a listed CIDR, is
237
+ dropped, and the banner says how many.
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.
241
+
242
+ **Webhook Payload**, the default `full` (the change itself is NOT in it: `details` carries one entry per finding at or above
243
+ `--alert-severity` — `description` is the finding's title and `severity` its own grade, lower-case; a finding with no
244
+ port, such as a domain's DNS-security entry that landed in the conclusion's evidence, carries `port` and `protocol`
245
+ null. The top-level `severity` is the `--alert-severity` value):
221
246
 
222
247
  ```json
223
248
  {
@@ -226,7 +251,7 @@ detail's `severity` is the `--alert-severity` value, not the flag's own grade):
226
251
  "severity": "high",
227
252
  "findingsCount": 1,
228
253
  "summary": "1 finding(s) detected on 192.168.1.20 at severity high or above",
229
- "details": [{ "port": 21, "protocol": "tcp", "service": "ftp", "description": null, "severity": "high" }]
254
+ "details": [{ "port": 21, "protocol": "tcp", "service": "ftp", "description": "FTP anonymous login enabled", "severity": "critical" }]
230
255
  }
231
256
  ```
232
257
 
@@ -263,6 +288,7 @@ AI_ENABLED=true AI_PROVIDER=openai OPENAI_API_KEY=sk-... OPENAI_REDACT=true NSA_
263
288
  | `scan_conclusion_raw.json` | Full unredacted data (admin reference) |
264
289
  | `scan_conclusion_raw.html` | Admin HTML dashboard with filters |
265
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) |
266
292
  | `scan_response_ai.html` | Styled HTML report with CVE links, severity badges |
267
293
  | `scan_response_ai.txt` | AI vulnerability assessment in markdown |
268
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. |
@@ -302,7 +328,7 @@ port or probe not measured, a CVE lookup that failed, an agent that did not run,
302
328
  changing under the same program and version — as RESOLVED. `report --since` reads each of those as
303
329
  NOT COMPARABLE with its reason, refuses the comparison outright when either run's chain is altered or cannot be
304
330
  measured (naming which side), and says what it did not evaluate.
305
- 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.
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.
306
332
 
307
333
  Read from its output:
308
334
  - New findings (unexpected exposure) — it compares FINDINGS, not services: a newly exposed service appears only if
@@ -322,13 +348,17 @@ User wants to...
322
348
  ├── Check a specific service/port → probe_service (Pro)
323
349
  ├── Look up CVEs for software version → get_vulnerabilities (Pro)
324
350
  ├── See available plugins → list_plugins
325
- ├── Audit TLS certificates → probe_service (Pro) with plugin 040 (scan_host RUNS it but does not return its findings)
326
- ├── Check DNS security (SPF/DKIM/DMARC) → probe_service (Pro) with plugin 060 (scan_host RUNS it but does not return its findings)
327
- ├── Detect debug leaks / CORS issues → probe_service (Pro) with plugin 050 (scan_host RUNS it but does not return its findings)
351
+ ├── Audit TLS certificates → probe_service (Pro) with plugin 040, one port — or scan_host:
352
+ │ `certAudit` on each port 040 audited
353
+ ├── Check DNS security (SPF/DKIM/DMARC) → probe_service (Pro) with plugin 060 — or scan_host on the domain name:
354
+ │ `dnsSecurity` (on the 53/udp record, else in the conclusion's evidence)
355
+ ├── Detect debug leaks / CORS issues → probe_service (Pro) with plugin 050 — or scan_host: `tribeHealth`,
356
+ │ only when TCP 8080 is open
328
357
  ├── Scan a subnet → CLI: --host CIDR --parallel N
329
358
  │ (a private range needs NSA_ALLOW_ALL_HOSTS=1 — §3)
330
359
  ├── Set up continuous monitoring → CLI: --watch --interval N
331
- │ (its webhook does NOT fire on a service change in this release — §5)
360
+ │ (its webhook alerts a changed host carrying a finding at or above
361
+ │ --alert-severity; never on the first cycle — §5)
332
362
  ├── Compare two scans → CLI (Pro): nsauditor-ai report --from <dir> --format executive --since prior
333
363
  │ (not MCP; NEVER by hand)
334
364
  ├── State framework COVERAGE → compliance_matrix (any tier)