MIT · zero-build MavenGradlenpmpnpm ComposerPyPINuGet GoRubyNative bins

Audit every dependency.No build. No leaks.

A polyglot CVE · EOL · outdated · license scanner that reads your source tree directly — no mvn, no npm install, no Docker. Findings merged from CVEProject, OSV, NVD & retire.js, prioritised with EPSS + CISA KEV, an audit-ready HTML & Word report (plus CycloneDX SBOM & CSAF VEX), and an air-gapped mode for confidential code.

$npm i -g fad-checker && fad -s ./your-project

⚠️ fad-checker is new and may still contain ( rare ) bugs. Treat its output as a strong first pass, double-check anything critical, and please report issues; they get fixed fast.

fad-checker animated terminal demo: an offline Maven audit — dependencies absent from Maven Central flagged as private/internal, the 15-step vulnerability-database checklist, then findings coloured by severity
One tool, one pass

Ten ecosystems + vendored JS + embedded JARs + native binaries

Point it at any checkout — multi-module, monorepo, polyglot. It parses manifests and lockfiles directly, and falls back to best-effort (pinned versions) when there's no lockfile.

Maven · pom.xmlGradle · build.gradle(.kts) + libs.versions.tomlnpm · package-lockYarn · v1 + Berry pnpm · v5/6/9Composer · composer.lockPyPI · poetry/uv/pdm/pip NuGet · .csproj/.fsproj/.vbprojGo · go.mod/go.sum Ruby · Gemfile.lockVendored JS · retire.js Embedded JARs · full inventory (ch.1B) Native bins · .dll/.so/.exe/.dylib
Why it's different

Built for audits, not just CI

01

No build, polyglot

Reads pom.xml, build.gradle & lockfiles across all ten ecosystems. No JDK, no install, no Docker — audit a checkout you can't even compile. It resolves the Maven graph the way Maven resolves it, and cracks open committed .jar/.war/.ear binaries in-memory to scan what is shaded inside them.

02

Merged sources, fewer false positives

CVEProject + OSV.dev + NVD + retire.js, merged & deduped, then cross-checked against NVD CPE version ranges to filter noise.

03

Risk-based priority

Every CVE enriched with EPSS (FIRST.org exploit-prediction) and CISA KEV (known-exploited). A composite score puts exploited-in-the-wild first, not just the highest CVSS.

04

Beyond CVEs

Flags end-of-life (endoflife.date, with a Source column tracing each verdict to its mapping rule — plus an opt-in out-of-active-support level and a zero-false-positive PHP runtime verdict from the Composer constraint), deprecated / abandoned / yanked, outdated versions, and licenses (SPDX + copyleft policy) — signals most scanners skip.

05

Reports you can hand over

One self-contained HTML + Word-compatible .doc, organised by ecosystem and by the manifest that declares each dep, with per-tool fix recipes, in English or French (--lang fr — the whole report, down to the chapter counts, the status pills and the fix recipes, never the evidence). Every table has a split Copy button: the left half copies it whole, the chevron copies just the first 5 rows, the criticals, critical + high, or only what CISA lists as exploited — each with its row count. Pastes into Word with formatting intact, and nothing ever scrolls sideways.

06

Exports & CI gating

Writes an HTML report and a findings.json by default — the JSON so the next run can --baseline against it. Emit CycloneDX 1.6 SBOM, CSAF 2.0 VEX, flat JSON and SARIF 2.1.0 (GitHub/GitLab code scanning). Gate with --fail-on critical|kev or — for repeat audits — on new findings only with --baseline + --fail-on-new; triage false-positives via --ignore / --vex.

06b

A --help that fits a screen

The long tail of switches folds into four flags: -d eol,nvd turns things off, -a licenses,snyk turns on what is off by default, -r html,json picks the outputs, -o says where they land. The individual flags still work — they are hidden, not removed — and --help-all lists them. An unknown token is a hard error, never a silent no-op.

