An open-source, single-script CVE scanner for RMM-managed fleets. Pure PowerShell 7, no agents, no appliances, no license fees.

It answers one question for every piece of software on every endpoint you manage: is the installed version known to be vulnerable, and how urgent is it? — by joining your RMM's software inventory against free, authoritative security feeds:

  • NIST NVD — known CVEs + affected version ranges
  • CISA KEV — actively exploited in the wild (bypasses the score threshold)
  • EPSS — probability of exploitation in the next 30 days
  • CISA SSVC — act / attend / track prioritization decisions
  • MSRC — proof a Microsoft patch already covers the finding
  • endoflife.date — software that will never be patched again

Outputs: a per-device CSV report, a SQLite history with SLA clocks and week-over-week change events, a self-contained HTML dashboard, and an optional markdown exposure report.

Status

v1.0.0. The engine was ported clean-room from a scanner that ran weekly in production against a Windows fleet (see docs/HISTORY.md). Before this release the two were run against the same live fleet and compared row-for-row: across tens of thousands of findings where both saw an identical (device, software, version), every computed field — status, CVSS, severity, KEV flag, SSVC decision, fix version — matched exactly.

That comparison covers the verdict logic. It did not exercise the history/SLA layer, and it is not a substitute for validating the tool in your own environment. Read docs/known-limitations.md before trusting it — it is written to be honest rather than flattering.

Quick start

You need PowerShell 7 (pwsh). Two ways to run:

A. Live NinjaOne scan

cp config.example.json config.json
# edit config.json: fill ninjaone.client_id / client_secret / base_url,
# set output.report_dir, and (recommended) nvd.api_key
pwsh -File fleet-cve-scan.ps1

B. Offline — any RMM, SCCM, or Intune (no API access)

Export your inventory to a CSV with columns hostname, software, version (optional device_id, os), then:

cp config.example.json config.json   # only output.report_dir is needed here
pwsh -File fleet-cve-scan.ps1 -InputCsv inventory.csv

-InputCsv makes zero NinjaOne calls — no OAuth, no API. It still queries NVD and the other public feeds, so you still want an nvd.api_key in config.json and a valid output.report_dir. See docs/rmm-adapters.md for the input contract and per-RMM export recipes.

Installing prerequisites

  • PowerShell 7 — macOS: brew install powershell; Windows: winget install Microsoft.PowerShell; Debian/Ubuntu: install from the Microsoft package repo (packages.microsoft.com).
  • sqlite3 (optional, powers history/trending) — macOS: preinstalled or brew install sqlite; Debian/Ubuntu: apt-get install sqlite3; Windows: download the SQLite "tools" bundle from sqlite.org and put sqlite3.exe on PATH or beside the script. Without it the scan still runs; first_seen falls back to the run date.

Requirements

  • PowerShell 7 (7.4+ recommended). The parallel scan requires 7.0+.
  • sqlite3 — optional; enables the history DB, trending, and SLA first_seen tracking. Absent = those features skip, scan still completes.
  • Network egress to NVD, CISA (KEV), epss.empiricalsecurity.com (the EPSS bulk-score host — FIRST moved EPSS data hosting there), MSRC, endoflife.date, and GitHub raw (CVE Program cvelistV5) — plus the NinjaOne API in live mode.
  • Credentials — live mode needs NinjaOne OAuth2 client id/secret; offline mode needs only an inventory CSV. An NVD API key is technically optional but effectively required at fleet scale: NVD allows 50 requests/30s with a key and only 5/30s without one. The scanner detects which you have and throttles to match, so a keyless run is correct but roughly 10x slower — fine for a handful of products, impractical for a real fleet. Keys are free.

Configuration reference

Config is a JSON file (default config.json beside the script; override with -ConfigPath). Start from config.example.json. Every key the scanner reads, its default when omitted, and what it does:

ninjaone (live mode)

