
An open-source, single-script CVE scanner for RMM-managed fleets. Pure PowerShell 7 — joins your RMM software inventory against NVD, CISA KEV, EPSS and SSVC to answer: is this version vulnerable, and how urgent is it?
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:
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.
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.
You need PowerShell 7 (pwsh). Two ways to run:
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
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.
brew install powershell; Windows:
winget install Microsoft.PowerShell; Debian/Ubuntu: install from the
Microsoft package repo (packages.microsoft.com).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.first_seen tracking. Absent = those features skip, scan still completes.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.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. |
| 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). |
| 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.