One command puts what is new, what is resolved and what changed severity into the self-contained HTML report you send the client. When the report can see a finding was not measured the same way twice, it sets that finding aside as not comparable, reason on the row, instead of counting it as fixed — and its two measured limits are stated plainly, not buried (see how it works). Underneath: exploit-first triage that puts CISA KEV-listed vulnerabilities, and those with an EPSS score of 0.10 or more, at the top of the queue (from the KEV and EPSS data you load), plus CVE matching, MITRE ATT&CK technique annotations and composite risk scoring.
Consultants hear it at the re-test. MSPs hear it at the quarterly review. A simple before-and-after diff answers fast and can cost you credibility. A finding vanishes because its host was left out of the re-test, a plugin timed out, the CVE lookup failed, or NVD stopped matching a CVE to software that never changed — and the diff shows it as fixed. That line can end up in front of a client’s board. Nobody fixed it: the scanner stopped looking, or the data moved under it.
With Pro, one command —
nsauditor-ai report --from <dir> --format executive --since <runId|prior>
— compares two scan runs and writes new, resolved and
changed-severity findings into the print-ready executive HTML report you hand the client. No
side-by-side spreadsheets, no rows copied from one report into the next.
A re-test is rarely the same scan twice. A slow plugin gets dropped from
--plugins, or the SSH
probe is left out and port 22 still answers, but nothing identified what is on it. In the previous release, the
rows that depended on what was left out read resolved. In the two cases below the report now
sets them aside instead of crediting a fix (the two measured limits are stated further down):
port-not-measured),
reported neither as fixed nor as new, until a rescan can read the program and version.22 UDP findings (since EE 1.2.0). We compared a real router scan with a twin of it that drops its 22 UDP rows. The release before the rule reported all 22 resolved; with the rule, the delta reported all 22 not comparable: the re-test never recorded those ports as closed or answering, so it does not get to call them fixed. That router pair was measured when the rule shipped; the 1.3.0 release test re-checked the rule on UDP rows of its own.
5 dnsmasq CVEs (since EE 1.2.0). A pre-publish test run caught this one. NVD stopped matching
five CVEs to dnsmasq 2.78 on a router that was still answering with dnsmasq 2.78. The earlier delta called all
five resolved, and the compliance pack counted them as closed findings with a mean time to remediate of about a
day. On the same pair of scans, the delta with the rule read 0 resolved and 5 not comparable — and the
1.3.0 release test reads the same on a local dnsmasq 2.78 pair — reason
vulnerability-data-changed
— the vulnerability data changed, not your estate.
Before a missing finding is called resolved, the comparison looks at how each run measured it. When the report can see the two runs did not measure it the same way, it files the finding under NOT-COMPARABLE, with the reason on the row. Among the reasons:
producer-unknown);evidence-gap);evidence-gap);evidence-gap);probe-not-measured);port-not-measured);identity-basis-changed);vulnerability-data-changed).And the report tells your client in plain words: a finding that could not be compared is not a finding that was fixed. When someone asks why a finding was set aside, the answer is on its row — along with which way it went: absent from this run, or appeared in it.
--plugins identical between the scans you compare.
This release states its two measured limits up front, so you meet them here and not in front of a client.
One: 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.
Two: 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 on Enterprise, with SLA tracking on, it can count as closed in MTTR and the control it
failed can read PASS.
Include the port scanner (003) in scans you compare.
On Enterprise, the same rule reaches the
compliance report. With SLA tracking on
(--compliance-history <dir>
or --sla-policy <file>)
and a prior --compliance
scan of the same host in the history, a control that failed on a finding whose port, probe, region or producer
the new scan did not measure is held FAILED, with the gap leading its explanation and counted
among the evidence gaps — never shown as a current finding. New in 1.3.0, that now includes the controls
behind a failed CVE lookup (a LOOKUP NOT MEASURED
record) and the controls behind an analysis agent’s rows when a plugin it reads was left out; and SLA age
and time to remediate in the HIPAA, NIST CSF, PCI DSS, ISO 27001, CIS, GDPR and NIST SP 800-171 packs no
longer reset on every scan, with a history written by any earlier release read correctly and no pack
regenerated (stated limit: two findings whose prose differs only by a control id would share one identity; none
does among the emitted texts, and a test holds that at zero). The second limit above reaches it too: on a scan that discovered ports with Nmap alone, a row on
a port it did not measure can count as closed in MTTR and its control can read PASS.
Some pairs are refused outright, with no rows guessed: two runs at different licence tiers, or two runs on either side of a release that changed what the reported findings count means. A refused comparison exits 2, so a script cannot mistake it for a clean delta; when the executive report is still written, its body says no comparison was made.
A quarter-over-quarter story needs a baseline that is still there and still intact. Pro keeps your scan history past the Community 7-day window, and for every scan run on 1.1.0 or later, the run record and each host’s findings files are sealed with SHA-256 digests and chained to the previous run. Both runs are checked first. If a sealed findings file was corrupted, partly restored or changed by an ordinary edit, the report names it and refuses the comparison — so you find out at report time, not in front of the client.
Already scanning? Baselines written since EE 1.0.0 are compared across the upgrade; one scanned before 1.1.0 carries no integrity digest, and the report says so. From your first two scans on 1.1.0 or later, both sides of the comparison are sealed.
The --since comparison is a Pro capability, included in Enterprise; its code
ships in the free Community package and your licence unlocks it. The watch-mode last-vs-current webhook alert
delta stays free.
# a client LAN is a private range: set NSA_ALLOW_ALL_HOSTS=1 for these scans — # the SSRF guard refuses RFC 1918 and loopback addresses by default # Q3 · the baseline scan, into the client’s engagement directory nsauditor-ai scan --host 192.168.1.0/24 --plugins all --out ~/engagements/acme/ # Q4 · the re-test, into the SAME directory with the SAME --plugins, so the report # finds both runs and compares like with like nsauditor-ai scan --host 192.168.1.0/24 --plugins all --out ~/engagements/acme/ # the newest run against the one before it, rendered into the executive report nsauditor-ai report --from ~/engagements/acme/ --format executive --since prior \ --out ~/engagements/acme/Acme_Q4_since_Q3.html # or name the baseline yourself: --since <runId> — the <id> in the baseline’s # scan_run_<id>.json at the --out root. report prints it as [report] baseline: runId …
npm i -g nsauditor-ai@0.2.57 @nsasoft/nsauditor-ai-ee@1.3.0 # Move both in one command. With Community below the floor shown under this box, # the scan skips Pro's CVE matching and analysis agents: since Community 0.2.56 # that is named on stderr; an older Community says nothing. # Rescan after upgrading before you compare: across the 1.2.0 -> 1.3.0 upgrade the # exposure agent's, the service agent's and plugin 1160's rows changed what identifies # them, so the report sets them aside (identity-basis-changed) rather than counting # them as fixed or new. Coming from before 1.1.0? The same reason covers the findings # whose identity 1.1.0 sharpened. # Also since 1.1.0: the offline NVD store default is ~/.nsauditor/nvd_cache # (NVD_CACHE_DIR or NSAUDITOR_NVD_CACHE_DIR override it). An old .nvd-cache # folder is no longer read, and a scan run from its directory says so. # Move it once, from the directory that holds it: mkdir -p ~/.nsauditor/nvd_cache && mv .nvd-cache/* ~/.nsauditor/nvd_cache/
Needs nsauditor-ai v0.2.57+ alongside your Pro or Enterprise licence — upgrade both packages.
A scan ends with a directory of artifacts. An engagement ends with something a client reads.
nsauditor-ai report closes that gap: point it at a finished run and it renders an
executive HTML report — one self-contained, print-ready file that
opens with no external network reference of any kind. It survives an email
attachment, a laptop with no connectivity, and a reviewer’s locked-down browser.
Add --brand when the report goes out under your own name and the cover page carries it.
--format jira writes a Jira-importer CSV instead, so a findings list
becomes a backlog. ⚠️ That CSV is written for Jira’s own CSV importer — the field
mapping is done inside Jira, and this is not a live-instance integration we have verified.
brand.jsonEvery field is optional. Omit the file and the report renders unbranded under its default title.
// brand.json — put it next to your logo { "title": "Q3 External Perimeter Review", "companyName": "Acme Manufacturing GmbH", "preparedBy": "J. Rivera, Security Consulting LLC", "contact": "security@example.com", "logoPath": "acme-logo.png" }
title replaces the headline;
companyName is who the report is
for; preparedBy is who prepared it.
logoPath is a local PNG or JPEG,
resolved next to the brand file and embedded into the HTML as a data URI.
That logo rule is strict on purpose, and every part of it protects the same promise —
that the file opens with no external network reference. A URL is refused and never fetched:
a remote logo is egress at render time and a tracking pixel every time your client opens the report.
SVG is refused, because SVG is markup that can carry a script and break the no-script guarantee from
inside the image. The path may not escape the brand file’s own directory, checked as typed and
again after symlinks resolve. The cap is 2 MB, and the format is read from the file’s
magic bytes rather than its extension — renaming an SVG to
.png does not get it past the check.
A brand file that cannot be read, is not valid JSON, or whose logo is refused is a fatal error
and no report is written: a flag you passed is never silently dropped, because that is how a report goes
out unbranded without anyone noticing.
A report that quietly skips a container reads exactly like a report that had nothing to skip. So every report carries a container census: where finding-like records sit in a place the report does not read, that place is named and counted on the page. Silence is disclosed — never rendered as absence.
report is a Pro capability and is included in Enterprise. It produces no PDF: the
HTML is print-ready, and your browser prints it when a PDF is what the client asked for.
# 1 · a normal scan, as always nsauditor-ai scan --host 192.168.1.0/24 --plugins all # 2 · turn that finished run into the document you send — --from is the scan’s # output root (out/ by default); report picks its newest run unless you pass --run <id> nsauditor-ai report --from out/ --format executive # or the same run as a Jira-importer CSV — you map the fields in Jira nsauditor-ai report --from out/ --format jira
Requires nsauditor-ai v0.2.57+ alongside your Pro or Enterprise licence. The report reads a run that
already finished — it never re-scans, and it never reaches the network to render.
A consulting firm audits a client’s gateway at 192.168.1.1
and has to hand back something the client can actually read. Six steps — the same six on a
/24, on a cloud account, or on a hundred hosts.
nsauditor-ai license --status # On Community, `report` refuses at exit 2 and writes no file: # "`report` is a Pro capability. Your licence resolves to tier "community". # No file has been written." # The other precondition is not a command: signed authorization for the # target range. The scope on that letter is what the report cover should match.
~/consulting/brand/ ├── brand.json └── acme-logo.png # The logo must sit inside this directory and be named by a plain relative # path. Checked as typed, and again after symlinks resolve.
# 192.168.1.1 is a private address: set NSA_ALLOW_ALL_HOSTS=1 for this scan — # the SSRF guard refuses RFC 1918 and loopback addresses by default nsauditor-ai scan --host 192.168.1.1 --plugins all \ --compliance all --out ~/engagements/acme-2026-09/ # Writes a run record `scan_run_<id>.json` at the root and a timestamped # evidence folder beside it, with a .sha256 sidecar per artifact. # One --out per engagement. `report` picks the NEWEST run in a directory and # only refuses on an exact start-time tie — so a directory holding two # engagements can render the wrong one silently. Name the run when in doubt.
# --run names YOUR OWN run: the <id> in the scan_run_<id>.json at the --out root # (report prints it back as [report] runId …). The one below is an example. nsauditor-ai report --from ~/engagements/acme-2026-09/ \ --run 20260904T184352Z-d9d796 \ --format executive \ --brand ~/consulting/brand/brand.json \ --out ~/engagements/acme-2026-09/Acme_192.168.1.1_executive.html [report] runId 20260904T184352Z-d9d796 · started 2026-09-04T18:43:52.970Z [report] wrote ~/engagements/acme-2026-09/Acme_192.168.1.1_executive.html ✓ exit 0 # Without --out it writes report_<runId>.html beside the run record — inside # your evidence tree, named after an id no client recognises. Name it for them.
nsauditor-ai report --from ~/engagements/acme-2026-09/ \ --run 20260904T184352Z-d9d796 \ --format jira \ --out ~/engagements/acme-2026-09/Acme_192.168.1.1_findings.csv # Summary,Description,Priority,Labels,External ID — one row per finding. # Passing controls are filtered out; nobody wants a ticket for a PASS. # External ID is stable, so a re-test maps back to the same rows. # ⚠ The column-to-field mapping is done inside Jira's own CSV importer. # That is not a live-instance integration we have verified.
# (a) A container census runs on load. If finding-like records sit somewhere # the report does not read, you are told — by container, with a count: [report] WARNING: N finding-like object(s) on host <h> live in `<container>`, which this report does not read. They are NOT in the output. # (b) Coverage caveats print too, and each is independent of the other: [report] coverage: partial — N of M not scanned [report] coverage: incomplete — the run did not record completion # (c) Then open it. A self-contained file with no external reference looks # the same offline as online — which is the first thing the client's own # security team will check.
Send the HTML on its own. It is one file — no asset folder,
no stylesheet to lose, and it prints from the browser when the client asked for a PDF. What you
keep is the evidence directory and its .sha256
sidecars: that is what lets you show, months later, that the report you sent came from that scan.
Re-rendering the same --run overwrites
the same filename rather than accumulating copies, so there is never a question of which one is current.
Version-based CVE matching is just the start. Every finding is scored — CVSS weighted by verification status, with an uplift for findings carrying an initial-access technique — and the queue is then ordered exploit-first. Findings are emitted unverified — you adjudicate what stays, not the scanner.
Severity ranks how bad a vulnerability would be if it were exploited. It cannot tell you whether anyone is exploiting it. Pro closes that gap at scan time: every finding carrying a CVE is joined by CVE ID against a local CISA KEV catalog and a local FIRST EPSS scores file, then banded and reordered exploit-first.
The justification travels with the finding — the KEV flag, the EPSS score with its percentile, the CVE ids that matched, and the store’s own as-of date. Your ranking is always something you can date and defend. Both catalogs are free, public and operator-supplied: point Pro at files you control and the join runs entirely on your machine.
Your existing riskScore does not move — exploitPriority is a new axis
beside it. And this is enrichment, not detection: it reorders what the scanner already found.
See Pro end to end — recorded on the 0.42 line:
The Pro overview covers exploit-first triage with CISA KEV and FIRST EPSS — free public feeds you download and point Pro at, failing closed when stale — offline-first CVE matching from detected service versions, findings emitted unverified for you to adjudicate, AI analysis on your own API keys, and risk-ranked reports built for one-click print to PDF.
# 1 · Download the feeds (public, no account needed for either) mkdir -p ~/.nsauditor/feeds curl -fsSL https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json -o ~/.nsauditor/feeds/kev.json curl -fsSL https://epss.empiricalsecurity.com/epss_scores-current.csv.gz | gunzip > ~/.nsauditor/feeds/epss.csv # 2 · Point the scanner at them (add both lines to your shell profile to persist) export NSAUDITOR_EXPLOIT_KEV_STORE=~/.nsauditor/feeds/kev.json export NSAUDITOR_EXPLOIT_EPSS_STORE=~/.nsauditor/feeds/epss.csv # 3 · Scan as usual — findings that carry CVEs arrive banded and ordered exploit-first nsauditor-ai scan --host 192.168.1.0/24 --plugins all
The stores are the feeds' own formats — no conversion step. Refresh KEV within 14 days and EPSS within 10 (a cron works);
a stale or unparseable store fails closed and the scan says so — it never silently reports
“not exploited”. Setting these outside a shell? ~ expands only in a shell —
in a --env file or Claude Desktop’s MCP env config, write the absolute path (e.g.
/Users/you/.nsauditor/feeds/kev.json); a path the scanner cannot read is reported as no-store, never as
“not exploited”. Full walk-through: getting-started → exploit intelligence.
Exploit intelligence is a Pro-tier capability and is off until you configure it. Enterprise includes Pro.
Pro sharpens the queue. Enterprise turns the result into something an assessor reads — 29 Enterprise plugins, 28 of them cloud auditors across AWS, Azure and GCP, and one read-only scan mapped to eight frameworks with an explicit out-of-scope column.
It is also where suppression lives — and suppression is an Enterprise capability, not a Pro one. Enterprise adds a suppression workflow for the compliance report: accepted-risk and false-positive records keyed on source and title pattern, approved with an approver’s signature, renewable on a cadence, and read by the compliance engine — which Pro does not run, so a suppression has no effect on a Pro report. The Enterprise compliance engine gives each decision an owner, a rationale, a date and an expiry inside the evidence pack — and as of EE 0.36.0 the report checks that approval’s signature against the key material the approver’s registry entry declares, rather than repeating what the record says about itself. A missing verdict reads not checked and never failed.
Isolated networks are Enterprise too: the offline install walkthrough — carry the package and the CVE data in, verify checksums, then import the feed — now lives on the Enterprise page.
None of that is part of a Pro licence. If your buyer is an auditor rather than a remediation queue, that is the tier to look at.
CISA KEV + FIRST EPSS joined by CVE ID at scan time, banded KNOWN_EXPLOITED / ELEVATED / BASELINE, queue ordered exploit-first. Operator-supplied catalogs; the join runs locally. See how →
Auto-generated CPEs from detected services matched against the NVD feed — offline-first: your local NVD store answers first, and on a miss the public NVD API is queried unless NSAUDITOR_OFFLINE_ONLY=1, which turns the miss into a coverage-gap finding instead of a request. Know which CVEs affect each host before writing a single line of your report.
Every finding is annotated with the ATT&CK techniques it relates to, with kill-chain context. Compliance teams love it. Clients understand it. Takes zero extra effort on your part.
Four analysis agents run concurrently on Pro — Auth, Crypto, Config and Service. The Exposure agent is Enterprise. Each produces structured findings for its category, then feeds the risk engine.
CVSS weighted down while a finding is unverified — with a 15% uplift for findings carrying an initial-access ATT&CK technique. Reports render severity-grouped, highest-impact first.
report --from <dir> --format executive renders a finished scan into one self-contained, print-ready HTML file that opens with no external network reference — optional cover-page branding via --brand. --format jira writes a Jira-importer CSV. Every scan still writes HTML, JSON and Markdown as before. See how →
Same OpenAI / Claude / Ollama providers you use in CE — but Pro injects CVE matches, MITRE techniques, and risk scores into every prompt. Vastly better output.
Add --since to the executive report and it compares two runs: new, resolved and changed-severity findings, plus a not-comparable list with the reason on every row, in the HTML report you send. Scan history is kept past the Community 7-day window. See how →
Configurable profiles (strict / moderate / minimal), additional patterns for AWS keys, DB connection strings, and JWTs. Full redaction audit log for compliance.
The MCP server registers seven tools and lists all seven to every client; the licence gate is applied when a tool is called. Pro unlocks probe_service and get_vulnerabilities on top of the three every tier can run.
--since)
list_plugins completes the every-tier set;
scan_cloud and
get_findings are Enterprise. Seven tools in total — all seven are listed to every client, and the licence is checked when one is called.
Start the MCP server with nsauditor-ai-mcp — works with Claude Desktop, Cursor, and any MCP-compatible AI assistant.
# @nsasoft/nsauditor-ai-ee is a private (restricted) package. # Use the npm read-token delivered with your license email. npm config set //registry.npmjs.org/:_authToken npm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Or, project-scoped, in an .npmrc file echo "//registry.npmjs.org/:_authToken=npm_xxxx..." >> ~/.npmrc
# One line — installs both the CE platform and the Pro package npm install -g nsauditor-ai @nsasoft/nsauditor-ai-ee
# The CE license installer verifies the JWT signature before persisting # and stores the key in macOS Keychain (or ~/.nsauditor/.env mode 0600 elsewhere). nsauditor-ai license install pro_eyJhbGciOiJFUzI1NiIs... ✓ Pro license installed Stored at: macOS Keychain (service=nsauditor-ai) Org: you@example.com Expires: 2027-04-29 # Verify nsauditor-ai license --status ✓ Pro license active
# private ranges (RFC 1918, loopback) need NSA_ALLOW_ALL_HOSTS=1 — # the SSRF guard refuses them by default nsauditor-ai scan --host 192.168.1.0/24 --plugins all # View your AI report in browser — one evidence folder per host open out/192.168.1.1_*/scan_response_ai.html # macOS xdg-open out/192.168.1.1_*/scan_response_ai.html # Linux # Each scan writes HTML, JSON, and Markdown reports — print the HTML to PDF for clients nsauditor-ai scan --host 10.0.0.0/24 --plugins all open out/10.0.0.1_*/scan_response_ai.html # print → PDF # And turn that finished run into the one file you send the client — # --from is the output root; report picks the newest run in it nsauditor-ai report --from out/ --format executive
Pro turns a finished scan into one self-contained HTML file you can send, or a Jira-importer CSV. Add
--since and that file shows what is new, resolved and changed in severity
since the last scan; a finding the report can see was not measured the same way twice is set aside with its reason
on the row, not counted as fixed, and the comparison’s two measured limits are stated (see how it works). You also unlock CVE matching, MITRE
ATT&CK annotations, risk scoring and parallel analysis agents. Cancel anytime; an annual plan stays active to
the end of its paid year, then reverts to Community Edition.