nsauditor-ai-agent-skill 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,9 @@ Works with **Claude Code**, **Claude Desktop**, **Cursor**, **Windsurf**, **VS C
13
13
 
14
14
  ## Current release
15
15
 
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).
16
+ **1.1.2** (2026-11-01) — paired with **Enterprise 2.1.1 / Community 1.1.1**; Community 1.1.1 is recommended beside Enterprise 2.1.1, and the floor is unchanged. ⚠️ **NOT a Community floor bump: requires Community >= 1.0.0, as 1.0.0 did** (Enterprise 2.1.1, like 2.1.0 and 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` since 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. The GCP Cloud Storage (1024) and IAM (1025) auditors report themselves down, with the cause, when no GCP project is set or no usable credential loads, as the GCP security audit (1021) does. Community 1.1.1 moves its OpenAI client to `openai` 7, so a project that depends on it holds one `zod` at 4.6.6 or later, with no `overrides`; its AI stage says at the head of the assessment when the provider reports reading less than half of the input, by the stage's own estimate; a tier denial names the tools the caller can use now; and `nsauditor-ai license` with an unknown subcommand, or none, exits 2. New in this release's teaching, in `SKILL.md`'s header: (j) rewritten, since Enterprise 2.1.1's GCP Cloud Storage and IAM auditors report themselves down with the cause, as the security audit does; (m) an AI assessment whose files open with the partial-input line covers part of the scan; (n) a tier denial names the tools the caller can use now; (o) `license` with an unknown subcommand, or none, exits 2. `compliance_matrix` is taught as open at every tier but needing the Enterprise package installed.
17
+
18
+ **Prior: 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
19
 
18
20
  **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
21
 
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.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).
18
+ > **Version:** <!-- nsa:derived id="skill-version" -->1.1.2<!-- /nsa:derived --> (knowledge current as of **EE <!-- nsa:derived id="ee-version" -->2.1.1<!-- /nsa:derived -->** · **requires CE ≥ <!-- nsa:derived id="ce-floor" -->1.0.0<!-- /nsa:derived -->**) — **2.1.1: The GCP Cloud Storage (1024) and IAM (1025) auditors report themselves down, with the cause, when no GCP project is set or no usable credential loads, as the GCP security audit (1021) does. Community 1.1.1 moves its OpenAI client to `openai` 7, so a project that depends on it holds one `zod` at 4.6.6 or later, with no `overrides`; its AI stage says at the head of the assessment when the provider reports reading less than half of the input, by the stage's own estimate; a tier denial names the tools the caller can use now; and `nsauditor-ai license` with an unknown subcommand, or none, exits 2.** Teach these first: never present an AI assessment whose files open with the partial-input line as covering the whole scan; never read a GCP plugin that is down with a cause as a project with nothing to report; 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 (m)–(o) are 2.1.1's, and (j) is rewritten for it; (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: a row that says the region is not enabled for this account means the account's enabled set was read, so no grant applies. When `ec2:DescribeRegions` is denied the enabled set is unknown: a listed region is scanned and read as before, and no `resolveError` is set for a listed scan (it is set only for an `all` scan and for the implicit default region); CloudTrail (1040) alone records the cause, as `enabledRegionSetUnknown`; an `all` scan walks the partition's list, and there `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; the other two GCP plugins from 2.1.1).** With `AZURE_SUBSCRIPTION_ID` unset or blank the four Azure plugins (1022, 1220, 1221, 1222), and with no GCP project id (`GCP_PROJECT_ID`, `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID`) or no usable Google credential the GCP plugins — the security audit (1021) from Enterprise 2.1.0, the Cloud Storage (1024) and IAM (1025) auditors from 2.1.1 — 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. On Enterprise 2.1.0, GCP can still read audited: 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; and 1025 reports itself up with an evidence gap when a project is set but no credentials load. `scan_cloud` counts a provider audited when any of its plugins ran. One limit holds on 2.1.1 too: without impersonation, resolving a credential fetches no token, so a key file that loads but is revoked, or a principal that lacks the IAM permission, still fails at the first call and stays that plugin's LOW evidence gap with `up: true`. Read the 1021, 1024 and 1025 rows 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. (m) **An AI assessment of part of the scan says so (Community 1.1.1).** After every AI response the CLI compares the input tokens the provider reports with its own estimate of the input (characters ÷ 4). When the provider reports reading less than half of it, `scan_response_ai.json` records `nsauditorMeta.inputCoverage` (`inputTruncated: true`, `providerInputTokens`, `estimatedInputTokens`) beside the raw response, `scan_response_ai.txt` and `.html` open with one line — *the provider reports reading N tokens of an input estimated at M; this assessment covers part of the scan* — and the run record carries the same figures for that host. Measured on 2026-10-28: a local Ollama model, with Ollama's settings left at their defaults, read 2,050 tokens of an input estimated at 19,992. When the line is there, never present the assessment as covering the scan; say what share it covers, and tell the user to check the model's context length in their Ollama configuration and re-run. A response with no input count is recorded as unknown, never as whole, and the threshold was not measured on the Claude provider. (n) **A tier denial names the tools the caller can use now (Community 1.1.1).** A tool refused for the caller's licence used to name one fixed list (`scan_host, list_plugins, compliance_matrix`) whatever the caller held. It now ends with "Tools available to you now:", derived from the licence and the install: `probe_service` and `get_vulnerabilities` with a Pro or Enterprise licence, `compliance_matrix` only when the Enterprise package resolves. Offer the user what that line names, never a tool from memory. (o) **`nsauditor-ai license` with an unknown subcommand, or none, exits 2 (Community 1.1.1).** It printed the usage and exited 0, so a mistyped or missing subcommand read as success. It now prints `unknown license subcommand: <name>` or `missing license subcommand` and the usage on stderr, and exits 2; `license --help` exits 0. The status flag is `license --status`, not `license status`. Read exit 2 as a usage error to fix, never as a licence verdict. (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.1.0<!-- /nsa:derived -->; Community <!-- nsa:derived id="ce-version" -->1.1.1<!-- /nsa:derived --> (published with this skill).
19
+ >
20
+ > **Prior: 1.1.1** (post-EE-2.1.0 · **requires CE ≥ 1.0.0**) — **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.** Its teaching — items (h)–(l) — still holds and is carried forward in the header above, with (j) rewritten for 2.1.1's GCP plugins; no clause of it is superseded.
19
21
  >
20
22
  > **Prior: 1.0.0** (post-EE-2.0.0 · **requires CE ≥ 1.0.0**) — **2.0.0: Node.js 24 is the supported runtime. Every one-shot compliance scan writes what changed since the previous scan beside its pack — controls to fail or to pass, controls that lost measurement, violations new or cleared, and what could not be measured, never read as cleared — and can notify a webhook, Slack or Teams when a control goes to fail or loses measurement, when a new violation reaches a severity you set, or when the drift could not be compared. A scan connects only to the addresses its host check resolved (Community 1.0.0), the AI payload carries no AWS account ID, and plugin 040's weak-key and weak-signature findings fail the certificate control in six of the eight frameworks.** Its teaching — the drift file and its notification (a), the resolved-address pin (b), the AI payload and plugin 040 (d) — still holds and is carried forward in the header above; its runtime sentence is superseded by the Node.js 24.22.1 floor, item (c).
21
23
  >
@@ -256,6 +258,8 @@ Out of scope, with the control ids and the per-group out-of-scope reasons.
256
258
  **ALWAYS call this before stating or tabulating any coverage matrix, and quote what it returns.** Do NOT derive a
257
259
  matrix from the plugin inventory, from a scan result, or from documentation — coverage is a property of the shipped
258
260
  framework maps, not of the plugin list, and a derived matrix will disagree with the customer's own report.
261
+ It is open at every licence tier but reads the framework maps the Enterprise package ships: with Enterprise not
262
+ installed it returns an error that names the licence the package needs. Say that, and state no matrix.
259
263
 
260
264
  ⚠️ **QUOTE the severity header the scan printed — never re-type it from the findings you read.** `scan_cloud` renders one line per provider, `N CRITICAL · N HIGH · N MEDIUM · N LOW · N INFO · N PASS`, and **the INFO column is load-bearing: it carries the evidence gaps and the deferred-scope boundaries.** Re-typing that line while summarising is how the column goes missing — measured 2026-09-18, a GCP reply reported `5 CRITICAL · 2 HIGH · 11 MEDIUM · 0 LOW · 11 PASS` over a header that read `… 0 LOW · 3 INFO · 11 PASS`. Every other number was right. **This is the exact regression the product itself fixed at EE 0.32.11**, when a header could read `…0 LOW · 17 PASS` over 62 unreported INFO records — reproduced one layer up, in the retelling. If you report tiers at all, report every tier the product printed, INFO included, even when it is zero.
261
265
 
@@ -556,7 +560,7 @@ User wants to...
556
560
  │ vanished unmeasured as fixed; report --since refuses it, but
557
561
  │ the header's two measured limits still apply — compare runs
558
562
  │ made by Enterprise 1.3.0 or later, and include the port scanner 003)
559
- ├── State framework COVERAGE → compliance_matrix (any tier)
563
+ ├── State framework COVERAGE → compliance_matrix (any tier; needs the Enterprise package installed, which it reads)
560
564
  └── Produce a compliance EVIDENCE PACK → CLI with --compliance (not MCP)
561
565
  ```
562
566
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai-agent-skill",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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",
@@ -361,7 +361,7 @@ User wants to...
361
361
  │ --alert-severity; never on the first cycle — §5)
362
362
  ├── Compare two scans → CLI (Pro): nsauditor-ai report --from <dir> --format executive --since prior
363
363
  │ (not MCP; NEVER by hand)
364
- ├── State framework COVERAGE → compliance_matrix (any tier)
364
+ ├── State framework COVERAGE → compliance_matrix (any tier; needs the Enterprise package installed, which it reads)
365
365
  ├── Produce a compliance EVIDENCE PACK → CLI: --compliance <fw> --out <dir> (not MCP)
366
366
  ├── Generate a formatted report → CLI: --output-format sarif|csv|md (not MCP)
367
367
  └── Produce a CLIENT-FACING report → CLI (Pro): nsauditor-ai report --from <dir>