nsauditor-ai-agent-skill 1.0.0 → 1.1.1
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 +6 -2
- package/SKILL.md +27 -8
- package/package.json +1 -1
- package/references/plugins.md +2 -2
- package/references/schemas.md +7 -1
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
|
-
**1.
|
|
16
|
+
**1.1.1** (2026-10-26) — docs only, correcting what 1.1.0 taught about a GCP scan with no project or no credentials: the GCP security audit (1021) reads down, but the Cloud Storage auditor (1024) still reads up, so `scan_cloud` can still count GCP audited; read the 1021 row and the gaps, never the provider line alone. Otherwise as 1.1.0, paired with **Enterprise 2.1.0 / Community 1.1.0**. ⚠️ **NOT a Community floor bump: requires Community >= 1.0.0, as 1.0.0 did** (Enterprise 2.1.0, like 2.0.0, imports a name that first ships in Community 1.0.0; below it Enterprise's core does not load while its plugins still do — on Community 0.2.57 as on 0.2.56 — so the scan skips its intelligence, analysis-agent and compliance stages: Community 0.2.56 and later name that on stderr when Enterprise was installed on its own or after Community, and 0.2.55 or older says nothing, the shape measured for Enterprise 2.0.0 and 2.1.0; naming an older Community (below 1.0.0) and Enterprise in ONE `npm install -g` command is different: npm nests a second Community, the newest that satisfies the peer floor, under Enterprise; Enterprise's stages then run against that copy's unverified licence and are skipped with no message and no compliance pack (measured for Enterprise 2.1.0 with Community 0.2.55–0.2.57, npm 11.19). Install the paired versions, or Community first). ⚠️ **Install Node.js 24.22.1 or newer before upgrading** (`engines.node >=24.22.1` in Enterprise 2.1.0 and Community 1.1.0; 2.0.0 and 1.0.0 declared `>=24.0.0`): on an older runtime npm prints an `EBADENGINE` warning and installs anyway, while the air-gap `install.sh` refuses. Node.js 24.22.1 or newer is the supported runtime — the October 2026 Node.js security release for the 24 line. The cloud inventories the scan could not read in full — GCP instances, Azure virtual machines, the AWS EC2 feed of the CIS hardened-image census — are evidence gaps, not empty projects or subscriptions, and a listed AWS region the account has not enabled is skipped and recorded as not enabled, not reported as missing trails or as a permission to grant, wherever the account's enabled regions can be read. Through the MCP server (Community 1.1.0), a host scan stops at a total time limit by default and names the plugins it did not run, a response saved with `NSA_MCP_RESPONSE_DIR` keeps its request beside it for `verify-call`, and a CRITICAL, HIGH or evidence-gap title in `scan_cloud` says when its finding holds more than it shows. New in this release's teaching, in `SKILL.md`'s header items (h)–(l): an unread census inventory is an evidence gap with its cause as a class, never an empty project or subscription; a listed region the account has not enabled is reported as not enabled, never as a permission to grant, wherever the account's enabled regions can be read; an Azure scan with no subscription id reads down, not audited, and a GCP scan with no project or no loadable credentials reports the GCP security audit (1021) down; a control loses measurement per scanner, with its `sources`. Plugin count unchanged at 29 Enterprise plugins; all eight coverage matrices unchanged. See [CHANGELOG.md](https://github.com/nsasoft/nsauditor-ai-agent-skill/blob/main/CHANGELOG.md).
|
|
17
|
+
|
|
18
|
+
**Prior: 1.1.0** (2026-10-26) — the same pairing and teaching, except that its (j) said a GCP scan with no project or no loadable credentials does not read audited, which 1.1.1 corrects.
|
|
19
|
+
|
|
20
|
+
**Prior: 1.0.0** (2026-10-15) — paired with **Enterprise 2.0.0 / Community 1.0.0**. ⚠️ **A floor bump: requires Community >= 1.0.0** (it was >= 0.2.57 — Enterprise 2.0.0 imports a name that first ships in Community 1.0.0; below it Enterprise's core does not load while its plugins still do — on Community 0.2.57 as on 0.2.56 — so the scan skips its intelligence, analysis-agent and compliance stages: Community 0.2.56 and later name that on stderr, and 0.2.55 or older says nothing, the shape measured for Enterprise 2.0.0), and the supported runtime is Node.js 24. Node.js 24 is the supported runtime. Every one-shot compliance scan writes what changed since the previous scan beside its pack — controls to fail or to pass, controls that lost measurement, violations new or cleared, and what could not be measured, never read as cleared — and can notify a webhook, Slack or Teams when a control goes to fail or loses measurement, when a new violation reaches a severity you set, or when the drift could not be compared. A scan connects only to the addresses its host check resolved (Community 1.0.0), the AI payload carries no AWS account ID, and plugin 040's weak-key and weak-signature findings fail the certificate control in six of the eight frameworks.
|
|
17
21
|
|
|
18
22
|
**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
23
|
|
|
@@ -29,7 +33,7 @@ Works with **Claude Code**, **Claude Desktop**, **Cursor**, **Windsurf**, **VS C
|
|
|
29
33
|
|
|
30
34
|
**Prior: 0.2.49** — paired with **Enterprise 0.44.0 / Community 0.2.51**. ⚠️ **NOT a floor bump: Community stays >= 0.2.49.** New in this release's teaching: **there is a Pro `report` subcommand** (`nsauditor-ai report --from <dir> --format executive|jira`) that turns a completed run into a client-facing HTML report or a Jira-importer CSV — with two limits an agent should state rather than let a user discover: the Jira import mapping has not been verified against a live Jira instance, and a value-less flag is a fatal error rather than a silent default; and **an S3 audit-trail gap now means BOTH streams are missing** (plugin 1020 cross-checks CloudTrail data-event coverage before reporting a missing server access log, so a gap disappearing after upgrade on a covered bucket is the fix working, while every uncertain answer — denied read, absent optional SDK, stopped trail, delivery error, prefix-scoped or one-sided selector — keeps the finding unchanged).
|
|
31
35
|
|
|
32
|
-
**Prior: 0.2.48** — paired with **Enterprise 0.43.0 / Community 0.2.50**. ⚠️ **NOT a floor bump: Community stays >= 0.2.49**, derived rather than reflexive — the one Community behaviour the Enterprise fix depends on already shipped there. New in this release's teaching: **a provider badge
|
|
36
|
+
**Prior: 0.2.48** — paired with **Enterprise 0.43.0 / Community 0.2.50**. ⚠️ **NOT a floor bump: Community stays >= 0.2.49**, derived rather than reflexive — the one Community behaviour the Enterprise fix depends on already shipped there. New in this release's teaching: **a provider badge no longer reads audited when that cloud's SDK failed to load** (an Enterprise scan whose cloud SDK failed to load used to report that provider as *audited*, because a plugin result carrying neither an error nor a skip flag falls through to `ran` and `auditedProviders` derives from `ran`; eighteen refusals across seventeen AWS and GCP plugins now carry their cause, so such a provider reads `errored` and drops out — a provider disappearing after upgrade is the fix, not a regression), and **an evidence gap states a cause, never an instruction** (the engine quoted whole thrown messages, so the loaders' `Install: npm install <pkg>@latest` reached the gap text of 7 SOC 2 / 2 GDPR / 12 NIST SP 800-171 controls — never quote a gap line to a user as remediation, and note that under air-gapped operation it cannot be followed at all). The two causes are now distinct — `plugin skipped:` vs `scanner error:` — because the remedies differ; and a GDPR Art. 32 report now fails closed on an Azure refusal where it previously failed nothing.
|
|
33
37
|
|
|
34
38
|
**Prior: 0.2.47** — paired with **Enterprise 0.42.0 / Community 0.2.49**. ⚠️ **A real floor bump: Community >= 0.2.49.**
|
|
35
39
|
New in this release's teaching: non-commercial AWS partitions (GovCloud, China, the ISO partitions and
|
package/SKILL.md
CHANGED
|
@@ -15,7 +15,9 @@ description: >
|
|
|
15
15
|
|
|
16
16
|
# NSAuditor AI — Agent Skill
|
|
17
17
|
|
|
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).
|
|
18
|
+
> **Version:** <!-- nsa:derived id="skill-version" -->1.1.1<!-- /nsa:derived --> (knowledge current as of **EE <!-- nsa:derived id="ee-version" -->2.1.0<!-- /nsa:derived -->** · **requires CE ≥ <!-- nsa:derived id="ce-floor" -->1.0.0<!-- /nsa:derived -->**) — **2.1.0: Node.js 24.22.1 or newer is the supported runtime — the October 2026 Node.js security release for the 24 line. The cloud inventories the scan could not read in full — GCP instances, Azure virtual machines, the AWS EC2 feed of the CIS hardened-image census — are evidence gaps, not empty projects or subscriptions, and a listed AWS region the account has not enabled is skipped and recorded as not enabled, not reported as missing trails or as a permission to grant, wherever the account's enabled regions can be read. Through the MCP server (Community 1.1.0), a host scan stops at a total time limit by default and names the plugins it did not run, a response saved with `NSA_MCP_RESPONSE_DIR` keeps its request beside it for `verify-call`, and a CRITICAL, HIGH or evidence-gap title in `scan_cloud` says when its finding holds more than it shows.** Teach these first: never read a cloud inventory the scan could not read in full as an empty project, subscription or region; never tell a user to grant the audit role access to a region the account has not enabled; never present a violation the drift file lists as could-not-measure as cleared; and never tell a user that a webhook URL a logged `--watch` run printed is safe. Items (h)–(l) are 2.1.0's; (a)–(g), carried forward from 2.0.0 and 1.3.0, still hold. (h) **A cloud inventory the scan could not read in full is an evidence gap (Enterprise 2.1.0).** The CIS hardened-image census (CIS v8 Safeguards 4.1, 4.2 and 4.6) read as complete when its feed came up short: a GCP instance list, an Azure VM list or an AWS EC2 region that failed, was denied or was cut off read exactly as a project, subscription or region with no machines. Each producer now records one constant evidence gap that fails those three Safeguards — `gcp-compute-inventory-unreadable-evidence-gap` (1021, beside `gcp-compute-inventory-evidence-gap` for zones the API named unreachable), `azure-vm-image-inventory-evidence-gap` (1022), `aws-ec2-image-inventory-evidence-gap` (1210) — with the cause as a class in `details.censusLoss` (`unreachable-zones`, `page-lost`, `sdk-not-loaded`, `image-metadata-unread`, `service-off`, `scope-unresolved`, `deadline`), never error text. The machines read before a failure are kept, and one whose image could not be read is counted apart (`imageUnread`), never as an image that did not match. The CIS report's `cisHardenedImage.partial` names each short cloud and its cause, its per-cloud figures name every census cloud ("not scanned this run" where its scanner did not run), and a failing Safeguard row reads "observed (census partial)" or "observed (control failing)", never ✅. No other framework holds a control over the census, so each other framework's pack lists the gap among the gaps routed to no control. Over MCP, `scan_cloud` lists these gaps in its evidence-gap section without the cause, which is in the compliance pack only. The census reads machine images only: no plugin inventories containers, so never offer a container or Docker Hub image as CIS hardened-image evidence. Stated limit: while such a gap stands, `report --since` sets aside that plugin's rows, so a new CRITICAL from the same plugin does not read as new in that comparison. (i) **An AWS region the account has not enabled (Enterprise 2.1.0).** An `all`-regions scan (`--aws-region all`, or `regions: ["all"]` over MCP) walks the regions the account has enabled, for every AWS plugin; it used to send fifteen of the nineteen plugins that use the lookup to the partition's whole list. A LISTED region the account has not enabled is not called: the shared region walker emits one LOW row for it, "Region '<region>' could not be scanned — the region is not enabled for this account (ec2:DescribeRegions). …", an evidence gap, never a clean result (so a plugin's several rows for that region become one, which is not a regression), S3's access-point walk says "ListAccessPoints was not attempted in <region>: the region is not enabled for this account …", and a previous run's rows there read not comparable, never fixed; CloudTrail (1040) records it in `details.regionsNotEnabled` and raises its zero-trails CRITICAL only when it read at least one region; the EC2 auditor (1210) puts it in the census gap as `service-off`. Tell the user to enable the region or drop it from the list — no grant can satisfy it, so never tell them to grant the audit role access there. A single-region or listed scan also names the enabled regions it did not scan (`regionsKnownButNotScanned`). Stated limit: when `ec2:DescribeRegions` is denied the enabled set is unknown, so a listed region is scanned and read as before and an `all` scan walks the partition's list (`resolveError` names the cause); a region named by `AWS_REGION` rather than listed is not covered by this change. (j) **No subscription or project? The scan says why (Enterprise 2.1.0).** With `AZURE_SUBSCRIPTION_ID` unset or blank the four Azure plugins (1022, 1220, 1221, 1222), with no GCP project id (`GCP_PROJECT_ID`, `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID`) or no Google credentials that can be loaded the GCP security audit (1021), now return `up: false` with the cause ("… no API call was made"), and those plugins' controls fail closed as not measured. Azure then reads not audited. GCP can still read audited: the GCP Cloud Storage auditor (1024) reports itself up in both cases, with an evidence gap when no credentials load; with no project id but credentials that load, it can read the project Google's client library finds on its own (a key file's `project_id`, `GCLOUD_PROJECT`, gcloud's default), which the scan did not name, so never present those rows as the user's project. The IAM auditor (1025) reports itself up with an evidence gap when a project is set but no credentials load, and `scan_cloud` counts a provider audited when any of its plugins ran. Read the 1021 row and the gaps, never the provider line alone. The fix is to set the variable or the credentials, never a role grant. Before 2.1.0, 1021 returned `up: false` with no error with no project, and with a project set but no credentials the CLI exited with status 1. (k) **The MCP server (Community 1.1.0).** `scan_host` stops at `SCAN_HOST_WALL_MS` (default 50000; `0` = off) and names the plugins it did not run (`callWall`, the `scan_host` section); with `NSA_MCP_RESPONSE_DIR` set the server saves each response with its request beside it (`<call_id>.request.json`), and the operator checks both with `verify-call --response … --request …` (the Receipts note); a `scan_cloud` CRITICAL, HIGH or evidence-gap title ending `(+N more via get_findings)` is one of that finding's N+1 entries, not the whole finding — drill with `get_findings` by that line's `plugin` and `severity`, and read the entries, which include evidence statements as well as defects, before counting; and a `get_findings` category filter that matches nothing returns `categoriesInScan` (both in the `scan_cloud` notes below). (l) **A scanner that did not run, and lost measurement (Enterprise 2.1.0).** A control loses measurement per SCANNER: a control that already carried one gap now loses measurement again when another scanner's gap arrives, and each `controls.lostMeasurement` entry, in the drift file and the notification, names its `sources` (the Slack and Teams line names them). `--notify-severity` governs new violations only: a control that went to fail or lost measurement always notifies, and a new evidence gap is listed but is not a trigger of its own. Stated limit: the judgement is per scanner, not per scope, so a scanner whose gap on a control WIDENS does not lose measurement there again. When a scanner reports itself down, every control it evidences still carries an evidence-gap row, whose "Why this violates" text is now one sentence: the scanner that evidences this control did not run. A Vanta push's description now says which rows are evidence gaps; the outcome it sends is unchanged (failed), and Drata and Secureframe pushes carry no such line yet. (a) **The drift file and its notification (Enterprise 2.0.0).** Every one-shot compliance scan writes `scan_drift_<fw>.json` beside its pack (never inside `--watch`): controls to fail or to pass, controls that lost measurement, violations new or cleared. "Previous" is the run that started immediately before in the same `--out` (the run `report --since prior` names), else the newest prior pack for the host in `--compliance-history`. A violation is cleared only when this scan looked for it and it was gone; one whose scanner did not run or reported itself down, or whose port or region was not measured, is could-not-measure, with its reason. A TLS check whose handshake failed in transport on a port the port scanner saw open is could-not-measure (Community 1.0.0 records the error class), except when another port of the same scan completed a TLS handshake: then the earlier TLS finding on the failed port can still read as cleared. The notification is opt-in — `--notify-webhook <url>` (public `https` only), `--notify-format generic|slack|teams`, `--notify-severity <sev>` (default `high`), `--notify-payload minimal|full` (default `minimal`) — and is sent when a control goes to fail or loses measurement, whatever the threshold, or a new violation is at or above `--notify-severity`; a drift that could not be compared always notifies. A timeout or a 5xx is retried twice, so a receiver that took a request and then failed can get it up to three times, and a hanging endpoint costs up to about 32 s per framework. The `--watch` alert is separate: `--alert-payload minimal|full`, default `full`, and its banner now prints the webhook host only — if a user ran `--watch` with a Slack or Teams URL where stdout is logged, tell them to rotate that URL. (b) **The resolved-address pin (Community 1.0.0).** A scan connects only to the addresses its host check resolved — CLI and MCP, the target and each redirect hop — except through an HTTP proxy set with `NODE_USE_ENV_PROXY` (which resolves the name itself) or over an idle connection opened earlier to the same name, for as long as it stays idle in the pool (a few seconds by default; a server's Keep-Alive hint holds it longer); an older Community does not pin. One exception, off by default: with `DNS_AUDIT_AXFR_CHECK` set, the DNS-security audit (060) opens TCP/53 to each of the scanned domain's nameservers, at addresses no host check resolved. (c) **A FLOOR BUMP to Node.js 24.22.1; the Community floor stays 1.0.0.** Enterprise 2.1.0 and Community 1.1.0 declare `engines.node >=24.22.1` (2.0.0 and 1.0.0 declared `>=24.0.0`). On an older runtime npm prints an `EBADENGINE` warning and installs anyway — it does not refuse — while the air-gap `install.sh` refuses before installing anything, comparing the whole version (a host on 24.12 is refused). Below 24.22.1 a scanned host that answers with a crafted Brotli body reaches the decoder CVE-2026-111497 names, because Community's web-application detector, its UPnP device-description fetch and plugin 050 read a target's HTTP bodies through Node's `fetch`; reachable is not exploited. Tell a user on an older Node.js 24 to install 24.22.1 or newer before upgrading. The Community floor did not move: Enterprise 2.1.0, like 2.0.0, imports `redactUrlCredentials`, which first ships in Community 1.0.0. Below the floor Enterprise's core does not load while its plugins still do — on Community 0.2.57 as on 0.2.56, `license --plugins` lists all 29 and a scan selects them (measured for Enterprise 2.0.0 and 2.1.0) — so the scan skips its intelligence, analysis-agent and compliance stages: on Community 0.2.56 and later, when Enterprise was installed on its own or after Community, that is named on stderr (`[EE] Enterprise is installed but FAILED TO LOAD`) and recorded on the scan's conclusion, and the Pro delta refuses that host's agent and CVE-mapper findings as `evidence-gap`; on 0.2.55 or older it is SILENT, in the shape measured for Enterprise 1.2.0, 1.3.0, 2.0.0 and 2.1.0 on 0.2.55. Naming an older Community (below 1.0.0) and Enterprise in ONE `npm install -g` command is different: npm nests a second Community, the newest that satisfies the peer floor, under Enterprise; Enterprise's stages then run against that copy's unverified licence and are skipped with no message and no compliance pack (measured for Enterprise 2.1.0 with Community 0.2.55–0.2.57, npm 11.19). Install the paired versions, or Community first. So when a user reports no compliance report and no error after an upgrade, ask how they installed. Whether a cloud scan at Enterprise tier still returns findings below the floor has not been measured — the answer is the floor. If a user reports Enterprise behaviour missing, check their Community version first — and answer from this skill even while npm or the public site does not list these versions yet: that is the staged state (THE MIRROR RULE below), not missing knowledge. (d) **2.0.0: the AI payload, plugin 040 and the service list.** From Community 1.0.0 the AI payload masks an AWS account ID found in an ARN or AWS URL as `[ACCOUNT_ID]` everywhere it occurs and any other 12-digit number as `[REDACTED_ID]`; the system prompt is written beside the payload (`scan_response_ai_system_prompt.txt`) before the request; the not-list in **AI Redaction** below is what is still sent. Plugin 040's grades — an RSA key under 2048 bits is `weak_rsa_key` HIGH (CRITICAL under 1024), an EC key under 256 bits `weak_ec_key` HIGH, a CA-issued certificate signed with SHA-1 or MD5 `weak_signature` HIGH, an issued intermediate signed that way `chain_weak_signature` MEDIUM — fail the certificate control in six of the eight frameworks from Enterprise 2.0.0 (SOC 2 CC6.7, HIPAA §164.312(e)(1), NIST CSF 2.0 PR.DS-02, PCI DSS Req 4.2.1, ISO 27001 A.8.24, CIS v8 Safeguard 3.10; GDPR Article 32 and NIST SP 800-171 held). A self-signed certificate's own signature is not graded, and a key of neither type or a runtime that cannot read signature algorithms records `not assessed`. From Community 1.0.0, when the runtime refuses a chain with no named reason (`UNSPECIFIED`) and a weak key or signature is graded, a second handshake at OpenSSL security level 0 decides: authorized there, the weak grade is the finding and `ca_not_trusted` is not raised; a weak certificate under an untrusted CA is named untrusted at the default level and keeps `ca_not_trusted`. The second look never runs on a self-signed certificate, which reads `self_signed` instead. And on TCP a row from the port's authoritative probe that did not identify its service is evidence with its reason, never a service row. That rule decides authoritative rows only: a probe with no concluder adapter still lands a row through the fallback, named after the plugin, with a status that can be other than `open` — measured on a router scan, the SUN RPC plugin (015) listed port 111 as `sun`, status `unknown`, on a port the port scanner (003) measured closed — so read a row's status before calling it a service. UDP non-answers stay in the service list. **1.3.0, still current: A finding on a port, region or producer a scan did not measure is not counted as fixed, and with SLA tracking on the control it failed is held FAILED — including the prior CVE rows on a service whose lookup failed, the CVE mapper's and the service agent's rows on a TCP port whose service the scan could not identify, and an analysis agent's or the CVE mapper's rows when a plugin they read was left out of the scan or did not complete. Two measured limits: a scan made before EE 1.3.0 could not record a plugin left out of it, so in a comparison with one, an agent's row that scan lacks is not refused — the report's Basis cell says so on the row; and a scan that discovered ports with the Nmap plugin (024) alone records no port oracle, so an analysis agent's or the CVE mapper's row on a port it did not measure can read RESOLVED and count as closed in MTTR, and the control it failed can read PASS — include the port scanner (003).** Never report a row in either of its two limits as fixed, or its control as passing. (e) **The NOT-COMPARABLE bucket catches four more cases.** A UDP-transport finding whose port did not answer in the other run (for a CVE row, also one that answered without identifying its service) is `port-not-measured`, listed with a `/udp` suffix; a CVE row whose LOOKUP failed in the other run is `evidence-gap` — the mapper's own `[COVERAGE GAP] <gapClass> — <protocol>/<service>` record is a coverage gap that opened or cleared, never an exposure; and a finding of an analysis agent that DID NOT RUN (`[COVERAGE GAP] AGENT NOT RUN — <agent> did not run: <cause class>`) is `evidence-gap`, while that agent's controls fail closed in every framework; and a CVE row that VANISHED — present in the baseline, absent now — while the SAME program and version answered on its port in both runs is `vulnerability-data-changed` — the mapper attributes on program and version alone, so the vulnerability data moved (NVD's answer, a cached answer's age, an offline store), not the estate: never present it as a fix, and in MTTR it is not a closed finding. Neither the delta's rule nor MTTR's reaches a port that answers with two different identified programs or versions, and MTTR's does not reach a prior row that recorded no program or version. A CVE that APPEARED on an unchanged service stays NEW with the basis note "the service is unchanged, the vulnerability data is not" — new knowledge about an old service, not a new exposure on the estate. Every detail names its run absolutely: "this run" is the current one, "the baseline run" the earlier. (f) **A compliance control can FAIL on a finding the PRIOR scan recorded.** When this scan did not measure the port or the probe that finding was on, did not cover its region, did not run what produced it, or (for a CVE row) could not complete its CVE lookup, the COMPLIANCE report (`nsauditor-ai scan --host <target> --compliance <fw> --out <dir> --sla-policy sla-policy.json` — never `report --since`, which does not list these records) holds the control failed with a record titled `<prior title> — [COVERAGE GAP] PORT NOT MEASURED — …` (or `PROBE NOT MEASURED` / `SCOPE NOT SCANNED` / `PRODUCER NOT RUN` / `LOOKUP NOT MEASURED`), counted among "Evidence gaps (not findings)". Never present it as a current finding: it says the surface was NOT measured, and it clears when a later scan measures it. It needs the compliance history (`--compliance-history <dir>`, or `--sla-policy <file>`, which reads the history kept under `--out`) and a prior `--compliance <fw>` scan of the SAME host in it (the history is read per framework and per host) — a first scan holds nothing, and neither does a prior scan run without `--compliance <fw>`; if the hold could not be evaluated, the report's warnings say so. (g) **1.3.0: the bucket and the hold reach further.** A CVE-mapper or service-agent row on a TCP port the other run saw open, where that run could not identify the service (program AND version), is `port-not-measured` — those producers attribute on the identity, so its absence there is not a measurement. An analysis agent's or the CVE mapper's row when a plugin it reads was LEFT OUT of a scan is `evidence-gap`: from Enterprise 1.3.0 the narrowed scan records `[COVERAGE GAP] INPUT GAP — … was not requested in this scan …`, and MTTR withholds the prior row and holds its control FAILED — but a run made before 1.3.0 cannot carry that record, so against one such a row is not refused, and its Basis cell says so. After a CVE lookup that failed, the controls the prior CVE rows failed are now held FAILED (`LOOKUP NOT MEASURED`, item (f)). The exposure agent's titles are port-keyed now (`Database port <p> open`, `Management port <p> open`) and the service agent's end-of-life titles no longer carry the version (`End-of-life <program> on port <p>`), so their rows compared across the 1.2.0 → 1.3.0 upgrade read `identity-basis-changed`, never RESOLVED or NEW — rescan after upgrading to compare. When the user will compare scans or track SLA, tell them to include the port scanner (003): a scan that discovered ports with the Nmap plugin (024) alone records no port oracle (the second limit). And the DNS-posture audit (060) DECLINES an IP-address target — status `skipped`, reason `DNS posture needs a domain name — …` — so `dnsSecurity` appears only for a domain name; on an IP-target compliance scan the controls its findings route to (SOC 2 CC6.1, CIS v8 9.5) are held as an evidence gap, with a warning that it could not run, never PASS. The TLS certificate audit (040) grades a name mismatch by the target's form: scanned by ADDRESS, a certificate that names DNS names only reads LOW, its detail ending `the certificate names DNS names only (…); a client connecting by address sees a name mismatch, expected where the service is reached by name`, so it no longer trips `--fail-on high` on its own; a DNS-name target the certificate does not name, a certificate naming a DIFFERENT address and one with no subjectAltName stay HIGH. A certificate with NO subjectAltName — many consumer routers serve one, naming the device only in its CN — says so: `the certificate carries no subjectAltName — modern clients reject it for any name (CN … is not checked)`, because a modern client checks no name in it. 040 also reads IP SANs now (Node prints them `IP Address:`), so a certificate naming the scanned address raises no mismatch, and a name mismatch on a chain the CA store verified no longer adds `ca_not_trusted`. A self-signed certificate still raises `self_signed` HIGH, so a router scanned by address can still fail `--fail-on high`. 040's key-size and signature checks fire on real servers from Community 0.2.57 (they had read fields Node never sets): an RSA key under 2048 bits is `weak_rsa_key` HIGH (CRITICAL under 1024), an EC key under 256 bits `weak_ec_key` HIGH, a CA-issued certificate signed with SHA-1 or MD5 `weak_signature` HIGH, and an issued intermediate signed that way `chain_weak_signature` MEDIUM. A self-signed certificate's own signature is not graded — no client verifies it — and the `self_signed` finding names the algorithm instead. A key of neither type (Ed25519), or a runtime that cannot read signature algorithms (Node 22 or older; Node 24 reads them), records `not assessed` on `certAudit`, never a pass. So a router serving a 1024-bit key now also shows `weak_rsa_key` HIGH. From Enterprise 2.0.0 they fail the certificate control — item (d). The teaching that follows is 0.2.53's, carried forward and still current. — the delta release: the product can now answer *what changed since the last scan*. Teach five things: (1) **`nsauditor-ai report --from <dir> --format executive --since <runId|prior>`** (`--format` is REQUIRED; `report` refuses without it) (Pro/Enterprise) compares two runs and reports new / resolved / severity-changed findings. (2) **NEVER present a `resolved` count as remediation on its own** — the report carries a NOT-COMPARABLE bucket with a reason per finding (host not scanned · the producing plugin did not run, ERRORED or TIMED OUT on that host, or cannot be identified · an evidence gap such as AccessDenied · **a PORT that was not measured on that host — a check that could not complete its connection, or a port that STOPPED ANSWERING between the two runs (a port that went quiet is not a fix)** · **the two runs COVERED DIFFERENT SCOPES — a narrower `--aws-region`, a different Azure subscription or GCP project — so that surface was not looked at, which is NOT the same as clean** · framework enumeration moved · **the PRODUCER CHANGED WHAT IT NAMES between the two releases**), and a finding that vanished for any of those reasons was not fixed. ⚠️ **THE LAST ONE ARRIVED IN EE 1.1.0 AND IT FIRES ON AN UPGRADE ACROSS 1.1.0.** FOURTEEN producers changed what identifies a finding at EE 1.1.0 — thirteen plugins that had left the object empty, named the REGION, recorded no region the report reads, carried a HIGH sentence that had to be corrected (1110, the effective-decrypt auditor), or (1023, the Zero Trust checker) now name the port a finding is about and keep the open-port count out of its title, and one analysis AGENT whose findings are keyed on their TITLE because they carry no object field at all. A comparison that straddles that upgrade reports those rows NOT-COMPARABLE instead of differencing them, so one surface under two keys is never read as a fix plus a fresh exposure. **So the first `report --since` a user runs across the 1.1.0 upgrade will legitimately show a LARGE not-comparable bucket — that is the declaration working, not a broken report.** Derive the reason list from `NOT_COMPARABLE_REASONS` in Community's `utils/scan_delta.mjs` rather than from this sentence; it is held in equality with what the engine emits, and this sentence is prose that has disagreed with it before. If a user asks what got fixed, read the NOT-COMPARABLE list before answering. **Two conditions refuse the whole comparison rather than bucketing findings**: the two runs straddle a product boundary where a reported number changed meaning, and the two runs ran at DIFFERENT LICENCE TIERS — the set of producers that ran differs, so a whole producer's findings would read as remediation. ⚠️ **AND ONE OF THOSE REASONS IS DECLARED RATHER THAN CHECKED: framework-enumeration movement is NOT EVALUATED in this edition**, because a run record carries no framework enumeration. The report says so as a limit on every comparison and each row's basis reads `framework enumeration: not evaluated`. Never tell a user that framework movement was ruled out. (3) **Integrity is a hash chain, not a signature — and say WHAT is sealed.** At finalize the run record AND each written host's findings files are digested over their exact persisted bytes, with the per-file digests sealed inside the record, which is itself digested and chained through `prevDigest`. So an altered **findings file** — which is what the comparison actually reads — is detected and NAMED, not merely an altered index; a file that did not exist at seal time is sealed as an explicit absence, so adding one is detected too. **BOTH runs are verified, not only the baseline.** An altered or unmeasurable run on either side REFUSES the comparison outright, and the refusal names which side. A file that cannot be READ is reported as unmeasurable rather than as tampering, and a record written before per-host sealing keeps its verdict while its reason says the findings files were not covered. The guarantee is tamper-EVIDENT against corruption and unsophisticated edits, **NOT tamper-proof against host-level access and NOT non-repudiation**. Do not describe it as file-integrity monitoring or a change-detection mechanism: PCI DSS `11.5.1` / `11.5.2` / `11.6.1` are enumerated OUT OF SCOPE in the shipped matrix. The code ships in Community; the capability is licence-gated to Pro/Enterprise. ⚠️ **`--since` REQUIRES COMMUNITY 0.2.55 OR LATER — AND ENTERPRISE 2.0.0 REQUIRES 1.0.0 (item (c) above).** The peer floor moved to 0.2.55 at EE 1.1.0 because **Enterprise CALLS the new Community code** — the per-plugin run statuses the Zero Trust checker reads before it will assert a posture, and the run-record fields `report --since` compares. The earlier floor said otherwise on the grounds that Enterprise did not call it; that stopped being true at EE 1.1.0. If a user reports the flag is unrecognised, check their Community version first. (4) **A `kms:Decrypt on Resource:*` HIGH from 1110 is NEVER downgraded on a live run.** 1110 reads KMS key policies and grants in the configured region only, and one region cannot show that NO key trusts a principal — a key in another region may — so the HIGH→INFO downgrade it used to apply is withheld on every shipped path. The reason is in that plugin's run summary as `summary.kmsDowngrade` (`reason` `single-region-enumeration` for a complete read; `access-denied` · `per-key-error` · `keys-truncated` · `grants-truncated` for an incomplete one, which also emits an evidence-gap row). Never tell a user a surviving HIGH was checked against every key, and never read a missing downgrade as a scanner fault. Its grant finding (`kms-grant-decrypt-no-identity-grant`, MEDIUM) likewise covers keys in the configured region only. (5) **The NVD store and response cache MOVED at EE 1.1.0 and every run records where they are.** They used to be a `.nvd-cache` folder in whatever directory the scan started from; they now resolve, from any directory, to `NVD_CACHE_DIR`, else `NSAUDITOR_NVD_CACHE_DIR`, else `~/.nsauditor/nvd_cache`. Only `feed import` (of a feed you downloaded yourself) takes `--cache-dir`; a scan does not read that flag, so it reads a store imported with `--cache-dir <d>` only when that chain resolves to `<d>` (e.g. `NVD_CACHE_DIR=<d>`). The MCP `get_vulnerabilities` cache reads `NSAUDITOR_NVD_CACHE_DIR`, never `NVD_CACHE_DIR`. An old `.nvd-cache` folder is NOT read, and the scan says so. If a user reports that offline CVE matching stopped after upgrading, that is the cause: move the folder once or set `NVD_CACHE_DIR` to it. Never read the resulting coverage gaps as a clean host. Plugin count UNCHANGED at <!-- nsa:derived id="plugins:ee" -->29<!-- /nsa:derived -->; <!-- nsa:derived id="matrix-movement" -->all eight coverage matrices unchanged since EE 2.0.0<!-- /nsa:derived -->; Community <!-- nsa:derived id="ce-version" -->1.1.0<!-- /nsa:derived --> (published with this skill).
|
|
19
|
+
>
|
|
20
|
+
> **Prior: 1.0.0** (post-EE-2.0.0 · **requires CE ≥ 1.0.0**) — **2.0.0: Node.js 24 is the supported runtime. Every one-shot compliance scan writes what changed since the previous scan beside its pack — controls to fail or to pass, controls that lost measurement, violations new or cleared, and what could not be measured, never read as cleared — and can notify a webhook, Slack or Teams when a control goes to fail or loses measurement, when a new violation reaches a severity you set, or when the drift could not be compared. A scan connects only to the addresses its host check resolved (Community 1.0.0), the AI payload carries no AWS account ID, and plugin 040's weak-key and weak-signature findings fail the certificate control in six of the eight frameworks.** Its teaching — the drift file and its notification (a), the resolved-address pin (b), the AI payload and plugin 040 (d) — still holds and is carried forward in the header above; its runtime sentence is superseded by the Node.js 24.22.1 floor, item (c).
|
|
19
21
|
>
|
|
20
22
|
> **Prior: 0.2.55** (post-EE-1.3.0 · **requires CE ≥ 0.2.57**) — the 1.3.0 release: what a scan did not measure is not counted as fixed, with two measured limits. Its register, its limits and its teaching still hold and are carried forward in the header above; its Community 0.2.57 floor is superseded by the 1.0.0 floor, item (c).
|
|
21
23
|
>
|
|
@@ -87,7 +89,7 @@ description: >
|
|
|
87
89
|
> UNCHANGED at CE ≥ 0.2.49** — derived, not a bump.
|
|
88
90
|
>
|
|
89
91
|
> **Prior: 0.2.48** — what EE 0.43.0 taught, and both items are about READING a scan rather than running one.
|
|
90
|
-
> **(1) A provider badge
|
|
92
|
+
> **(1) A provider badge no longer reads audited when that cloud's SDK failed to load.** An Enterprise scan whose
|
|
91
93
|
> cloud SDK failed to load reported that provider as **audited**: eighteen refusals across
|
|
92
94
|
> seventeen AWS and GCP plugins returned `up: false` with the cause written only into
|
|
93
95
|
> `warnings`, and the Community plugin manager derives `auditedProviders` from plugins that
|
|
@@ -161,6 +163,14 @@ skill teaches you how to operate it via MCP tools and CLI.
|
|
|
161
163
|
NSAuditor AI exposes tools via Model Context Protocol (stdio transport). Available tools
|
|
162
164
|
depend on the license tier (Community / Pro / Enterprise).
|
|
163
165
|
|
|
166
|
+
> **Receipts.** Every tool response ends with a `── MCP call receipt ──` footer and a `call_id`. You cannot verify a
|
|
167
|
+
> response yourself, and you cannot save one — never call a response verified. The operator checks one with
|
|
168
|
+
> `nsauditor-ai mcp verify-call <call_id> --response <file>`, which needs the WHOLE response; when the client showed it
|
|
169
|
+
> truncated, tell them about `NSA_MCP_RESPONSE_DIR` (below): with it set, the server saves each response as
|
|
170
|
+
> `<call_id>.txt` and its request as `<call_id>.request.json`, and the footer says "A copy of this response was saved
|
|
171
|
+
> as <call_id>.txt, and its request as <call_id>.request.json, in the directory NSA_MCP_RESPONSE_DIR names." only
|
|
172
|
+
> when it did. The operator checks both with `--response <file> --request <file>`; you cannot read either file.
|
|
173
|
+
|
|
164
174
|
### Community Edition Tools (always available)
|
|
165
175
|
|
|
166
176
|
#### `scan_host`
|
|
@@ -172,9 +182,16 @@ order (discovery → service probes → OS detection → result fusion).
|
|
|
172
182
|
| `host` | string | ✅ | — | Target hostname or IP address |
|
|
173
183
|
|
|
174
184
|
Every plugin runs under the server's `PLUGIN_TIMEOUT_MS` (default 30000), including a plugin that
|
|
175
|
-
declares a longer budget of its own;
|
|
185
|
+
declares a longer budget of its own; the call takes no timeout argument. A plugin that runs out of time
|
|
176
186
|
reads `timeout` in the result's `manifest` — its surface was NOT measured, which is never a clean
|
|
177
|
-
result.
|
|
187
|
+
result. **The whole call stops at `SCAN_HOST_WALL_MS`** (default 50000; `0` = off), because a client may end a
|
|
188
|
+
tool call at a limit of its own (Claude Desktop 3.x at about 60 s) and return nothing. When it stops, the response's
|
|
189
|
+
`callWall.reached` is `true`, `callWall.notStarted` and `callWall.cutShort` list the plugins that did not measure
|
|
190
|
+
(in the order they would have run), a `note` says the scan is partial, and the `markdown` opens with the same. **Say
|
|
191
|
+
so in your answer: name those plugins and say the result is partial — never present the services found as the host's
|
|
192
|
+
whole posture.** The plugins that run late (the TLS Scanner 011, the TLS certificate audit 040) are the first to be
|
|
193
|
+
lost; for a complete scan the operator runs the CLI, or raises `SCAN_HOST_WALL_MS` in a client that does not cut the
|
|
194
|
+
call.
|
|
178
195
|
|
|
179
196
|
⚠️ **What `scan_host` returns, and what it does NOT.** Services, the OS, and the flags the service checks leave on
|
|
180
197
|
each service record: weak SSH algorithms, an SNMP default community, weak TLS protocols / ciphers and a self-signed
|
|
@@ -209,7 +226,7 @@ analysis-agent findings, and plugins 040 and 060 had recorded 2 and 3 HIGH findi
|
|
|
209
226
|
response then. Those audit entries land on the records now and are counted; the CVEs and the agent findings still are
|
|
210
227
|
not.
|
|
211
228
|
|
|
212
|
-
**Returns:** `{ host, conclusion, manifest[], pluginsRan, markdown }` — `conclusion.result` is the fused
|
|
229
|
+
**Returns:** `{ host, conclusion, manifest[], pluginsRan, markdown, callWall{ limitMs, reached, notStarted[], cutShort[] } }` (plus a `note` when `callWall.reached`) — `conclusion.result` is the fused
|
|
213
230
|
record (`summary` is a one-line string; `host`, `services[]`, `evidence[]`), and `manifest[]`
|
|
214
231
|
is every plugin's `{ id, name, status, reason, duration_ms }`; `pluginsRan` is the number of `manifest[]` entries
|
|
215
232
|
whose status is `ran`. There is no `findings` array. See
|
|
@@ -336,11 +353,11 @@ These tools return a license upgrade prompt on CE installations:
|
|
|
336
353
|
| Tool | Tier | Purpose |
|
|
337
354
|
|------|------|---------|
|
|
338
355
|
| `scan_cloud` | Enterprise | Audit one or more cloud accounts (AWS / GCP / Azure) for security & compliance posture using the server-configured credentials. No network host needed. Input: `{ providers?: ("aws"\|"gcp"\|"azure")[], regions?: string[] }` — **pass only the cloud(s) the user names** (`providers:["aws"]` for "audit my AWS account"); omit `providers` only when the user asks to audit ALL clouds. Use this (not `scan_host`) when the user asks to "audit my AWS account", "audit my AWS and Azure accounts", or "check my cloud compliance". CE/Pro callers get an upgrade message. **`regions` (AWS only)** — AWS region codes (e.g. `["us-east-1","eu-west-1"]`) or `["all"]`. **Default — omit `regions` (MOST requests):** a plain "audit my AWS account", a "quick check", or any request that names no region AND does not explicitly ask for all/every/whole-account/complete/full coverage → **OMIT `regions`** (the auditors that take their region from the client audit the server-configured `AWS_REGION` and no other; the three that enumerate their own region list — CloudTrail trail discovery (1040), GuardDuty/Inspector (1200), EC2 instances (1210) — still attempt every enabled region; do NOT fan out or batch). Omitting does NOT scan all regions with every auditor. **Specific regions:** when the user names region(s), pass exactly those. **All regions — ONLY on an explicit "all regions / every region / whole account / full coverage" request:** use the discover-then-batch approach in the region-scope note below — do NOT pass a single `["all"]` call and rely on it (it fans every regional plugin across all enabled regions and usually EXCEEDS the host's MCP tool-call timeout, e.g. Claude Desktop's, returning nothing). Unknown region codes are rejected before the scan runs (the WHOLE call fails — fix the region and re-call; never silently drop it). |
|
|
339
|
-
| `get_findings` | Enterprise | Drill into the findings of the MOST RECENT `scan_cloud` scan — a per-provider, **per-session** cache (NOT live state; cleared when the MCP server restarts). Input: `{ scanId?, provider?, plugin?, severity?, category?, cursor?, limit? }`. Use it AFTER `scan_cloud` when the summary's **category rollup** shows a category you want to expand to specific resources, or when you need the FULL untruncated text of a finding. Pass the **`scanId` from the `scan_cloud` summary footer** + the `provider`; filter by `category`/`severity`/`plugin`; paginate with `cursor`/`limit` (server-capped at 20 — follow `nextCursor`). If you get a **"re-run scan_cloud"** error the cache was cleared or superseded — **re-run `scan_cloud`, do NOT retry `get_findings`**. CE/Pro callers get the same upgrade message as `scan_cloud`. |
|
|
356
|
+
| `get_findings` | Enterprise | Drill into the findings of the MOST RECENT `scan_cloud` scan — a per-provider, **per-session** cache (NOT live state; cleared when the MCP server restarts). Input: `{ scanId?, provider?, plugin?, severity?, category?, cursor?, limit? }`. Use it AFTER `scan_cloud` when the summary's **category rollup** shows a category you want to expand to specific resources, or when you need the FULL untruncated text of a finding. Pass the **`scanId` from the `scan_cloud` summary footer** + the `provider`; filter by `category`/`severity`/`plugin`; paginate with `cursor`/`limit` (server-capped at 20 — follow `nextCursor`). If you get a **"re-run scan_cloud"** error the cache was cleared or superseded — **re-run `scan_cloud`, do NOT retry `get_findings`**. **A `category` filter that returns `total: 0` also returns `categoriesInScan`** — every category this scan holds, with its `total` and the `plugins` that emitted it, whatever `plugin` or `severity` you passed — and a `note`. Read both: if the category is listed, it exists in this scan at another severity or from another plugin, so say which; if it is not listed, say this scan had no finding in that category — **never say the release does not, or cannot, emit it, nor that it may belong to a later or different release** — nothing in the response tells you that. CE/Pro callers get the same upgrade message as `scan_cloud`. |
|
|
340
357
|
|
|
341
|
-
> **Interpreting `scan_cloud` results — never report a false clean:** read **`findingsSummary`** for the findings — it maps each provider to `counts` (per-severity totals) and a `findings` list of the CRITICAL/HIGH items (`{severity, plugin, title}`); report those. A cloud was effectively audited only if it appears in `auditedProviders`. If the result has `audited: false`, any `notes` entries, or `pluginsRan: 0`, the cloud was **NOT** audited (no plugins, missing credentials, or skipped) — report the gap explicitly; an empty result is **not** a clean pass. Do not infer "clean" from an empty `findingsSummary` when the cloud is not in `auditedProviders`. **Beyond CRITICAL/HIGH, `findingsSummary[provider].rollup` groups the remaining findings by `category` with counts (count-descending) across **MEDIUM, LOW *and* INFO** — and the INFO tier is not optional reading: every deferred-scope boundary is INFO, and an evidence gap carries its own finding's severity, which can be any tier up to HIGH, so read every tier. ⚠️ This sentence said "MEDIUM + LOW" until EE 0.39.0, which was FALSE against the summariser's own three rollup buckets, and false in the direction that hides the tier a reader most needs. Report every tier that occurs; these are actionable too: a category like `sqs-age-alarm-missing` or `*-public` is a real gap, not noise, and reporting only CRITICAL/HIGH while the rollup is non-empty is itself a false clean. To enumerate the specific resources behind a rollup category, or to read a finding's full untruncated text, call `get_findings` with the `scanId` from the summary footer + the `category`.**
|
|
358
|
+
> **Interpreting `scan_cloud` results — never report a false clean:** read **`findingsSummary`** for the findings — it maps each provider to `counts` (per-severity totals) and a `findings` list of the CRITICAL/HIGH items (`{severity, plugin, title}`); report those. A title that ends `(+N more via get_findings)` is one of that finding's N+1 entries, not the whole finding: call `get_findings` with the `scanId`, that line's `plugin` and `severity`, and read the row whose `resource` begins the title — its `text` holds every entry. The entries include evidence statements as well as defects, so read them before counting anything. A cloud was effectively audited only if it appears in `auditedProviders`. If the result has `audited: false`, any `notes` entries, or `pluginsRan: 0`, the cloud was **NOT** audited (no plugins, missing credentials, or skipped) — report the gap explicitly; an empty result is **not** a clean pass. Do not infer "clean" from an empty `findingsSummary` when the cloud is not in `auditedProviders`. **Beyond CRITICAL/HIGH, `findingsSummary[provider].rollup` groups the remaining findings by `category` with counts (count-descending) across **MEDIUM, LOW *and* INFO** — and the INFO tier is not optional reading: every deferred-scope boundary is INFO, and an evidence gap carries its own finding's severity, which can be any tier up to HIGH, so read every tier. ⚠️ This sentence said "MEDIUM + LOW" until EE 0.39.0, which was FALSE against the summariser's own three rollup buckets, and false in the direction that hides the tier a reader most needs. Report every tier that occurs; these are actionable too: a category like `sqs-age-alarm-missing` or `*-public` is a real gap, not noise, and reporting only CRITICAL/HIGH while the rollup is non-empty is itself a false clean. To enumerate the specific resources behind a rollup category, or to read a finding's full untruncated text, call `get_findings` with the `scanId` from the summary footer + the `category`.**
|
|
342
359
|
|
|
343
|
-
> **Reporting `scan_cloud` region scope — never overstate coverage:** Report the regions you ACTUALLY scanned, derived from the `regions` you **passed** — NOT from the findings. If you OMITTED `regions`, the auditors that take their region from the client scanned only the server-default region (`AWS_REGION`) — say that, and that the account's OTHER enabled regions were NOT covered by them (offer to re-run for all regions). The three auditors that enumerate their own region list — CloudTrail trail discovery (1040), GuardDuty/Inspector (1200), EC2 instances (1210) — attempt every enabled region when `regions` is omitted. Say one of them covered every region ONLY if it `ran` and carries no multi-region evidence gap (a single-region fallback, or errored or denied regions); any region such a gap names was NOT audited, say so. 1040's CloudWatch-alarm and AWS Config checks cover the configured region only. Never call the whole scan single-region when these three ran. **Never escalate a single-region or "quick" request into a multi-region scan.** Do NOT claim "all regions" / "every region" / "across N regions" for the scan as a whole because those three list per-region findings: their coverage is NOT evidence the other auditors ran outside the region(s) you passed. When you DO pass `regions`, their region-scoped checks follow the list you passed.
|
|
360
|
+
> **Reporting `scan_cloud` region scope — never overstate coverage:** Report the regions you ACTUALLY scanned, derived from the `regions` you **passed** — NOT from the findings. If you OMITTED `regions`, the auditors that take their region from the client scanned only the server-default region (`AWS_REGION`) — say that, and that the account's OTHER enabled regions were NOT covered by them (offer to re-run for all regions). The three auditors that enumerate their own region list — CloudTrail trail discovery (1040), GuardDuty/Inspector (1200), EC2 instances (1210) — attempt every enabled region when `regions` is omitted. Say one of them covered every region ONLY if it `ran` and carries no multi-region evidence gap (a single-region fallback, or errored or denied regions); any region such a gap names was NOT audited, say so. 1040's CloudWatch-alarm and AWS Config checks cover the configured region only. Never call the whole scan single-region when these three ran. **Never escalate a single-region or "quick" request into a multi-region scan.** Do NOT claim "all regions" / "every region" / "across N regions" for the scan as a whole because those three list per-region findings: their coverage is NOT evidence the other auditors ran outside the region(s) you passed. When you DO pass `regions`, their region-scoped checks follow the list you passed — except a listed region the account has not enabled (from Enterprise 2.1.0, wherever `ec2:DescribeRegions` can be read): no plugin calls it, and its row reads "Region '<region>' could not be scanned — the region is not enabled for this account", an evidence gap. Report that region as NOT scanned because it is not enabled, never as audited, clean or missing a permission; `["all"]` walks the regions the account has enabled.
|
|
344
361
|
|
|
345
362
|
> **Full all-region coverage — discover then batch** (use ONLY when the user explicitly asked for all/every/whole-account/complete/full region coverage; NEVER for a plain or "quick" request — those stay on the `regions` default above, with no explicit fan-out): a single `regions:["all"]` call usually exceeds the host's MCP tool-call timeout (e.g. Claude Desktop's) and returns nothing. Reliable pattern: (1) run a default scan (omit `regions`) — its GuardDuty/Inspector findings enumerate every enabled region, giving you the full list while the region-scoped auditors cover the default region; (2) scan the REMAINING regions in small batches (3–5 region codes per `regions:[...]` call) across successive calls until every enabled region is covered; (3) merge and report the TOTAL number of regions actually covered — **count** them, don't guess. If you try `["all"]` and it times out, that result is INCOMPLETE — fall back to batching and continue until complete; never report a timed-out or partial scan as full coverage.
|
|
346
363
|
> **⚠️ COMPLIANCE IS A CLI SURFACE, NOT AN MCP TOOL.** `compliance_check` is WITHDRAWN — there is no such tool and there never was, and the evidence pack is produced by `nsauditor-ai scan --host <target> --compliance <fw> --out <dir>`, which runs the compliance PHASE and writes `scan_compliance_<framework>.{json,md,html}` plus its attestation and chain-of-custody sidecars. `scan_cloud` **never runs that phase** and maps no finding to any framework, so no MCP call produces a pack. Use `compliance_matrix` to state COVERAGE (what is covered / partial / out of scope); use the CLI to produce EVIDENCE.
|
|
@@ -644,6 +661,8 @@ See `references/schemas.md` for complete structures:
|
|
|
644
661
|
| `DNS_CHECK_AXFR` / `DNS_AXFR_DOMAIN` | false / — | Let the DNS check try a zone transfer of that zone — without both, `axfrAllowed` stays null (not tested) |
|
|
645
662
|
| `DNS_AUDIT_AXFR_CHECK` | false | Let the DNS-security audit (060) try a zone transfer of the scanned domain against each of its nameservers (`1`, `true`, `yes` or `on`). It connects to each nameserver's own address, outside the scan's host check, and records only an open transfer |
|
|
646
663
|
| `ENABLE_SYN_SCAN` | false | Enable Nmap TCP SYN scanning (requires root) |
|
|
664
|
+
| `SCAN_HOST_WALL_MS` | 50000 | MCP server only: the time limit for the whole `scan_host` call (`0` = off), so a client that ends tool calls at about 60 s still gets a disclosed partial — read `callWall` |
|
|
665
|
+
| `NSA_MCP_RESPONSE_DIR` | unset (off) | MCP server only: an ABSOLUTE directory where the server saves each tool response, exactly as returned, as `<call_id>.txt`, with the call's request (tool and arguments, verbatim) beside it as `<call_id>.request.json`, both or neither (directory `0700`, files `0600`, never overwriting), so `verify-call` works on a response the client truncated and can check what the call asked for (`--request`). Set in the client config's `env` block, then fully quit and reopen the client. The files hold scan results and are never removed by the server |
|
|
647
666
|
|
|
648
667
|
### Plugin-Specific Timeouts
|
|
649
668
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nsauditor-ai-agent-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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",
|
package/references/plugins.md
CHANGED
|
@@ -267,7 +267,7 @@ not check a website's 80/443.
|
|
|
267
267
|
| ID | Name | Tier | Purpose | SOC 2 Controls |
|
|
268
268
|
|----|------|------|---------|----------------|
|
|
269
269
|
| 1020 | AWS Cloud Scanner | Enterprise | S3 bucket hardening (PAB, encryption, versioning, Object Lock, MFA Delete) + object-access audit trail as a JOIN of server access logging AND CloudTrail S3 data-event coverage — a bucket a trail records is not a gap; any uncertain answer keeps the finding | CC7.1 / C1.1 / C1.2 / A1.2 |
|
|
270
|
-
| 1021 | GCP Cloud Scanner | Enterprise | Firewall rule audit + IAM bindings + Storage bucket public-access | CC6.1 / CC6.6 / C1.1 |
|
|
270
|
+
| 1021 | GCP Cloud Scanner | Enterprise | Firewall rule audit + IAM bindings + Storage bucket public-access + Compute image inventory (feeds CIS v8 4.1 / 4.2 / 4.6 only) | CC6.1 / CC6.6 / C1.1 |
|
|
271
271
|
| 1022 | Azure Cloud Scanner | Enterprise | NSG rule analysis + RBAC role assignments + Storage account hardening | CC6.1 / CC6.3 / CC6.6 / C1.1 / A1.2 |
|
|
272
272
|
| 1023 | Zero Trust Checker | Enterprise | Segmentation + encryption + identity + lateral movement scoring (reads OBSERVED open ports) | CC6.1 / CC6.2 / CC6.6 / CC6.7 |
|
|
273
273
|
| 1024 | GCP Cloud Storage Auditor (EE 0.6.8) | Enterprise | Multi-cloud parity sister of plugin 1020 AWS S3. 6 dimensions: bucket-level IAM public bindings (allUsers = CRITICAL, allAuthenticatedUsers = HIGH); Uniform Bucket-Level Access enforcement (closes legacy bucket-ACL false-PASS class); Object Versioning; Bucket Lock retention policy (SEC 17a-4 / FINRA 4511 WORM-alignment); CMEK via Cloud KMS (four-tier custody ladder); bucket-level access logging. NEW SDK dep `@google-cloud/storage`. | CC6.1 / CC6.6 / CC7.1 / C1.1 / C1.2 / A1.2 |
|
|
@@ -293,7 +293,7 @@ not check a website's 80/443.
|
|
|
293
293
|
| 1210 | AWS EC2 Instance Auditor (**NEW EE 0.13.1** — first new plugin since 1200; plugin count 24 → 25; the AWS producer for CIS-Hardened-Image detection + EC2 instance-level + EBS-encryption coverage) | Enterprise | Audits EC2 instances (orthogonal to plugin 1170 which audits the SG perimeter policy). **Multi-region** via ec2:DescribeRegions (single-region fallback emits an evidence-gap). Dimensions: **IMDSv1 enabled** (CC6.1 — MEDIUM when an IAM instance profile is attached / LOW without; **IMDSv2 hop-limit > 1** re-opens container credential theft) + **EBS volume unencrypted** (C1.1 + CIS 3.11 — HIGH; resolves every attached BlockDeviceMappings volume via DescribeVolumes) + **account default-EBS-encryption disabled** (C1.1 preventive — GetEbsEncryptionByDefault) + **public-IP exposure** incl. secondary-ENI/EIP + IPv6 GUA (CC6.6 substrate, INFO) + **instance-store (ephemeral) volume** evidence-gap. **DIM 4 — AMI inventory** → result.cisImageInventory (the producer feed that makes CIS-Hardened-Image detection LIVE on Safeguards 4.1/4.2/4.6; Azure 1022 + GCP 1021 feed the same contract). Conservative classifier: LOW + evidenceGap on indeterminate metadata; AccessDenied → INFO + evidenceGap (never silent-PASS); terminated/shutting-down instances skipped. Survived 3 review rounds / 5 adversarial skill lenses (network-security-audit + CIS + IAM-effective-permissions + soc2-evidence + cloud-plugin-false-negatives). `@aws-sdk/client-ec2` reused. | CC6.1 / C1.1 / CC6.6 |
|
|
294
294
|
| 1220 | Azure Storage Account Data-Protection Auditor (**NEW EE 0.13.2** — Move C-2; **+2 dims EE 0.13.3** — Move C-2.1; plugin count 25 → 26; first dedicated Azure auditor beyond the multi-purpose 1022 scanner) | Enterprise | Audits the Azure Storage Account data-protection surface across **7 dims** — **orthogonal to plugin 1022** which owns the network-exposure dims (no double-emission; mirrors the AWS 1020 S3-scanner + 1120 S3-lifecycle two-plugin split). (1) **HTTPS-only transit** (`enableHttpsTrafficOnly` — HIGH on plaintext HTTP; CC6.7) + (2) **minimum TLS version** (< TLS1_2 → MEDIUM; CC6.7) + (3) **Shared Key authorization** (`allowSharedKeyAccess` — bypasses Azure AD identity; CC6.1; Azure field-default discipline: absent = ENABLED, never silent-PASS) + (4) **infrastructure (double) encryption** (`requireInfrastructureEncryption`; C1.1) + (5) **encryption key source incl. CMK reachability + rotation** (`encryption.keyVaultProperties` — keySource=Microsoft.Keyvault PASS only when the key is currently resolvable + auto-rotating; a disabled/revoked/version-pinned CMK degrades, not silent-PASS; C1.1) + (6) **blob recoverability** (soft-delete + versioning via the secondary `blobServices.getServiceProperties` path — A1.2; EE 0.13.3) + (7) **per-container anonymous public access** (account-toggle-aware via `blobContainers.list` — `publicAccess=Blob/Container` + allowBlobPublicAccess=true → HIGH effective exposure; C1.1; EE 0.13.3). Conservative classifier: indeterminate field / AccessDenied / secondary-path failure → evidence-gap (never silent-PASS); `for await` pagination. Single-subscription scope surfaced explicitly. Built + reviewed through the cloud-plugin-false-negatives lens. `@azure/arm-storage` + `@azure/identity`. | CC6.7 / CC6.1 / C1.1 / A1.2 |
|
|
295
295
|
| 1221 | Azure NSG Perimeter Auditor (**NEW EE 0.14.0** — Move C-2.2; **UDP lane EE 0.14.1**; plugin count 26 → 27; the Azure analog of AWS plugin 1170) | Enterprise | A CC6.6 network-segmentation perimeter auditor for Azure Network Security Groups — **non-overlapping-by-depth with plugin 1022's flat per-rule NSG lint** (no double-emission of a verdict; mirrors the AWS 1023-observed / 1170-declared two-plugin precedent). Evaluates each NSG's inbound rules in **Azure priority order** (first match wins; DenyAllInbound default): (1) **all-protocol (`*`) public Allow** — every port reachable from the public internet + (2) **public-source (`*`/`0.0.0.0/0`/`Internet`) to a restricted TCP port** (SSH/RDP/MSSQL/MySQL/Postgres/Redis/Memcached/MongoDB/Elasticsearch/CouchDB/SMB/WinRM/Oracle/Docker/Kubelet) + (3) **`::/0` IPv6-wildcard to a restricted port** (the dimension 1022's flat lint misses) + **(2u/3u) public-source / `::/0` to a restricted UDP service** (DNS/TFTP/rpcbind/NTP/NetBIOS/SNMP/CLDAP/IKE/Syslog/IPMI/OpenVPN/MSSQL-Monitor/SSDP/IPsec-NAT-T/Memcached — closes the public-UDP false negative, **EE 0.14.1**) + (4) **public→non-restricted port** INFO substrate + (5) **PASS substrate** when no public restricted exposure. **Attachment-aware severity** via the read-only `nsg.subnets[]`/`networkInterfaces[]` back-references — attached → CRITICAL (effective exposure); orphaned → MEDIUM (latent). Per-transport effective priority/deny-override resolution + port-range expansion + `0.0.0.0/1` split-range coverage + service-tag/ASG-source normalization (VirtualNetwork/AzureLoadBalancer/ASG = not public). Conservative classifier: denied/indeterminate listAll → evidence-gap; one malformed NSG degrades per-resource (never aborts the scan); `for await` pagination; single-subscription scope explicit. Built + reviewed through the cloud-plugin-false-negatives lens (EE 0.14.0 SHIP-WITH-FOLDS 2 LOW; EE 0.14.1 UDP lane SHIP-WITH-FOLDS 2 MED + 1 LOW, all folded). `@azure/arm-network` + `@azure/identity`. | CC6.6 |
|
|
296
|
-
| 1222 | Azure Key Vault Deep Auditor (**NEW EE 0.15.0** — Move C-2.3; plugin count 27 → 28; the third dedicated Azure auditor after 1220 storage + 1221 NSG; **hotfixed EE 0.15.1** — Dim-3 SDK-shape + Dim-4 inherited-admin severity re-tune) | Enterprise | The deep Key Vault auditor — the KV analog of how 1221 deepens 1022's flat NSG dim; **orthogonal to plugin 1022's vault-property dims** (purge/soft-delete/network-ACL/RBAC-mode) — no double-emission of a verdict. Enumerates each vault's **keys, role assignments, and diagnostic settings** across 4 dims: (1) key **auto-rotation policy** — keys with no `rotationPolicy` / no rotate-action → finding + (2) key **expiry** — keys with no `expires` (or already-expired), with epoch-s/ms/Date/string coercion (closes the ms-overflow false-clean) + (3) **diagnostic logging → Log Analytics** — vaults with no `AuditEvent`→workspace diagnostic setting; reads `@azure/arm-monitor`'s `diagnosticSettings.list()`
|
|
296
|
+
| 1222 | Azure Key Vault Deep Auditor (**NEW EE 0.15.0** — Move C-2.3; plugin count 27 → 28; the third dedicated Azure auditor after 1220 storage + 1221 NSG; **hotfixed EE 0.15.1** — Dim-3 SDK-shape + Dim-4 inherited-admin severity re-tune) | Enterprise | The deep Key Vault auditor — the KV analog of how 1221 deepens 1022's flat NSG dim; **orthogonal to plugin 1022's vault-property dims** (purge/soft-delete/network-ACL/RBAC-mode) — no double-emission of a verdict. Enumerates each vault's **keys, role assignments, and diagnostic settings** across 4 dims: (1) key **auto-rotation policy** — keys with no `rotationPolicy` / no rotate-action → finding + (2) key **expiry** — keys with no `expires` (or already-expired), with epoch-s/ms/Date/string coercion (closes the ms-overflow false-clean) + (3) **diagnostic logging → Log Analytics** — vaults with no `AuditEvent`→workspace diagnostic setting; reads `@azure/arm-monitor` 8's `diagnosticSettings.list()` by iterating every page of its paged async-iterator (EE 2.1.0 — monitor 7 returned an `await`-ed `{value:[]}` collection, which the EE 0.15.1 H-1 fix read; a return of any other shape is an evidence gap, never an empty list), soft-degrades to an evidence-gap if the SDK cannot be loaded + (4) **privileged-access depth** — RBAC `roleAssignments` (admin/data-plane/**scope-aware severity**: vault-scoped control-plane god roles + Key Vault Administrator at any scope → HIGH; inherited subscription/management-group-scope Owner/User-Access-Administrator → MEDIUM, inherited Contributor → LOW — the EE 0.15.1 H-2 re-tune that stops flagging the ubiquitous inherited-admin reality as HIGH on every RBAC vault) + legacy `accessPolicies` breadth (export + wide-crypto permissions). **Secret/cert expiry is a deliberate data-plane scope boundary** (explicit caveat — the control-plane mgmt API does not expose data-plane object expiry). Conservative classifier: indeterminate field / AccessDenied / arm-monitor absent → evidence-gap; one malformed vault degrades per-resource (never aborts the scan); single-subscription scope explicit. Findings route across every shipped framework (SOC 2 CC6.3/C1.1/CC6.1/CC7.2 · HIPAA §164.312(a)(2)(iv)/(b)/(a)(1) · NIST CSF PR.DS-01/DE.CM-09/PR.AA-05 · PCI DSS 3.5.1/10.2.1/7.2.1 · ISO 27001 A.8.24/A.8.15/A.5.15+A.8.2 · CIS v8 3.11/8.2/5.4 · GDPR Art.32(1)(b)-confidentiality + Art.32(1)(b)-integrity · NIST SP 800-171 3.1.2/3.1.4/3.1.5/3.1.7/3.3.1/3.5.3/3.13.10) — every coverage matrix UNCHANGED at that release. Built + reviewed through the cloud-plugin-false-negatives lens (SHIP-WITH-FOLDS 0C/3H/2M/4L; 4 folded). `@azure/arm-keyvault` + `@azure/arm-monitor` + `@azure/arm-authorization` + `@azure/identity`. | CC6.3 / C1.1 / CC6.1 / CC7.2 |
|
|
297
297
|
| 1230 | AWS DocumentDB Auditor (**NEW EE 0.41.0** — plugin count 28 → 29; owns the `docdb` engine on the shared RDS control plane, closing the measured false clean where an unencrypted DocumentDB cluster read CLEAN under the RDS auditor's Aurora-only cluster filter) | Enterprise | Seven dimensions: storage encryption at rest + KMS custody classification (customer-alias PASS / AWS-managed MEDIUM / key-UUID and foreign-account custody stay evidence gaps); `tls` cluster parameter (disabled = HIGH); `audit_logs` parameter + CloudWatch `audit` export (generating-but-unexported is its own MEDIUM); automated backup retention vs the 7-day floor; deletion protection; replica/AZ availability topology under the CLUSTER lens (never the per-instance Multi-AZ flag — that is RDS semantics and does not apply); manual-snapshot `restore`-attribute probes (`all` = publicly restorable CRITICAL; external account ids = HIGH). Non-docdb engines counted and skipped; DocumentDB Elastic clusters declared deferred. Paired 1140 change: docdb/neptune engine-filtered with counted skips + a standing disclosure — a Neptune estate is explicitly UNAUDITED. | CC6.1 / CC6.6 / CC7.2 / C1.1 / A1.2 |
|
|
298
298
|
|
|
299
299
|
---
|
package/references/schemas.md
CHANGED
|
@@ -74,7 +74,8 @@ Complete data structures for all MCP tool inputs and outputs.
|
|
|
74
74
|
}
|
|
75
75
|
],
|
|
76
76
|
"pluginsRan": 1,
|
|
77
|
-
"markdown": "# NSAuditor AI Scan Report …"
|
|
77
|
+
"markdown": "# NSAuditor AI Scan Report …",
|
|
78
|
+
"callWall": { "limitMs": 50000, "reached": false, "notStarted": [], "cutShort": [] }
|
|
78
79
|
}
|
|
79
80
|
```
|
|
80
81
|
|
|
@@ -85,6 +86,11 @@ Complete data structures for all MCP tool inputs and outputs.
|
|
|
85
86
|
`error`. A `timeout` or `error` means that surface was **NOT measured** — never read it as clean.
|
|
86
87
|
- `pluginsRan` is the number of `manifest[]` entries whose status is `ran` — in this example, one of the two entries
|
|
87
88
|
shown. It counts plugins, never ports or results.
|
|
89
|
+
- `callWall` is on every response: the call's total time limit (`SCAN_HOST_WALL_MS`, `limitMs` 0 when off) and
|
|
90
|
+
whether it was `reached`. When it was, `notStarted` (plugins the limit reached before they started) and `cutShort`
|
|
91
|
+
(plugins stopped at it) list, in the order they would have run, plugins whose `manifest[]` entries read `timeout`;
|
|
92
|
+
a `note` field and the head of `markdown` say the scan is partial. Never read such a result as the host's whole
|
|
93
|
+
posture.
|
|
88
94
|
- There is **no `findings` array** in a `scan_host` result, and no `techniques`: the CLI adds ATT&CK
|
|
89
95
|
techniques to its own report, and this tool does not.
|
|
90
96
|
|