Key Default Purpose
ninjaone.client_id "" NinjaOne API OAuth2 client id. Required for live scans; leave blank for -InputCsv.
ninjaone.client_secret "" NinjaOne API OAuth2 client secret.
ninjaone.base_url — (example ships https://app.ninjarmm.com) NinjaOne API base URL; required for live scans, no built-in fallback. Use your region's host (e.g. eu.ninjarmm.com, oc.ninjarmm.com).

nvd

Key Default Purpose
nvd.api_key "" NVD API key, sent as the apiKey header. Blank runs unauthenticated, which drops the defaults for nvd_rate_limit and nvd_min_spacing_ms to NVD's anonymous ceiling (4/30s, 6500ms) instead of the authenticated one (48/30s, 700ms). Free, and ~10x faster.

Scan & scoring

Key Default Purpose
cvss_threshold 7.0 Minimum CVSS base score for a scored CVE to be reported VULNERABLE. KEV-listed CVEs bypass this floor.
ssvc_mission_prevalence high SSVC Mission Prevalence stakeholder input (low / medium / high).
ssvc_public_wellbeing high SSVC Public Well-being stakeholder input (low / medium / high).

Caching & rate limiting

Key Default Purpose
nvd_cache_ttl_days 7 Days an NVD result (per software name) stays cached before re-query.
nvd_cache_flush_every 500 Checkpoint the NVD cache to disk every N completed items, so a killed scan keeps its fetches. 0 disables (end-of-scan save still runs).
nvd_min_spacing_ms 700 with an API key, 6500 without Minimum milliseconds between NVD calls. The binding rate constraint — tune this first.
nvd_rate_limit 48 with an API key, 4 without Sliding-window cap on NVD calls per 30s (a backstop). Floor of 1.
parallel_throttle 20 ForEach-Object -Parallel runspace count. NVD calls are serialized regardless; this only bounds cache-hit concurrency. Floor of 1.
kev_cache_ttl_hours 24 TTL for the cached CISA KEV catalog.
epss_cache_ttl_hours 24 TTL for the cached FIRST.org EPSS score file.
eol_cache_ttl_days 7 TTL for cached endoflife.date lookups.
msrc_cache_ttl_days 30 TTL for cached MSRC CVRF data.
cvelist_cache_ttl_days 7 TTL for cached CVE Program (cvelistV5) / CISA-ADP SSVC data.
cvelist_fetch_budget 300 Max CVE-record fetches per run for SSVC/vulnrichment enrichment; the rest defer to the next run.

nvd_min_spacing_ms, nvd_rate_limit, and parallel_throttle are read by the scanner but not present in config.example.json — add them only if you need to tune. See docs/rate-limiting.md.

Enrichment & SLA

Key Default Purpose
epss_spike_threshold 0.10 Minimum rise in a finding's EPSS score between scans to emit an EPSS_SPIKE change event.
sla_days_critical 14 SLA window (days from first_seen) for CRITICAL findings.
sla_days_high 30 SLA window for other scored severities.
denied_software_csv "" Path to an optional denied-software CSV cross-check. Empty disables the check.

output

Key Default Purpose
output.report_dir (required) Directory for all outputs — CSV, history DB, dashboard, and caches.
output.wiki_security_page "" Optional path to a markdown exposure page. Written only when set.
output.dashboard_share "" Optional path/share that receives a copy of the dashboard each run. Empty = local-only.

Outputs

Everything below lands in output.report_dir.

  • vuln-report-YYYY-MM-DD.csv — the per-finding report. 28 columns (schema below).
  • vuln-history.db — SQLite history (requires sqlite3): runs (per-run aggregates), findings (per (device, software, cve) open-finding history with first_seen / last_seen / resolved_date / epss_score), and changes (typed week-over-week events).
  • cve-dashboard.html — self-contained HTML dashboard (charting library inlined, no external requests). Optionally copied to output.dashboard_share.
  • eol-report-YYYY-MM-DD.csv — written when end-of-life families are detected.
  • denied-report-YYYY-MM-DD.csv — written when denied_software_csv is configured and matches.
  • Cache filesnvd-cache.json, kev-cache.json, epss-cache.json, eol-cache.json, msrc-cache.json, cvelist-cache.json (all gitignored).
  • Markdown exposure page — at output.wiki_security_page, only when set.

CSV schema (28 columns)

The column set is fixed and gate-enforced. In order:

# Column Meaning
1 device_id RMM device id (or a deterministic synthetic id in -InputCsv mode).
2 hostname Device hostname.
3 os Operating system string (may be blank).
4 software Product name as inventoried.
5 installed_version Version string as inventoried.
6 cve_id CVE identifier.
7 cvss_score CVSS base score (0 if none).
8 severity CVSS severity label.
9 cvss_version Which CVSS standard produced the score (4.0/3.1/3.0/2.0/blank).
10 epss_score FIRST.org EPSS probability (blank if the CVE is absent from the catalog).
11 epss_percentile EPSS percentile (blank if absent).
12 ssvc_exploitation Raw CISA SSVC exploitation option (none/poc/active; blank without ADP data).
13 ssvc_decision SSVC decision Act/Attend/Track (blank without ADP data).
14 status VULNERABLE / PATCHED / UNKNOWN.
15 published_date CVE published date.
16 unknown_reason Why a row is UNKNOWN (SUSPECT_VERSION / NO_VERSION_BOUND / VERSION_PARSE_ERROR; blank otherwise).
17 is_kev True when the CVE is on the CISA KEV catalog.
18 kev_due_date CISA KEV remediation due date.
19 kev_ransomware_use KEV ransomware-campaign use flag.
20 fix_version Computed fix-boundary version.
21 fix_boundary_inclusive Whether the fix boundary is inclusive.
22 advisory_url Vendor advisory or NVD detail URL.
23 patch_evidence Evidence when a Microsoft UNKNOWN was flipped to PATCHED.
24 triage_status Triage disposition from triage.json (blank if none).
25 triage_note Triage note.
26 first_seen Date the finding was first observed (from the history DB).
27 sla_due SLA due date.
28 sla_breach Whether the SLA is breached.

Scheduling

Run-WeeklyScan.ps1 is a wrapper for scheduled runs: it runs the scanner from its own directory, captures all output streams to a dated log under <script dir>/logs/ (override with -LogDir), and propagates the scanner's exit code so the scheduler sees failures.

Windows Task Scheduler:

schtasks /Create /TN "CVE-Scan-Weekly" ^
  /TR "pwsh -NoProfile -File C:\path\to\Run-WeeklyScan.ps1" ^
  /SC WEEKLY /D SUN /ST 22:00 /RU SYSTEM

cron (Linux/macOS):

0 22 * * 0  pwsh -File /path/to/Run-WeeklyScan.ps1

Dashboard hosting

cve-dashboard.html is fully self-contained — open it directly, drop it on any static host or file share, or copy it automatically each run via output.dashboard_share. For access-controlled hosting, the dashboard-auth/ directory ships a minimal ASP.NET host with Microsoft Entra (Azure AD) authentication; see its README and RUNBOOK.

Honest limitations (read before trusting it)

  • CPE matching is fuzzy by nature. 40–60% of typical inventory has no NVD entry at all and is skipped (logged, never silently). Name-based CPE selection can pick the wrong product; a suppression workflow exists because it has to. This is the ceiling of the free-NVD approach — a paid scanner with a curated detection catalog will be more accurate. This tool's value is being free, transparent, and auditable.
  • UNKNOWN is a first-class verdict. When the scanner can't prove vulnerable-or-patched, it says so instead of guessing.
  • Verified against NinjaOne only. The scan engine is RMM-agnostic (input is just device + software + version); the -InputCsv mode lets any RMM/SCCM/Intune export feed it, but only the NinjaOne adapter has been run in production.

The full, unflinching list — the CPE skip rate, dedup-key masking, page-1 totalResults, MSRC supersedence gaps, and more — is in docs/known-limitations.md.

Design notes

A few structural choices that look unconventional are deliberate:

  • Single file, not a module. The scan hot path runs inside ForEach-Object -Parallel, and those runspaces cannot see outer-scope functions — which is why a handful of helpers are intentionally duplicated inside the parallel block. A one-function-per-file module layout would not remove that duplication (the parallel body would still need the source injected per runspace); it would just add a build step and break the copy-one-file deployment model. If the project ever outgrows single-file, packaging is the v2-shaped change, not a patch.
  • [PSCustomObject] records, not classes. Finding records are produced inside parallel runspaces and marshaled back to the main runspace. PowerShell class instances do not reliably survive that boundary; [PSCustomObject] does, and the CSV column contract is enforced by the release gate instead of a type system.
  • Plain PS7 test scripts, not Pester. The suites in tests/ are dependency-free scripts with exit-code assertions, runnable anywhere pwsh exists — including the scan server — with nothing to install. CI runs them all on Windows and Ubuntu.

Documentation

  • docs/how-it-works.md — the scan pipeline stage by stage, verdict logic, cache discipline, and the fail-soft contract.
  • docs/known-limitations.md — every known way the scanner can be wrong, and why.
  • docs/rate-limiting.md — the NVD rate-limit lessons as an operator guide and tuning table.
  • docs/rmm-adapters.md — the input contract, the two built-in inventory paths, and how to add a native adapter.
  • docs/HISTORY.md — condensed, sanitized version history and the engineering lessons behind each release.

Data sources & attribution

This tool is only as useful as the free, authoritative feeds it joins against. Each of the following is queried at scan time; please honor their terms if you build on this.

  • NIST National Vulnerability Database (NVD) — CVE records and affected version ranges, via the NVD API. This product uses the NVD API but is not endorsed or certified by the NVD. https://nvd.nist.gov/
  • CISA Known Exploited Vulnerabilities (KEV) Catalog — the authoritative list of CVEs known to be exploited in the wild. https://www.cisa.gov/known-exploited-vulnerabilities-catalog
  • CVE Program — cvelistV5 — CVE records plus the SSVC / vulnrichment data contributed by the CISA ADP. CVE® is a registered trademark of The MITRE Corporation. https://github.com/CVEProject/cvelistV5
  • SSVC (Stakeholder-Specific Vulnerability Categorization) — the act / attend / track decision logic in this tool is derived from CERT/CC SSVC (Carnegie Mellon University Software Engineering Institute) and the CISA SSVC Coordinator decision tree, version 2.0.3. Credit to CERT/CC and CISA is required. https://www.cisa.gov/ssvc · https://github.com/CERTCC/SSVC
  • FIRST.org EPSS (Exploit Prediction Scoring System) — probability of exploitation in the next 30 days. EPSS data is provided by FIRST. See Jay Jacobs, Sasha Romanosky, Benjamin Edwards, Michael Roytman, Idris Adjerid (2021), Exploit Prediction Scoring System, Digital Threats: Research and Practice, 2(3). https://www.first.org/epss/
  • Microsoft Security Response Center (MSRC) — the CVRF API, used to confirm when a Microsoft security update already covers a finding. © Microsoft Corporation. https://api.msrc.microsoft.com/ · https://msrc.microsoft.com/
  • endoflife.date — end-of-life and end-of-support dates for software that will never be patched again. Data courtesy of the endoflife.date project. https://endoflife.date/