threat-finder finds the vulnerable software actually running on a host — not
what a manifest claims — and tells you which findings are network-reachable.
It resolves each running service (and, with --scope all, every installed OS
package) to an exact Package-URL and
matches it against the OffSeq Radar catalog using
ecosystem-native version rules, so backported/fixed builds aren't false-flagged.
Vulnerability summary (highest risk first):
openssh-server@1:8.9p1-3ubuntu0.6 — 1 finding(s) [PUBLIC tcp 0.0.0.0:22]
[ACT-NOW 92] HIGH CVE-2024-6387 [KEV] regreSSHion: remote code execution in OpenSSH
→ fix: 1:8.9p1-3ubuntu0.10 https://radar.offseq.com/threat/…
nginx@1.18.0-6ubuntu14.4 — 1 finding(s)
[SCHEDULE 41] MED CVE-2023-44487 HTTP/2 Rapid Reset
2 confirmed finding(s) across 2 asset(s); 1 exposed, 1 known-exploited.
brew install offseq/tap/threat-finder # Homebrew (macOS/Linux), prebuilt
cargo binstall threat-finder # prebuilt binary, no toolchain
cargo install --locked threat-finder # from sourcePrebuilt archives for Linux/macOS (x86_64 + arm64) and Windows (x86_64) are on the releases page. Requires Rust ≥ 1.89 to build from source. Linux, macOS, the BSDs, and Windows are all supported.
export OFFSEQ_API_KEY=... # from https://radar.offseq.com/console
threat-finderScans the running services, prints a risk-ranked summary, and writes the full
JSON report to ./threats.json (owner-only). Add --scope all to also scan every
installed OS package.
threat-finder [OPTIONS]| Flag | Description |
|---|---|
-o, --output <PATH> |
Write the JSON report to PATH (default: prompt, or ./threats.json) |
--json |
Print the JSON report to stdout instead of a file |
--scope <SCOPE> |
running (default) or all (+ every installed OS package) |
--severity <LEVEL> |
Only report findings at/above critical|high|medium|low |
--strict |
Drop coordinate-unconfirmed findings (report only confirmed) |
--fail-on <WHAT> |
Exit 5 if matching findings exist: any|critical|high|medium|low|kev|exploited|act-now|exposed |
--sarif <PATH> |
Also write a SARIF 2.1.0 report (for code-scanning UIs) |
--include <GLOB> / --exclude <GLOB> |
Filter assets by name glob (repeatable) |
-q, --quiet |
Suppress the banner, progress, and summary |
--no-color |
Disable ANSI colors |
-y, --yes |
Assume defaults, never prompt (CI/cron) |
--reset |
Re-enter the API key, ignoring the saved one |
--register |
Register the scanned host with Radar for continuous monitoring (no prompt) |
--no-register |
Don't register or prompt for monitoring this run |
--host-name <NAME> |
Friendly hostname to send with a registration |
--unregister |
Remove this host's inventory from Radar and exit |
--sbom <FILE> |
Scan an SBOM or lockfile instead of the host (one request; never registers) |
--respect-decisions |
Keep findings silenced by a recorded Radar decision out of --fail-on (needs registration; one extra request) |
--vex <PATH> |
Write this host's decisions as a VEX document (needs registration and a paid plan; one extra request) |
--vex-format <FORMAT> |
openvex (default) or cyclonedx |
-v, --verbose |
Debug-level runtime log file (RUST_LOG overrides) |
--windows-missing-updates |
(Windows) Also list pending security updates from the Windows Update Agent (online scan; run elevated) |
-h, --help / -V, --version |
Help / version |
# CI: only high+ findings, JSON to stdout, no prompts
OFFSEQ_API_KEY=… threat-finder --yes --json --severity high > report.json
# Fail the build only on findings that need action now (exposure-aware)
OFFSEQ_API_KEY=… threat-finder --yes --quiet --fail-on act-nowExit codes: 0 ok · 1 lookup/IO error · 2 no API key · 3 unsupported
OS · 4 rate limit/quota, or API access required (upgrade needed) · 5
--fail-on threshold met. With --fail-on, a run where any lookup failed exits
1 rather than passing; packages Radar could not evaluate (listed under
rejected) don't count as failed lookups.
API key (get one from the Radar Console), resolved in order:
OFFSEQ_API_KEYenvironment variable (best for CI/cron).- The saved key in
$XDG_CONFIG_HOME/offseq-rust/config.toml(0600), unless--resetis given. - An interactive, hidden prompt on a TTY — then saved for next time.
Non-interactive (--yes / no TTY) with no key available exits 2.
Set OFFSEQ_CONFIG_DIR to override where the config lives (default: the OS
per-user config dir) — handy for containers/CI that need a deterministic path.
Exact-coordinate matching. Each asset becomes a purl carrying its full
version (epoch + distro revision) and a ?distro= qualifier, e.g.
pkg:deb/ubuntu/openssh-server@1:8.9p1-3ubuntu0.6?distro=jammy. The inventory is
matched in batched POST /match/batch calls (one request per tier-sized chunk)
server-side, with ecosystem-native version rules (dpkg/rpm/apk/semver) — so a
backported-and-fixed build like 1.18.0-6+deb11u3 is correctly not flagged, and
there's no client-side version guessing. Findings are split by the API's
confirmed flag: confirmed matches are reported; coordinate matches whose
version can't be confirmed are surfaced separately as unconfirmed / triage
(excluded from the count, byCve, and --fail-on; drop them with --strict).
Network-exposure correlation. Manifest scanners (Trivy, Grype, osv-scanner)
read package lists; external scanners (Nessus, OpenVAS) need a second host. This
tool maps each running service's process to the sockets it is listening on
(/proc/net on Linux, lsof on the other Unixes, Get-NetTCPConnection /
netstat on Windows) and classifies reachability —
loopback / private / public. A vulnerable service on 0.0.0.0 is a very
different risk from one on 127.0.0.1: findings rank exposed-first and
--fail-on exposed gates CI on exactly that. No packets are sent. Findings also
carry CISA KEV (due date, ransomware use), EPSS, public exploit
maturity, and exploitation seen by ENISA EUVD or OffSeq Mirage
honeypots (--fail-on exploited).
Exposure-aware prioritization. Every finding gets a riskScore (0–100) and an
SSVC-style decision band — act-now · soon · schedule · track — fused
from severity, EPSS, KEV (plus an overdue bonus), exploit maturity, EUVD/Mirage
exploitation and the owning asset's network exposure. The summary leads each
line with a [ACT-NOW 92]-style badge and sorts by it, so the handful of
public-facing, exploited issues float to the top of a noisy host. The formula is
identical to Radar's server-side score, and appears in --json and SARIF
(properties). Each asset also gets Radar's upgradeTo: the smallest version
that fixes every finding on it.
A one-off scan is a point in time. Register a host once and Radar keeps watching: when a newly-published CVE affects one of its coordinates, you get an alert (email + Console), prioritized by exposure — no re-scan needed.
After an interactive scan, the tool asks:
Add these 42 services to Radar for continuous monitoring & alerts? [Y/n/never]
Y registers this host; n skips this run; never remembers your choice (it's
saved to the config and you won't be asked again). On a re-scan it also reports
drift (+added / -removed / ~changed) and any findings new since your last
scan and how many were resolved since. Manage your hosts — toggle monitoring, view findings, deregister — under
Inventory in the Radar Console.
For automation, skip the prompt: --register registers non-interactively (off by
default in CI), --no-register opts out, --host-name <NAME> labels the host, and
--unregister removes it. A stable per-host id and your prompt preference live in
$XDG_CONFIG_HOME/offseq-rust/config.toml. Registration never changes the exit
code — a monitoring hiccup won't fail your scan. Every plan can register hosts
(Free: 2); alerts need a paid plan.
--sbom <FILE> checks every package in a file instead of the host, in a single
POST /scan request. Radar detects the format: CycloneDX (JSON/XML), SPDX
(JSON/tag-value), purl-per-line lists, package-lock.json, npm-shrinkwrap.json,
yarn.lock, pnpm-lock.yaml, poetry.lock, uv.lock, Cargo.lock, go.mod,
go.sum, composer.lock, Gemfile.lock and pinned requirements*.txt.
threat-finder --sbom package-lock.json --yes --fail-on high --sarif deps.sarifThe report has the usual shape, keyed by purl; assets.*.upgradeTo comes from
Radar's remediation and packages it couldn't check (no version, unsupported
ecosystem) are listed under rejected. A file has no network exposure, so
riskScore/decision are Radar's exposure-free score. --severity, --strict,
--fail-on, --json and --sarif work as for a host scan; registration and
the monitoring prompt are skipped. The file must stay under 10 MB once
JSON-encoded, and non-JSON files can't have lines over 4,096 characters (checked
locally before sending). Packages per scan are capped by plan (the same as the
batch size); a larger file exits 4 with the cap named.
Decisions recorded in the Radar Console's decision ledger (accept, not affected,
mitigated, defer) can drive the CLI. Both options need this run to register the
host (--register or the prompt) and cost one extra request each; without a
registration they warn and are skipped.
--respect-decisionsfetches the host's findings and attaches the most specific decision to each matching finding asrecordedDecision(verdict,status,until,staleReasons,suppressing). A decision suppresses a finding, as it suppresses Radar's alerts, when it's active or review-due, not pastuntil, and notpatch_now; those findings are tagged[DECIDED …], marked as suppressed in SARIF, and kept out of--fail-on. Stale or expired decisions don't suppress. Only paid plans can record decisions.--vex <PATH>writes the host's decisions as a VEX document (--vex-format openvex|cyclonedx), exactly as Radar renders it. This export needs a paid plan; on Free it warns and the exit code is unchanged.
threat-finder --yes --register --respect-decisions --fail-on act-now --vex host.openvex.json--scope running (default) scans live services — the small, high-signal set
whose exposure can be correlated. --scope all additionally enumerates every
installed OS package (dpkg/rpm/pacman/apk/brew/pkg/pkg_info),
expanding the matched surface 10–50×. A package that also backs a running,
exposed process keeps that exposure (assets are deduplicated and merged by
coordinate). The kernel is covered as its package (linux-image…) under
--scope all.
--scope allcan yield hundreds–thousands of packages. Lookups are batched per plan (Free 50 per request at 20 requests/hour and 30/day; Base 200; Pro 1,000; Enterprise 4,000), and the tool warns when an inventory needs many free requests. Free plans also see at most 10 matches per package (riskiest first); capped assets are listed undertruncated.
| OS | Discovery | Coordinate source |
|---|---|---|
| Linux (systemd) | systemctl list-units / show → /proc/<pid>/exe |
dpkg / rpm / pacman / apk |
| Linux (SysV/OpenRC) | service --status-all / rc-status |
package DB |
| macOS | launchctl list → ps (third-party only) |
Homebrew |
| FreeBSD / DragonFly | service -e |
pkg |
| OpenBSD | rcctl ls started |
pkg_info |
| NetBSD | /etc/rc.d status |
pkg_info |
| Solaris / illumos | svcs → svcprop |
probe (--version) |
| Windows | Win32_Service → Get-NetTCPConnection / netstat |
registry / winget / Appx / Chocolatey / Scoop → CPE; npm / pip / dotnet → purl; the OS build → CPE |
Where no package owns a binary, the version falls back to a hardened --version
probe (absolute path only, sanitized env). On macOS, Apple system services
(com.apple.*, SIP-protected paths) are skipped — they're covered by the OS
version, and probing hundreds of them is pointless.
On Windows, everything is gathered via the built-in powershell.exe
(no extra runtime, no admin for the core inventory). Installed apps map to NVD
CPEs through a curated name table (unmapped apps fall back to a name search
rather than a wrong guess); the OS edition/build becomes a feature-qualified OS
CPE (e.g. windows_11_23h2); and --windows-missing-updates adds an opt-in
Windows Update Agent advisory of pending security patches.
JSON, with deterministic (sorted) keys and no timestamp, so reports diff cleanly:
services—pkg@version→ confirmed findings (cveId,severity,cvssScore,cvssVector,epss,kev,kevDueDate,kevOverdue,ransomware,exploited,exploitedOutsideKev,exploitMaturity,riskScore,decision,confirmed,matchedRange,matchBasis,fixedVersions,patchLinks,remediation,cwes,references,radarUrl, and with--respect-decisionsrecordedDecision), highest-risk first.unconfirmed— coordinate matches whose version couldn't be confirmed (triage).assets—pkg@version→{ exe, versionSource, exposed, reachability, listeners, upgradeTo }(versionSource=package-db|probe;reachabilitycovers TCP and UDP).byCve— each CVE rolled up across every affected asset ("patch once, fix many").errors— per-asset lookup failures, so a failure never reads as "clean".rejected— packages Radar could not evaluate, and why (not a lookup failure).truncated— assets whose matches the plan's per-query result cap cut short.registration— present when the run registered the host:hostId,monitoring,assetCount,drift,summary,newSinceLastCount,resolvedCount.meta—{ tool, version, schemaVersion }(schemaVersion3).
A SARIF 2.1.0 report (--sarif) is also available for code-scanning UIs.
| OffSeq | EU security audits, threat monitoring, CISO-as-a-Service, NIS2 compliance |
| Radar | Real-time threat intelligence — the catalog threat-finder matches against |
| Radar Console | Subscriptions, custom feeds, and your OFFSEQ_API_KEY |
| Radar API | REST docs for the /match endpoint used here |
| Radar Threats | Searchable CVE / malware / threat-actor database |
| Radar Feeds | Custom feeds aggregating CISA, CIRCL, ThreatFox, … |
| Radar Pricing | Free tier through Enterprise |
| Breach | Dark-web data-leak & exposed-credential monitoring |
| Veil | Client-side PNG steganography (AES-256-GCM) |
| Guard | AI website security & compliance analyst |
| Training | PECB-accredited security & privacy courses |
cargo build --release
cargo test # unit tests
cargo test -- --ignored # + macOS live-discovery smoke test
cargo clippy --all-targetsThe engine is a library crate (find_threats) with a Collector abstraction
(running-services and os-packages; SBOMs and lockfiles go through Radar's
/scan), so the binary is a thin CLI over it.
Dual-licensed under MIT or Apache-2.0, at your option.