07

Air-gapped

Export an anonymized descriptor (public coordinates only), enrich online, report offline. The confidential codebase never leaves the enclave.

08

Extract & normalise the descriptors

-t <dir> writes a parallel tree of normalised dependency descriptors: each pom.xml reduced to the dependency-relevant nodes, reactor parents rewired to their real in-tree path, ${…} resolved in coordinates — plus every non-Maven lockfile/manifest mirrored (npm/yarn/pnpm, Composer, PyPI, NuGet, Go, Ruby). Online it also probes each coordinate and reports the ones absent from Maven Central — your private/internal modules — which -e <regex> then strips. Then it stops: a buildless, sanitised dependency inventory to archive, hand over, or feed to any scanner.

09

Native binaries, by checksum

Committed .dll/.exe/.so/.dylib (magic-byte confirmed — images/assets are never picked up) are identified by hash via deps.dev + CIRCL: flags tampered/unknown files and libraries that should be declared dependencies. No malware/AV lane.

10

Private registries & config files

Point it at private Nexus/Artifactory (Maven), Verdaccio/GitHub Packages (npm), devpi (PyPI), Gemfury (Ruby), GOPROXY (Go), Azure Artifacts/BaGet (NuGet) or Satis/Private Packagist (Composer) — tried first, public last, Basic or Bearer auth. Stash reusable defaults in .fad-env.json / --config or the FAD_CHECKER_ENV variable, and prune sub-paths with gitignore-style --exclude-path — every skipped directory is listed back in the report's Ignored directories appendix.

11

Unmanaged vendored JS, inventoried

Every standalone JS lib committed into the tree (jQuery, Bootstrap, PDF.js, …) that no package manager governs is inventoried — vulnerable or not (via retire.js --verbose). A cyber-hygiene constat on unknown-provenance third-party code, the JS twin of the native-binary scan.

12

Audit-grade & reproducible

If a data source goes dark mid-scan and the warm cache does not cover the gap, the run stops before writing anything and names the domain, the error codes, the failing URL and the flag that skips that source — exit 2, distinct from the 1 that --fail-on uses, so CI can tell vulnerable from not trustworthy. A source whose cache covered every lookup stays silent. Every report carries a provenance manifest (data-source freshness + run config) and a Methodology, data sources & limitations chapter that states what fad doesn't assess. Artifacts ship a SHA256SUMS integrity manifest (sha256sum -c). Differential audits diff a scan against a prior run (--baseline, or fad diff a.json b.json) so CI can fail on new findings only.

13

Certificates & keys

Finds committed X.509 certificates (flags expired, expiring, weak key RSA<2048, weak signature MD5/SHA1, self-signed), private & public keys — each labelled private (a leaked secret) or public — across PEM, OpenSSH every algorithm, PuTTY, PGP and one-line SSH (id_rsa, authorized_keys, known_hosts), plus JKS/PKCS#12 keystores. Parsed 100% offline with the built-in X.509 parser — no network.

For auditors

Why use fad-checker for code audits?

What it does for an audit that the others don't. Same column set and sourcing discipline as COMPARISON.md — an amber cell is partial and says how, and every cell is meant to be checkable.

