
fleet-cve-scanner v1.1.0
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?
fleet-cve-scanner
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 putsqlite3.exeonPATHor beside the script. Without it the scan still runs;first_seenfalls 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_seentracking. 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 ProgramcvelistV5) — 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.