What an auditor actually needs to dofad-checkerOSV-ScannerTrivyGrype+SyftOWASP DCSnyk OSS
Audit a 100-module polyglot monorepo in one command, with no toolchain installed 1✓ 105 modulesreactor skippedneeds ~/.m2opt-inJava buildmvn build
Scan offline / air-gapped without dropping transitive deps 2✓ 657/657~/.m2opt-inmirror
Identify the private/internal deps across a big project 3
Extract cleaned deps descriptors into an external directory 4-t
Report EOL / deprecated frameworks & deps, transitive ones included 5deprecated onlyOS distros onlyweb UI only
Report committed keys & certificates 6key rule
Spot committed binaries (.dll, .exe, …) and check them against their checksums 7somepatterns
Clearly list what was not scanned — before the client asks 8✓ ch. 0 + 6.3logloglogloglog
Answer “against what data?” six months later 9DB dateNVD date
Send a report, not a JSON dump 10✓ HTML + .docHTML listtemplateHTML listsnyk-to-html
Charts, per-CVE drill-down and a pasteable Word copy 11
Make delta reports showing only what changed 12--baselinecloud
  1. No mvn/go/npm/pip/dotnet — manifests parsed off disk, nothing installed or executed. 105 × pom.xml in one pass: 790 pairs vs OSV-Scanner's 657, 133 fad-only, versions mediated per module not flattened.
  2. 657 of 657 of OSV-Scanner's online Maven result, under unshare -rn — no network interface. Tripwire-tested; only public coordinates ever leave the enclave.
  3. Chapter 0 names every coordinate that every configured registry answered 404 for — Maven, npm, PyPI, NuGet, Composer, Go and RubyGems — with the manifest(s) declaring it. A registry that timed out is never counted. -e <regex> then excludes them.
  4. -t <dir>: normalised POMs plus every non-Maven lockfile mirrored, private coordinates stripped. Archivable, and scannable by anything — --snyk included.
  5. endoflife.date, split direct vs transitive so you know which dep to bump, plus deprecated / abandoned / yanked and outdated. Trivy covers OS distros only; Snyk's package health is web-only.
  6. Inventory and verdicts: expiry, RSA<2048, MD5/SHA1, self-signed; private vs public keys; JKS/PKCS#12. Offline parser. Trivy's secret rule finds the file, not the flaw.
  7. Identified by hash via deps.dev + CIRCL → should-be-declared / name≠checksum / unknown / malicious. Syft's patterns name a version, not an identity.
  8. Chapter 0 flags what this scan couldn't reach (missing lockfiles, BOM-only versions, Yarn Berry, undeterminable PHP runtime); chapter 6.3 states what the tool never assesses. Elsewhere the first is a log line the audit never sees, the second isn't written down.
  9. Provenance manifest: tool, runtime, mode, run configuration and cache freshness for all 13 sources. Grype and Dependency-Check carry one source's date, not the run.
  10. Chapters 0→6 with an executive summary and fix recipes, self-contained HTML plus a Word .doc twin. None of the others emits Word.
  11. Four inline-SVG charts — CWE by worst severity, vulnerable transitives per root dep, your most vulnerable modules, fix-priority bands — rendered in the .doc too, with one-click copy as PNG (or a table as rich HTML) that pastes into Word formatted. Every CVE keeps its CVSS vector, CWE, references, CPE config and via-path behind a drill-down, with zero external assets.
  12. --baseline adds a Δ chapter (new / fixed / unchanged); --fail-on-new gates on new findings only. Snyk tracks this on its platform, not as a local diff.

Where it loses — containers/OS packages, auto-fix PRs, and CVE coverage against Snyk's curated feed: the gap, measured.
Deliberately not a goal — reachability. A finding is a vulnerable version on the dependency graph, and the report says exactly that (ch. 6.3) instead of guessing at call paths. Whether the vulnerable code is reachable in this application is the auditor's call, made with application context no scanner has.

The deliverable

An executive summary, then the detail

Severity tiles, a CVE table with a priority column (KEV / EPSS), CWE & fix versions, EOL / obsolete / outdated / license chapters, and a "likely false positives" appendix — all in a single file you can email.

fad-checker HTML report: executive summary with severity tiles and a detailed CVE table with CWE, descriptions and fix versions
cve-report.html — generated by fad -s ./project
Confidential by design

Scan online. Never expose the code.

The vuln databases need the network; your client's code does not. The scan splits in three — only public coordinates ever leave the secure machine. Caches are keyed by coordinate, so warming them online and replaying offline just yields cache hits.

Designed for any air-gapped / confidential review. The online box only ever sees lodash@4.17.21 — never your repository.

# 1 — OFFLINE (audited machine): export anonymized descriptor
fad -s ./proj -e "^(client|internal)\." --export-anonymized deps.json

# 2 — ONLINE (any box, no source): warm the caches
fad --import-anonymized deps.json
fad --export-cache fad-cache.tar.gz

# 3 — OFFLINE again: full report, real paths
fad --import-cache fad-cache.tar.gz
fad -s ./proj --offline
Honest positioning

Where it fits

Not a Trivy/Grype replacement — those own containers and continuous CI. fad-checker owns the zero-setup source audit with an air-gapped story (and now emits SBOM/VEX, flags licenses and prioritises by EPSS/KEV too).

Capabilityfad-checkerOSV-ScannerTrivyGrype+SyftOWASP DCSnyk OSS
Reads lockfiles without build~Java~Maven/pip build
Best-effort without a lockfile~~~
EOL detection~
Outdated / deprecated~exp.~
EPSS / KEV prioritizationKEV~web
License compliance~~✓ paid
SBOM / VEX export✓ CDX+CSAF~✓ Enterprise
HTML + Word reportjsonjsonjsonhtmlcloud
Air-gapped, code never exposed
Containers / OS packages~

² For pom.xml, transitives need Maven Central once — after that fad-checker resolves the graph from its cached POMs, which is what makes the air-gapped run work. Snyk needs a real build, Trivy needs the network or a populated ~/.m2. Reach for Trivy or Grype+Syft for continuous CI, containers and SBOM pipelines.

³ EOL row is scoped to application frameworks and libraries. Trivy does compute an end-of-service-life status, but only for OS distributions. Snyk's package-health signal exists on its website, not in the CLI or CI output. Cells re-verified against upstream docs on 2026-09-14 (OSV-Scanner 2.6.0, Trivy 0.74.0, Grype 0.118.0 + Syft 1.51.1, Dependency-Check 13.0.0, Snyk CLI 1.1307.2) — detail and sources in COMPARISON.md.

Measured, not asserted

Benchmark: Apache Dubbo 2.7.8

Five scanners, one public 105-module Maven reactor pinned to a commit, two questions. Every number below is recomputable from the tools' own JSON — the commands are in the benchmark doc.

1 · At full capability

Every scanner online, best configuration, populated ~/.m2. Union of all findings: 908 pairs.

ScannerFoundUnique to itNot reported
fad-checker790 · 87.0%125118 → 0 real
OSV-Scanner 2.4.0657 · 72.4%0251
Snyk 1.1302.1 (mvn build)603 · 66.4%117305
Trivy 0.72.0546 · 60.1%0362
Grype 0.116.0 + Syft 1.49.045 · 5.0%0863

“Not reported” is union arithmetic, not a verified recall gap. It counts pairs another tool produced and this one did not — a miss only if the public record actually binds that vulnerability to that coordinate and version. For fad-checker that was checked pair by pair: 0 are recall bugs. The other rows have not been adjudicated, so read their column the same way. Trivy and Grype are container/SBOM scanners; a raw source checkout is not the job they are built for.

2 · With no network at all

Same tree, every scanner under unshare -rn — a namespace with no network interface, not just an offline flag. Reference: OSV-Scanner's own online output, 657 pairs.

Scanner, no networkRecovers
fad-checker --offline657 · 100%
Grype + Syft45 · 6.8%
Trivy --offline-scan, cold ~/.m240 · 6.1%
OSV-Scanner --offline37 · 5.6%

100% here means "recovers everything OSV-Scanner finds with network access", not "finds everything that exists" — table 1 is the answer to that. OSV-Scanner returns the identical 37 with and without the namespace: it behaves exactly as documented, its transitive resolution is disabled offline by design.

3 · Every other ecosystem

fad-checker claims ten ecosystems, and benchmarking one of them is not an answer. Same method on five more public projects, findings normalised to (package@version | CVE). Expect parity — that is the point. Outside Maven the transitive graph is already in the lockfile, so no scanner has a structural advantage and none should.

target apache/dubbo 2.7.8 · pinned 0be2a1bb · reads 105 × pom.xml

ScannerPairs
OSV-Scanner657
fad-checker790
communs657
fad seul133
OSV seul0

The one ecosystem where the graph is not in the tree: it must be resolved from the registry. That is why an offline gap exists here and nowhere else.

Equal CVE counts do not make two tools equivalent: on these same projects fad also reported 1 120 outdated dependencies on mastodon, 618 and 2 EOL frameworks on phpMyAdmin, 65 on poetry — questions OSV-Scanner does not attempt. Full per-ecosystem detail, targets and method → BENCHMARK.md.

4 · The Snyk delta, explained

131 pairs came back from Snyk and not from fad-checker. Adjudicated one by one against OSV, none is a recall bug. Here is what they actually are.

01It is one commercial database, not the field

Every such pair is Snyk's. OSV-Scanner, Trivy and Grype+Syft each found 0 pairs nobody else had, so this was never “fad-checker is behind the other scanners” — it is every public advisory source, aggregated, against one hand-curated commercial feed.

02It is a Maven/Java phenomenon

The delta was measured on the Maven target. Outside Maven the resolved graph is already in the lockfile, every scanner reads the same input, and the per-ecosystem tables above measure identical finding sets on npm, RubyGems and Composer. There is no equivalent gap there to explain.

03Two thirds contradict the public record

57 name a coordinate the advisory does not bind, 31 a version outside every declared affected range. Reporting them would mean shipping false positives — which is why every claimed miss is adjudicated before it becomes a ticket.

04The verdicts, against OSV

57 wrong artifact · 31 out of range · 23 not in OSV (19 proprietary SNYK-*, 4 NVD-only) · 19 no Maven binding · 1 already reported under the CVE alias · 0 confirmed. Under NVD's looser CPE ranges 5 would be reported — named, with the tension that creates, in BENCHMARK.md.

05The clean example

CVE-2023-6481 is claimed on logback-classic@1.2.2. It binds logback-core at [1.2.12, 1.2.13) — wrong artifact, and a version published before the flaw existed. GHSA-72hv-8253-57qq is claimed on jackson-core@2.10.4, whose ranges start at 2.15.0.

06Closing it from public data was tried, and failed

--nvd-cpe-match uses NVD's broader CPE ranges to match rather than only to filter. Measured: 76 new findings, 12% corroborated. CPE names frameworks (netty, spring_framework) while Maven names artifacts, so a spring-web flaw lands on spring-core, spring-beans and spring-context alike. It ships as a triage aid with its 12% stated in its own help text.

Which is why --snyk exists

Rather than pick a side, fad-checker takes snyk test output as an input and merges it: union beats either tool alone, and the merge is one flag. On a tree with private modules, extract it with -t first — the normalised descriptors it writes have those coordinates stripped, so Snyk gets something it can actually resolve. Merging it is a coverage choice, not a correction: two thirds of what Snyk adds on its own contradicts the public record. The per-pair adjudication is in BENCHMARK.md.

# strip private modules, scan, merge Snyk
fad -s ./proj -t ../clean -e "^client\." --snyk

Full method, per-tool configuration, the four bugs this benchmark found in fad-checker, and every caveat → BENCHMARK.md.

Documentation

Everything is documented

Per-flag usage, internals, and the full source — all in the repo.

# read-only full scan (default: all sources on)
fad -s ./proj

# pick ecosystems · go fully offline · skip the Maven Central queries
fad -s . --ecosystem maven,npm,pypi
fad -s . --offline

# extract only: normalised descriptors + private modules identified
fad -s ./proj -t ../clean -e "^client\."

# …and the same run, with a scan + Snyk merged in
fad -s ./proj -t ../clean -e "^client\." --snyk