fad-checker 2.4.3 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_POLICY.md +74 -0
- package/CHANGELOG.md +171 -0
- package/CONTRIBUTING.md +61 -0
- package/LICENSE +21 -0
- package/README.md +62 -36
- package/SECURITY.md +60 -0
- package/fad-checker.js +38 -1
- package/lib/attribution.js +116 -0
- package/lib/core.js +58 -2
- package/lib/cpe.js +101 -9
- package/lib/cve-match.js +43 -2
- package/lib/dep-record.js +9 -0
- package/lib/maven-version.js +21 -0
- package/lib/retire.js +19 -4
- package/lib/transitive.js +22 -2
- package/lib/version-overlay.js +67 -22
- package/package.json +1 -1
package/AI_POLICY.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# AI policy
|
|
2
|
+
|
|
3
|
+
## How this project is built
|
|
4
|
+
|
|
5
|
+
`fad-checker` is based on a personal project then enhanced with heavy use of Claude Code. `CLAUDE.md` in the repo root is exactly
|
|
6
|
+
what it looks like: the orientation map the assistant works from.
|
|
7
|
+
|
|
8
|
+
I would rather state that plainly than have someone discover it and wonder what else was not
|
|
9
|
+
said. For a security tool the provenance of the code is a fair question, so here is what the
|
|
10
|
+
work is actually held to.
|
|
11
|
+
|
|
12
|
+
## What the code is held to
|
|
13
|
+
|
|
14
|
+
None of this is a claim about how the code was written. All of it is checkable by you:
|
|
15
|
+
|
|
16
|
+
- **606 tests**, run with `npm test`. No test touches the network.
|
|
17
|
+
- **`test/offline-guarantee.test.js`** is a tripwire: it installs a fetcher that throws if
|
|
18
|
+
anything reaches for the network under `--offline`, on a cold cache. The zero-network
|
|
19
|
+
guarantee is a test, not a promise. It is also reproducible outside the suite:
|
|
20
|
+
`unshare -rn node fad-checker.js -s ./proj --offline` runs in a namespace with no network
|
|
21
|
+
interface at all.
|
|
22
|
+
- **Incremental history.** The work is spread across the commit log, not dropped in one
|
|
23
|
+
initial commit.
|
|
24
|
+
- **Measured, not asserted.** Coverage numbers in the docs come from comparison against a
|
|
25
|
+
Snyk baseline on a real multi-module project, with the methodology stated where they appear.
|
|
26
|
+
- **Fixtures come from real-world shapes.** Parsing behaviour is locked by a fixture in
|
|
27
|
+
`test/fixtures/`, derived from a manifest layout that actually occurs, not from an
|
|
28
|
+
invented one.
|
|
29
|
+
|
|
30
|
+
The honest counterweight: this is a young single-maintainer project and it has false positives
|
|
31
|
+
and false negatives. The [README says so](README.md), and the
|
|
32
|
+
[accuracy issue template](.github/ISSUE_TEMPLATE/false_positive.yml) exists because those
|
|
33
|
+
reports are the most valuable thing the project can receive.
|
|
34
|
+
|
|
35
|
+
## Where review actually mattered
|
|
36
|
+
|
|
37
|
+
One example, because it is the kind of failure that matters for a scanner.
|
|
38
|
+
|
|
39
|
+
The first implementation of per-module Maven version mediation
|
|
40
|
+
(`lib/version-overlay.js`) re-resolved each module using only that module's own
|
|
41
|
+
`<dependencies>`. That is wrong: Maven inherits `<dependencies>` from the parent chain into
|
|
42
|
+
every child, where they stay direct and beat any transitive of the same coordinate. The
|
|
43
|
+
consequence was that a parent-declared, reactor-wide remediation was invisible, so the overlay
|
|
44
|
+
"recovered" the very version that remediation overrides, and reported CVEs against a version
|
|
45
|
+
no classpath holds.
|
|
46
|
+
|
|
47
|
+
Fabricating findings is worse for a security tool than missing them. The fix walks the local
|
|
48
|
+
parent chain, and `test/version-overlay-inherited-direct.test.js` locks the behaviour in both
|
|
49
|
+
directions: the masked version must surface, and the overridden one must not.
|
|
50
|
+
|
|
51
|
+
The general rule this project follows: a plausible-looking finding is not a finding. Unresolved
|
|
52
|
+
versions are excluded from matching rather than assumed vulnerable, CPE configurations are
|
|
53
|
+
cross-checked against the actual version, and there is no `!version means affected` shortcut
|
|
54
|
+
anywhere in the matcher.
|
|
55
|
+
|
|
56
|
+
## What is expected of contributions
|
|
57
|
+
|
|
58
|
+
The same bar, regardless of how the code was produced:
|
|
59
|
+
|
|
60
|
+
- A test that fails before the change and passes after it.
|
|
61
|
+
- For parsing changes, a fixture derived from a real-world shape.
|
|
62
|
+
- No claim in the docs that is not backed by something a reader can run.
|
|
63
|
+
- You understand the change well enough to explain it in review without assistance.
|
|
64
|
+
|
|
65
|
+
Use whatever tools you like to get there. What is not welcome is a change whose author cannot
|
|
66
|
+
answer questions about it, because reviewing that costs more than writing it did. See
|
|
67
|
+
[`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
68
|
+
|
|
69
|
+
## Reports generated by this tool
|
|
70
|
+
|
|
71
|
+
`fad-checker` uses **no LLM at runtime**. Findings come from public vulnerability databases
|
|
72
|
+
(CVEProject, OSV.dev, NVD), registry metadata and deterministic parsers. Severity, priority and
|
|
73
|
+
version matching are computed, not generated. Nothing in a report is written by a language
|
|
74
|
+
model, and the tool makes no network call to any AI service, ever.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,178 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
### Added
|
|
9
|
+
- **`--nvd-cpe-match` (opt-in, off by default): match dependencies against NVD's CPE version
|
|
10
|
+
ranges.** OSV/GHSA declare affected ranges per release *branch*; NVD declares them for every
|
|
11
|
+
affected branch. For `CVE-2020-9546`, OSV covers 2.9.0–2.9.10.4 while NVD also covers
|
|
12
|
+
2.0.0–2.7.9.7 — so `jackson-databind:2.5.2` is affected and never got a fix. fad already had
|
|
13
|
+
that data in its NVD cache and only ever used it **subtractively**, to filter false positives.
|
|
14
|
+
This uses it additively, restricted to coordinates with an **unambiguous 1:1** entry in
|
|
15
|
+
`data/cpe-coord-map.json` (no name heuristics), and only for CVEs already enriched, so it adds
|
|
16
|
+
no network path.
|
|
17
|
+
|
|
18
|
+
**It is off by default because its measured precision is poor, and the reason is structural.**
|
|
19
|
+
On Apache Dubbo 2.7.8 it adds 76 findings of which **9 (12%) are corroborated by Snyk**. CPE
|
|
20
|
+
products are *framework*-level (`spring_framework`, `netty`, `log4j`) while Maven coordinates
|
|
21
|
+
are *artifact*-level, so a framework CVE lands on every artifact of that framework —
|
|
22
|
+
`CVE-2016-1000027` is a spring-web flaw and CPE puts it on spring-core, `CVE-2019-20444` is
|
|
23
|
+
netty-codec-http and CPE puts it on netty-common. Curation cannot fix a granularity mismatch;
|
|
24
|
+
allowing the map's deliberate 1:N entries made it worse still (262 findings, 8% corroborated).
|
|
25
|
+
Shipped as a triage aid ("what might I be missing?"), never as a default. Locked by
|
|
26
|
+
`test/nvd-cpe-match.test.js`, including a test asserting that a coordinate with no curated
|
|
27
|
+
entry is not matched even when the name heuristic would have accepted it.
|
|
28
|
+
|
|
29
|
+
The investigation behind it also settled what fad's 118 benchmark misses actually are, and
|
|
30
|
+
the answer is not flattering: **they are real misses, not the other scanner's noise.** All 87
|
|
31
|
+
public-CVE ones were checked against NVD — 7 confirmed, 13 where NVD's own range disagrees,
|
|
32
|
+
67 where NVD names no CPE for the artifact. Both minorities were traced. The "NVD is silent"
|
|
33
|
+
bulk are public-database coverage gaps: `CVE-2023-6481` on `logback-classic@1.2.2` exists in
|
|
34
|
+
OSV with **no Maven package binding at all** (only a GIT range), while its own fixed-version
|
|
35
|
+
list `1.2.12, 1.3.13, 1.4.13` shows the 1.2.x branch was affected and fixed at 1.2.12 — so
|
|
36
|
+
1.2.2 is vulnerable and no public-source scanner can see it. The "NVD contradicts" cases are
|
|
37
|
+
NVD contradicting itself: sibling jackson-databind gadget CVEs published weeks apart declare
|
|
38
|
+
`2.0.0–2.7.9.7 / 2.8.0–2.8.11.6 / 2.9.0–2.9.10.4` (CVE-2020-9546) versus `2.9.0–2.9.10.4`
|
|
39
|
+
alone (CVE-2020-10672). Public advisory data declares ranges per release *branch* and old
|
|
40
|
+
unpatched branches are routinely absent; a hand-curated commercial database fills that in and
|
|
41
|
+
aggregating public sources does not reproduce it. Documented in `docs/BENCHMARK.md` rather
|
|
42
|
+
than left as "not yet diagnosed".
|
|
43
|
+
|
|
8
44
|
### Fixed
|
|
45
|
+
- **An imported BOM's `<properties>` leaked into the importing project — and won.**
|
|
46
|
+
`<scope>import</scope>` imports a BOM's `<dependencyManagement>` and **nothing else**: the
|
|
47
|
+
BOM resolves its managed versions in its own property context, and its `<properties>` never
|
|
48
|
+
become the importer's. `core.js` merged them, and merged them so the BOM won
|
|
49
|
+
(`{...merged.properties, ...imported.properties}`), so a BOM silently redefined the
|
|
50
|
+
importing project's own property values. On Apache Dubbo 2.7.8 the reactor root sets
|
|
51
|
+
`<hibernate_validator_version>5.2.4.Final</hibernate_validator_version>` and
|
|
52
|
+
`dubbo-dependencies-bom` redefines it to `5.4.1.Final`, so `dubbo-filter-validation`'s
|
|
53
|
+
`<version>${hibernate_validator_version}</version>` resolved to the wrong version — and a
|
|
54
|
+
different version is a different CVE set. (`mvn dependency:tree` reports
|
|
55
|
+
`hibernate-validator:jar:5.2.4.Final:test` for that module.) The BOM's managed entries are
|
|
56
|
+
now interpolated against the BOM's own properties at the import boundary and the properties
|
|
57
|
+
are dropped, mirroring what `transitive.js#effectivePom` already did for EXTERNAL import
|
|
58
|
+
BOMs. Locked by `test/bom-property-leak.test.js`, which guards both directions: the leak
|
|
59
|
+
must stop **and** importing a BOM must still supply managed versions.
|
|
60
|
+
- **A version declared only at test scope is now reported as dev.** `isDev` lives on the
|
|
61
|
+
coord-wide record, so a version declared solely at `<scope>test</scope>` inherited the
|
|
62
|
+
coordinate's production flag whenever the same coordinate was production at some other
|
|
63
|
+
version — counting toward the production total and the `--fail-on` gate. Per-version scopes
|
|
64
|
+
are now recorded next to per-version paths (`versionScopes`, mirror of `versionPaths`), and
|
|
65
|
+
attribution applies the same widest-wins rule already used for overlay-recovered versions.
|
|
66
|
+
On Dubbo, `hibernate-validator:5.2.4.Final` moves to the dev chapter, attributed to
|
|
67
|
+
`dubbo-filter-validation` — exactly what Maven reports.
|
|
68
|
+
|
|
69
|
+
Air-gapped recall on the public benchmark reaches **657/657 (100%)** of OSV-Scanner's own
|
|
70
|
+
online finding set, up from 653.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
- **The benchmark now measures every scanner at full capability, not just air-gapped.**
|
|
74
|
+
`docs/BENCHMARK.md` carries two tables, because they answer different questions. **Full
|
|
75
|
+
capability** (all five online, best configuration, populated `~/.m2`, union 908 pairs):
|
|
76
|
+
fad-checker 790 (87.0%), OSV-Scanner 657, Snyk 603, Trivy 546, Grype+Syft 45. **No tool
|
|
77
|
+
finds everything, fad included** — its 118 misses all come from Snyk, 30 of them under a
|
|
78
|
+
proprietary `SNYK-*` id no public database carries, and **88 genuine public-CVE misses that
|
|
79
|
+
remain undiagnosed**. **No network** (`unshare -rn`, against OSV-Scanner's online output):
|
|
80
|
+
fad 657/657, Grype+Syft 45, Trivy 40, OSV-Scanner 37.
|
|
81
|
+
Also documented: Trivy's result is identical online and with a fully populated `~/.m2` (the
|
|
82
|
+
local repository substitutes for the network entirely), Grype+Syft does not move at all
|
|
83
|
+
between default and fully-enabled configuration, and the `settings.xml` mirror trick for
|
|
84
|
+
reproducing the run when Maven Central rate-limits the IP.
|
|
85
|
+
- **The per-module overlay could not recover a version held only on a TEST classpath.**
|
|
86
|
+
The overlay exists because the global transitive pass dedupes by `g:a` across the whole
|
|
87
|
+
reactor and keeps one version per coordinate — but it hardcoded
|
|
88
|
+
`includedScopes: ["compile","runtime","provided"]`, so a version reachable only through a
|
|
89
|
+
test-scoped dependency was structurally unreachable. Measured on Apache Dubbo 2.7.8, this
|
|
90
|
+
one omission accounted for **every one** of the 78 findings OSV-Scanner reported that fad
|
|
91
|
+
missed (`jackson-databind:2.8.4:test` in dubbo-registry-sofa,
|
|
92
|
+
`hibernate-validator:5.2.4.Final:test` in dubbo-filter-validation, `okhttp:3.11.0` /
|
|
93
|
+
`okio:1.14.0` in dubbo-configcenter-apollo, `commons-compress:1.18:test` in
|
|
94
|
+
dubbo-remoting-etcd3 — each verified against `mvn dependency:tree`).
|
|
95
|
+
Air-gapped recall on that project: **579 → 653 of 657 (88.1% → 99.4%)**, production
|
|
96
|
+
findings unchanged at 651, dev findings 11 → 147, **zero production finding lost**.
|
|
97
|
+
- **A version is now dev only when EVERY module resolving it does so at test scope.**
|
|
98
|
+
On Dubbo, `jackson-databind:2.10.4` is test-scoped in `dubbo-config-spring` but
|
|
99
|
+
**compile**-scoped in `dubbo-configcenter-nacos`. Reading the first recorded provenance
|
|
100
|
+
called the version dev and dropped a genuine production finding out of the count and out
|
|
101
|
+
of the `--fail-on` gate.
|
|
102
|
+
- **The overlay records provenance per module even when the version is already known.**
|
|
103
|
+
It used to `continue` on the first module to contribute a version, so a second module
|
|
104
|
+
resolving the same version at a different scope left no trace at all — which is exactly
|
|
105
|
+
what made the previous item invisible.
|
|
106
|
+
- **A DECLARED version now wins over any transitive provenance for the same version.**
|
|
107
|
+
`xstream:1.4.10` is declared outright in `dubbo-registry-eureka` *and* reached as a
|
|
108
|
+
test-scoped transitive of `dubbo-config-api`; letting the transitive provenance win
|
|
109
|
+
demoted 35 findings, one of them KEV, into the dev chapter. A manifest that writes
|
|
110
|
+
`<version>` for a coordinate is the authority on that version.
|
|
111
|
+
|
|
112
|
+
### Previously fixed
|
|
113
|
+
- **The transitive closure of a test-scoped dependency was never scanned.** Maven's scope
|
|
114
|
+
matrix says `test → compile = test`: the compile dependencies of a test-scoped dependency
|
|
115
|
+
are on the test classpath, and so are theirs, recursively (only `test → test` is omitted).
|
|
116
|
+
`expandWithTransitives` passed test-scoped roots into resolution (`includeTestDeps`, on
|
|
117
|
+
unless `--ignore-test`) but `resolveTransitiveDeps` then filtered accepted propagated
|
|
118
|
+
scopes to `compile/runtime/provided`, so **every child of a test root was discarded at the
|
|
119
|
+
first hop**. Net effect: the dev chapter only ever listed *directly declared* test
|
|
120
|
+
dependencies, never their transitives. On Apache Dubbo 2.7.8 this hid
|
|
121
|
+
`spring-boot:1.5.17.RELEASE` and `spring-boot-autoconfigure:1.5.17.RELEASE`, four hops down
|
|
122
|
+
`registry-test → registry-server-integration → spring-boot-starter`, all of which
|
|
123
|
+
`mvn dependency:tree` reports at scope=test.
|
|
124
|
+
- **Scope is now widened, never narrowed, when a coord is reached by several paths.** This is
|
|
125
|
+
the half of the fix above that matters most. The traversal dedupes by `g:a` and keeps the
|
|
126
|
+
first chain walked, so marking everything under a test root as dev let **BFS order decide
|
|
127
|
+
the scope**: a coordinate reachable from *both* a test root and a compile root got stamped
|
|
128
|
+
test if the test path happened to be walked first. Measured: 6 production findings
|
|
129
|
+
(`spring-core:4.3.16.RELEASE`, `commons-lang:2.6`, …) silently moved into the dev chapter,
|
|
130
|
+
dropping out of the production count **and out of the `--fail-on` gate**. A false negative
|
|
131
|
+
on the production classpath is worse than the gap being fixed. A revisit now upgrades
|
|
132
|
+
`test` to the wider propagated scope and never the reverse. Locked by
|
|
133
|
+
`test/transitive-test-scope.test.js` (6 tests, including the both-paths case and the
|
|
134
|
+
`test → test` omission).
|
|
135
|
+
Net on Dubbo: **575 → 579** recovered pairs, production findings **unchanged at 650**, dev
|
|
136
|
+
findings 7 → 11.
|
|
137
|
+
- **Maven hard-pin versions (`[1.2.3]`) are now normalised to the bare version.**
|
|
138
|
+
Maven's `[x]` syntax means *exactly* x — a concrete version wearing range brackets —
|
|
139
|
+
and real upstream POMs use it (`io.grpc:grpc-netty:1.22.1` declares
|
|
140
|
+
`<version>[4.1.35.Final]</version>`). fad kept the brackets verbatim, so the coordinate
|
|
141
|
+
was wrong **everywhere downstream**: the report, the purl, and every SBOM/CSAF/SARIF/JSON
|
|
142
|
+
export carried `netty-codec-http2@[4.1.35.Final]`, which cannot be joined with any other
|
|
143
|
+
tool's output for the same dependency. Fixed by `lib/maven-version.js#normalizeHardPin`,
|
|
144
|
+
applied on both paths that produce a version — `cve-match.js#resolveDepVersion` (declared
|
|
145
|
+
deps, after `${…}` interpolation, so `[${netty.version}]` works) and `lib/transitive.js`
|
|
146
|
+
(deps read out of upstream POMs, which is where this actually came from). A **genuine
|
|
147
|
+
range keeps its brackets**: choosing a version out of `[1.0,2.0)` is resolution, not
|
|
148
|
+
normalisation, and it must keep surfacing as unresolved rather than silently becoming
|
|
149
|
+
concrete. Found by the new public benchmark, where it cost **9 recovered findings**
|
|
150
|
+
(566 → 575 of OSV-Scanner's reference set on Apache Dubbo 2.7.8). Note that the OSV cache
|
|
151
|
+
is keyed by coordinate **and** version, so this fix invalidates entries warmed under the
|
|
152
|
+
old string — an offline re-run needs a cache re-warm to see the corrected coordinate.
|
|
153
|
+
- **Docs referenced a `--transitive` flag that does not exist.** Transitive resolution is
|
|
154
|
+
**on by default**; `--no-transitive` disables it. Corrected in `ARCHITECTURE.md` and
|
|
155
|
+
`COMPARISON.md`.
|
|
156
|
+
|
|
157
|
+
### Added
|
|
158
|
+
- **`docs/BENCHMARK.md` — a reproducible air-gapped recall benchmark.** Replaces the
|
|
159
|
+
unverifiable private-project figure that headlined the README. Measured on **Apache Dubbo
|
|
160
|
+
2.7.8** (105-module reactor, pinned commit), with both scanners run under `unshare -rn` in
|
|
161
|
+
a namespace with **no network interface**, and graded against **OSV-Scanner's own online
|
|
162
|
+
output** as the reference set (657 distinct `package@version | vulnerability` pairs) rather
|
|
163
|
+
than against fad's own notion of a finding: **fad-checker recovers 575 (87.5%)**,
|
|
164
|
+
**OSV-Scanner recovers 37 (5.6%)**. Documents the exact commands, the tool versions, the
|
|
165
|
+
82 pairs fad misses **and why** (version-mediation divergence, plus two genuinely
|
|
166
|
+
unresolved `spring-boot` coordinates), and the caveats — a warmed cache is required, and
|
|
167
|
+
one project is one shape.
|
|
168
|
+
|
|
169
|
+
### Changed
|
|
170
|
+
- `docs/COMPARISON.md`: four competitor cells corrected after re-verification against
|
|
171
|
+
upstream docs and source. Syft **does** have Maven transitive resolution
|
|
172
|
+
(`java.resolve-transitive-dependencies`, opt-in, off by default); Trivy consults a local
|
|
173
|
+
`~/.m2` before the network; Trivy **does** report end-of-service-life, but only for OS
|
|
174
|
+
distributions (the EOL row is now scoped to *application* frameworks); and the
|
|
175
|
+
"scan without exposing the codebase" row now concedes the SBOM-then-scan-online route,
|
|
176
|
+
keeping only the two differences that are sourceable. Adds a version stamp for every tool
|
|
177
|
+
compared and an explicit note that no ⚠️/❌ cell means "unmaintained".
|
|
178
|
+
|
|
179
|
+
### Previously fixed
|
|
9
180
|
- **External `<parent>` POMs (spring-boot-starter-parent) now backfill their managed
|
|
10
181
|
versions.** A versionless dep whose version is inherited from an external `<parent>`
|
|
11
182
|
(e.g. `spring-boot-starter-actuator` under `spring-boot-starter-parent`, whose own
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for looking. This is a young single-maintainer project, so the most valuable
|
|
4
|
+
contribution isn't necessarily code — it's **telling me where the output is wrong**.
|
|
5
|
+
|
|
6
|
+
## The most useful thing you can do
|
|
7
|
+
|
|
8
|
+
Run it on a real project and report what it got wrong:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx fad-checker -s ./your-project
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A [false positive / false negative report](https://github.com/9pings/fad-checker/issues/new?template=false_positive.yml)
|
|
15
|
+
with the coordinate and the manifest snippet that produced it is worth more than a feature
|
|
16
|
+
request. Dependency resolution has a long tail of real-world shapes — inherited BOMs, profile
|
|
17
|
+
matrices, `replace` directives, vendored jars — and the only way to cover them is to be shown
|
|
18
|
+
one that breaks.
|
|
19
|
+
|
|
20
|
+
## Development setup
|
|
21
|
+
|
|
22
|
+
Node ≥ 20, no other tooling required.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install
|
|
26
|
+
npm test # 606 tests via node --test
|
|
27
|
+
node --test test/core.test.js # a single file
|
|
28
|
+
node fad-checker.js -s test/fixtures/monorepo-mixed --offline --no-report
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Building the single-file binaries needs [bun](https://bun.sh): `npm run build`.
|
|
32
|
+
|
|
33
|
+
## Ground rules for changes
|
|
34
|
+
|
|
35
|
+
- **Tests must not touch the network.** Every fixture-driven test drives an in-memory
|
|
36
|
+
registry/fetcher. `test/offline-guarantee.test.js` is a tripwire that fails if any code path
|
|
37
|
+
reaches for the network under `--offline` — don't work around it.
|
|
38
|
+
- **Add a fixture, not just an assertion.** New parsing behaviour belongs in
|
|
39
|
+
`test/fixtures/`, next to the shapes that already live there.
|
|
40
|
+
- **A parse failure must never kill the run.** One malformed manifest logs a warning and the
|
|
41
|
+
scan continues; there is no `process.exit(1)` mid-pipeline.
|
|
42
|
+
- **New ecosystem = new codec.** Implement the interface in
|
|
43
|
+
`lib/codecs/codec.interface.js`, register it, and the orchestrator needs no edits. See
|
|
44
|
+
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
45
|
+
- Commit messages follow Conventional Commits (`fix(maven): …`, `feat(gradle): …`).
|
|
46
|
+
- [`CLAUDE.md`](CLAUDE.md) is the code-level orientation map — conventions, invariants and the
|
|
47
|
+
reasoning behind the non-obvious ones. Read it before a non-trivial change; it will save you
|
|
48
|
+
from "simplifying" something that's load-bearing.
|
|
49
|
+
|
|
50
|
+
## On AI-assisted development
|
|
51
|
+
|
|
52
|
+
This codebase is written with heavy use of Claude Code, and I'm not going to pretend
|
|
53
|
+
otherwise — `CLAUDE.md` in the repo root is exactly what it looks like. What I ask of my own
|
|
54
|
+
changes and of contributions is the same either way: a test that fails before and passes
|
|
55
|
+
after, a fixture derived from a real-world shape, and no claim in the docs that isn't backed
|
|
56
|
+
by something you can run. If you find a place where the code doesn't meet that bar, that's a
|
|
57
|
+
bug report I want.
|
|
58
|
+
|
|
59
|
+
## Reporting a vulnerability in fad-checker itself
|
|
60
|
+
|
|
61
|
+
See [`SECURITY.md`](SECURITY.md) — please report privately, not as a public issue.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pp9Ping
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,34 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/fad-checker)
|
|
4
4
|
[](https://www.npmjs.com/package/fad-checker)
|
|
5
|
-
[](
|
|
5
|
+
[](LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
|
+
[](https://github.com/9pings/fad-checker/actions/workflows/ci.yml)
|
|
7
8
|
|
|
8
9
|
> **F**abulous **A**utonomous **D**ependency **C**hecker<br>
|
|
9
10
|
> AKA **F**uckin' **A**utonomous **D**ependency **C**hecker<br>
|
|
10
11
|
|
|
11
|
-
`fad-checker` audits **Maven · Gradle · npm · Yarn · pnpm · Composer · PyPI · NuGet · Go · Ruby**, vendored JavaScript, committed native binaries and cryptographic material (certificates & private/public keys) in any source tree
|
|
12
|
+
`fad-checker` audits **Maven · Gradle · npm · Yarn · pnpm · Composer · PyPI · NuGet · Go · Ruby**, vendored JavaScript, committed native binaries and cryptographic material (certificates & private/public keys) in any source tree; multi-module, monorepo, polyglot; and produces a self-contained **HTML + Word report** (CVE prioritised by EPSS + CISA KEV, EOL, obsolete, outdated, licenses) plus **CycloneDX SBOM / CSAF VEX / SARIF / JSON** exports. **No build tools, no Docker, no network needed**; it reads lockfiles and manifests straight off disk.
|
|
12
13
|
|
|
13
14
|
🌐 **[Project site & docs →](https://9pings.github.io/fad-checker/)**
|
|
14
15
|
|
|
15
16
|
> [!WARNING]
|
|
16
|
-
> **Young project
|
|
17
|
+
> **Young project; expect rough edges.** fad-checker is new and under active development, so it may still contain bugs (including false positives and false negatives). Treat its output as a strong first pass, **double-check anything critical**, and please [report issues](https://github.com/9pings/fad-checker/issues); they get fixed fast.
|
|
17
18
|
|
|
18
|
-
<p align="center"><img src="docs/assets/demo.gif" alt="fad-checker animated terminal demo
|
|
19
|
+
<p align="center"><img src="docs/assets/demo.gif" alt="fad-checker animated terminal demo; a [n/N] checklist warming each vulnerability database, then CVE findings coloured by severity with KEV badges" height="600"></p>
|
|
19
20
|
|
|
20
21
|
## Features
|
|
21
22
|
|
|
22
|
-
- **10 ecosystems in one pass
|
|
23
|
-
- **Crypto material
|
|
24
|
-
- **No build tools
|
|
25
|
-
- **CVE, merged & prioritised
|
|
26
|
-
- **Per-module Maven version mediation
|
|
27
|
-
- **Air-gapped
|
|
28
|
-
- **Supply-chain risk
|
|
29
|
-
- **Lifecycle
|
|
30
|
-
- **Licenses** *(opt-in `--licenses`)
|
|
31
|
-
- **Audit-grade & reproducible
|
|
32
|
-
- **Outputs & CI
|
|
23
|
+
- **10 ecosystems in one pass**; Maven, **Gradle**, npm/Yarn/pnpm, Composer (PHP), PyPI, NuGet, Go, Ruby; plus **vendored JS** (retire.js), committed **native binaries** (`.dll`/`.exe`/`.so`/`.dylib`, identified by checksum via deps.dev + CIRCL) and **embedded JARs** (fat-jars/war/ear, unzipped in-memory).
|
|
24
|
+
- **Crypto material**; committed **certificates** (X.509, PEM/DER), **private & public keys** (PEM / OpenSSH every algorithm / PuTTY / PGP / one-line SSH) and **keystores** (JKS/JCEKS/PKCS#12). Each key is labelled **private** (a committed secret → critical) or **public**; certs are checked for **expiry, weak key (RSA<2048), weak signature (MD5/SHA1) and self-signed**; all parsed offline with the built-in X.509 parser, no network. `--no-certs` to disable.
|
|
25
|
+
- **No build tools**; reads `pom.xml`, `build.gradle(.kts)`/`gradle.lockfile`/`libs.versions.toml`, `package-lock`/`yarn.lock`/`pnpm-lock`, `composer.lock`, `poetry`/`Pipfile`/`uv`/`pdm` locks, `packages.lock.json`/`*.csproj`, `go.mod`, `Gemfile.lock` directly. No `mvn`/`gradle`/`npm install`/`pip`/`dotnet restore`/`go build`/`bundle`, no `node_modules/`. → [how it stays build-free](docs/COMPARISON.md#how-its-autonomous-no-build-tools)
|
|
26
|
+
- **CVE, merged & prioritised**; CVEProject + OSV.dev + NVD, CPE/version cross-checked to cut false positives, ranked **CISA KEV → EPSS → CVSS**.
|
|
27
|
+
- **Per-module Maven version mediation**; recovers vulnerable transitive versions that a global `<dependencyManagement>` pin hides in another module, applying Maven's own nearest-wins semantics per module rather than resolving the whole reactor as one tree.
|
|
28
|
+
- **Air-gapped**; **zero network under `--offline`** (regression-tested), offline Maven transitive resolution, and `--osv-db` for cache-independent offline OSV recall. Benchmarked on **Apache Dubbo 2.7.8** (105 modules) against OSV-Scanner, Snyk, Trivy and Grype+Syft: at **full capability** fad finds 790 of a 908-pair union (87%, the highest of the five, and it still misses 118 that Snyk finds); **under `unshare -rn` with no network interface** it recovers **657/657** of OSV-Scanner's *online* result, versus 45 / 40 / 37 for the others. → [Benchmark](docs/BENCHMARK.md) · [Air-gapped](#air-gapped-audits)
|
|
29
|
+
- **Supply-chain risk**; known-**malicious** advisories (`MAL-`, always block the CI gate) + suspected **typosquats** (`--typosquat`).
|
|
30
|
+
- **Lifecycle**; EOL (endoflife.date), obsolete/deprecated, outdated; across every ecosystem.
|
|
31
|
+
- **Licenses** *(opt-in `--licenses`)*; SPDX-normalised, copyleft/proprietary flagged.
|
|
32
|
+
- **Audit-grade & reproducible**; every report carries a **provenance manifest** (data-source freshness + run config) and a **Methodology, data sources & limitations** chapter; artifacts ship a **`SHA256SUMS`** integrity manifest (`sha256sum -c`); **differential audits** diff against a prior run (`--baseline`, or `fad diff a.json b.json`) and CI can gate on *new* findings (`--fail-on-new`).
|
|
33
|
+
- **Outputs & CI**; HTML + Word `.doc`, CycloneDX 1.6 SBOM, CSAF 2.0 VEX, SARIF 2.1.0, JSON; gate with `--fail-on` / `--fail-on-new`, triage with `--ignore`/`--vex`. Private registries for Maven, npm, PyPI, Ruby, Go, **NuGet** and **Composer**.
|
|
33
34
|
|
|
34
35
|
📖 **[Usage & all flags](docs/USAGE.md)** · **[Architecture](docs/ARCHITECTURE.md)** · **[Comparison vs other tools](docs/COMPARISON.md)** · **[Data sources](docs/DATA-SOURCES.md)**
|
|
35
36
|
|
|
@@ -40,18 +41,25 @@ npm install -g fad-checker
|
|
|
40
41
|
fad-checker -s ./my-project # → ./fad-checker-report/cve-report.html
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
A free [NVD API key](https://nvd.nist.gov/developers/request-an-api-key) (instant) gives 10× faster enrichment: `fad-checker --set-nvd-key YOUR_KEY`. A few common runs
|
|
44
|
+
A free [NVD API key](https://nvd.nist.gov/developers/request-an-api-key) (instant) gives 10× faster enrichment: `fad-checker --set-nvd-key YOUR_KEY`. A few common runs; full list via `fad-checker --help` or [docs/USAGE.md](docs/USAGE.md):
|
|
44
45
|
|
|
45
46
|
```bash
|
|
46
47
|
fad-checker -s ./proj -e "^com\.acme\." # exclude private libs (coord regex)
|
|
47
48
|
fad-checker -s ./proj -t ../clean -e "^com\.acme\." --snyk # cleaned POM tree + merge Snyk
|
|
48
|
-
fad-checker -s ./proj --offline # fully offline (zero network)
|
|
49
|
+
fad-checker -s ./proj --offline # fully offline (zero network, needs a warmed cache)
|
|
49
50
|
fad-checker -s ./proj --osv-db --typosquat # offline-complete OSV + typosquat
|
|
50
51
|
fad-checker -s ./proj --licenses --fail-on high # license chapter + CI gate
|
|
51
52
|
fad-checker -s ./proj --report-json --baseline last.json --fail-on-new # differential audit: fail CI on NEW findings
|
|
52
53
|
fad-checker diff last.json this.json # standalone diff of two findings JSONs
|
|
53
54
|
```
|
|
54
55
|
|
|
56
|
+
> [!IMPORTANT]
|
|
57
|
+
> **`--offline` reads the cache, it doesn't replace it.** On a *cold* cache there is nothing to
|
|
58
|
+
> match against, so an offline first run legitimately reports **0 CVE / 0 EOL / 0 outdated**;
|
|
59
|
+
> that's an empty cache, not a clean project. Warm it once (a normal online run on any project,
|
|
60
|
+
> or `--import-cache`), then `--offline` returns the full result set with zero network calls.
|
|
61
|
+
> Air-gapped machines get their cache via [`--export-cache` / `--import-cache`](#air-gapped-audits).
|
|
62
|
+
|
|
55
63
|
A single self-contained binary (no Node), from-source install and shell completion are in → [docs/USAGE.md](docs/USAGE.md).
|
|
56
64
|
|
|
57
65
|
## What it finds
|
|
@@ -61,51 +69,51 @@ The report is organised into **root chapters** (each grouping related sub-chapte
|
|
|
61
69
|
| Chapter | Source | What it catches |
|
|
62
70
|
| --- | --- | --- |
|
|
63
71
|
| **0. Warnings** *(top)* | local heuristics | Missing lockfiles, unresolved Maven versions (BOM-managed), private libs not on Maven Central |
|
|
64
|
-
| **Δ. Changes since baseline** *(top, with `--baseline`)* | diff vs prior JSON | New / fixed / unchanged findings per category + the list of **new production CVEs
|
|
65
|
-
| **1. CVE** *(X direct, Y indirect, Z dev)* | CVEProject + OSV.dev + NVD + CPE | **1.1 Production
|
|
66
|
-
| **2. Unmanaged / unversioned components** | deps.dev + CIRCL (by checksum), retire.js, built-in X.509 | **2.1 Embedded binaries
|
|
72
|
+
| **Δ. Changes since baseline** *(top, with `--baseline`)* | diff vs prior JSON | New / fixed / unchanged findings per category + the list of **new production CVEs**; for repeat audits and `--fail-on-new` CI gating |
|
|
73
|
+
| **1. CVE** *(X direct, Y indirect, Z dev)* | CVEProject + OSV.dev + NVD + CPE | **1.1 Production**; public CVE / GHSA in prod deps, per ecosystem, per manifest, **prioritised** by CISA KEV + EPSS + CVSS · **1.2 Vendored JS vulns** ([retire.js](https://retirejs.github.io/)) · **1.3 Dev** (`test`/`provided`, `dev`/`optional`/`peer`) · **1.4 Likely false positives** (CPE-filtered) |
|
|
74
|
+
| **2. Unmanaged / unversioned components** | deps.dev + CIRCL (by checksum), retire.js, built-in X.509 | **2.1 Embedded binaries**; CVEs in libs shipped inside committed `.jar`/`.war`/`.ear` (fat-jars, shaded uber-jars) · **2.2 Native binaries** (`.dll`/`.exe`/`.so`/`.dylib`) identified by hash, flagged should-be-managed / name≠checksum / unknown / malicious · **2.3 Vendored JavaScript** inventory (jQuery, Bootstrap, …) vulnerable *or not* · **2.4 Certificates & key material**; committed certs (expiry / weak key / weak signature / self-signed), **private vs public keys** (PEM/OpenSSH/PuTTY/PGP/SSH) and keystores, all parsed offline |
|
|
67
75
|
| **3. Maintenance / lifecycle** *(X EOL, Y obsolete, Z outdated)* | endoflife.date · curated + registry flags · Maven Central / npm / Packagist / PyPI / NuGet | **3.1 End-of-Life** frameworks · **3.2 Obsolete / deprecated / abandoned / yanked** · **3.3 Outdated** (newer version available, with release dates) |
|
|
68
76
|
| **4. Licenses** *(opt-in: `--licenses`)* | registry metadata + Maven POMs → SPDX policy | Each dep's license normalised to SPDX and classified; copyleft (GPL/AGPL/LGPL/MPL), proprietary and unknown flagged for review |
|
|
69
77
|
| **5. Fix Recommendations** | computed | Per-ecosystem pin recipes: Maven `<dependencyManagement>`, Gradle `constraints { }`, npm `overrides`, yarn `resolutions`, `composer require`, `pip install`, `dotnet add package` |
|
|
70
78
|
| **6. Scan context & limitations** | provenance manifest + walk | **6.1 Scanned descriptors** (every manifest parsed) · **6.2 Ignored directories** (pruned paths + rule) · **6.3 Methodology, data sources & limitations** (data-source freshness, run config, explicit statement of **what fad-checker does *not* assess**) |
|
|
71
|
-
| **Supply-chain risk** *(cross-cutting)* | OSV `MAL-…` + name heuristic | **Known-malicious** packages (always block the CI gate, any `--fail-on` level) and **suspected typosquats** (`--typosquat`: an npm/PyPI name one edit from a popular package
|
|
79
|
+
| **Supply-chain risk** *(cross-cutting)* | OSV `MAL-…` + name heuristic | **Known-malicious** packages (always block the CI gate, any `--fail-on` level) and **suspected typosquats** (`--typosquat`: an npm/PyPI name one edit from a popular package; `lodahs`↔`lodash`) |
|
|
72
80
|
|
|
73
81
|
The HTML report opens in any browser, contains every detail (CVSS vectors, references, full descriptions, CPE configurations, via-paths for transitives) and ships a Word-compatible `.doc` twin. Every match carries a **composite priority** (KEV-exploited > EPSS likelihood > CVSS severity), and the run can additionally emit a **CycloneDX 1.6 SBOM** (`--report-sbom`, vulnerabilities inline) and a **CSAF 2.0 VEX** (`--report-csaf`) for downstream tooling.
|
|
74
82
|
|
|
75
|
-
<p align="center"><img src="docs/assets/report.png" alt="fad-checker HTML report
|
|
83
|
+
<p align="center"><img src="docs/assets/report.png" alt="fad-checker HTML report; executive summary with severity tiles and a detailed CVE table with CWE, descriptions and fix versions" width="900"></p>
|
|
76
84
|
|
|
77
85
|
## Air-gapped audits
|
|
78
86
|
|
|
79
87
|
> **Zero-data-sent guarantee.** Under `--offline`, fad-checker makes **no network calls
|
|
80
|
-
> whatsoever
|
|
88
|
+
> whatsoever**; it reads only the warmed `~/.fad-checker/` caches and never transmits a
|
|
81
89
|
> dependency, path or finding off the machine. It is regression-tested
|
|
82
90
|
> (`test/offline-guarantee.test.js`, a tripwire fetcher that throws if touched) and
|
|
83
91
|
> auditor-reproducible: `unshare -rn node fad-checker.js -s ./proj --offline …` runs it in a
|
|
84
92
|
> namespace with **no network interface** and yields byte-identical findings. Unlike the
|
|
85
|
-
> mainstream OSS scanners, fad also resolves the **Maven transitive graph** offline
|
|
93
|
+
> mainstream OSS scanners, fad also resolves the **Maven transitive graph** offline; so on
|
|
86
94
|
> an air-gapped multi-module project it finds the transitive CVEs they can't.
|
|
87
95
|
|
|
88
96
|
When the audited system is **offline / confidential** (typical of a regulated or air-gapped audit) it
|
|
89
97
|
can't reach OSV / NVD / Maven Central / npm. Split the work across machines while keeping
|
|
90
98
|
**zero environment information** off the secure enclave: an anonymized descriptor carries
|
|
91
|
-
only **public package coordinates
|
|
92
|
-
hostnames/usernames
|
|
99
|
+
only **public package coordinates**; no filesystem paths, no registry URLs, no
|
|
100
|
+
hostnames/usernames; and the **detailed report is produced back on the offline machine**.
|
|
93
101
|
|
|
94
102
|
The transfer relies on a property of fad-checker's caches: they are keyed by *coordinate*
|
|
95
103
|
or *vuln id*, never by path, so they are **machine-independent**. The online step just
|
|
96
104
|
**warms the caches**; the offline step replays the scan and gets cache hits.
|
|
97
105
|
|
|
98
106
|
```bash
|
|
99
|
-
# ── Phase 1
|
|
107
|
+
# ── Phase 1; OFFLINE (audited machine): export the anonymized descriptor ──
|
|
100
108
|
# Exclude private/internal packages with -e (offline we can't tell private from public).
|
|
101
109
|
fad-checker -s ./proj -e "^(client|internal)\." --export-anonymized deps.json
|
|
102
110
|
# → deps.json: public coordinates only. Review it before it leaves the enclave.
|
|
103
111
|
|
|
104
|
-
# ── Phase 2
|
|
112
|
+
# ── Phase 2; ONLINE (any machine, no source needed): warm the caches ──
|
|
105
113
|
fad-checker --import-anonymized deps.json # scans coordinates → OSV/NVD/CVE/registry/EOL + retire signatures
|
|
106
114
|
fad-checker --export-cache fad-cache.tar.gz # bundle the warmed ~/.fad-checker/
|
|
107
115
|
|
|
108
|
-
# ── Phase 3
|
|
116
|
+
# ── Phase 3; OFFLINE (audited machine): full report, all local context ──
|
|
109
117
|
fad-checker --import-cache fad-cache.tar.gz
|
|
110
118
|
fad-checker -s ./proj --offline # re-collect locally (real paths) + cache hits
|
|
111
119
|
# → full HTML/.doc report with manifests & structure, generated inside the enclave.
|
|
@@ -121,18 +129,36 @@ What the descriptor (`fad-deps/1`) contains vs. drops:
|
|
|
121
129
|
| scope, isDev | parent chains, lockfile type |
|
|
122
130
|
|
|
123
131
|
The online phase report is itself path-free; vendored-JavaScript (retire.js) findings are
|
|
124
|
-
produced **offline in phase 3**, since retire needs the actual `.js` files
|
|
132
|
+
produced **offline in phase 3**, since retire needs the actual `.js` files; its signature
|
|
125
133
|
DB is warmed online (phase 2) and carried by `--export-cache`. Full offline/cache control →
|
|
126
134
|
[`docs/USAGE.md`](docs/USAGE.md).
|
|
127
135
|
|
|
128
136
|
## Docs
|
|
129
137
|
|
|
130
|
-
- [`docs/USAGE.md`](docs/USAGE.md)
|
|
131
|
-
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
|
132
|
-
- [`docs/COMPARISON.md`](docs/COMPARISON.md)
|
|
133
|
-
- [`docs/
|
|
134
|
-
- [`
|
|
138
|
+
- [`docs/USAGE.md`](docs/USAGE.md); every flag and workflow: offline/cache control, private registries, config files, recipes, safety rails.
|
|
139
|
+
- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); internals: codecs, collection, matching, report pipeline.
|
|
140
|
+
- [`docs/COMPARISON.md`](docs/COMPARISON.md); vs OSV-Scanner / Trivy / Grype / OWASP DC / Snyk, and how it stays build-free.
|
|
141
|
+
- [`docs/BENCHMARK.md`](docs/BENCHMARK.md) — reproducible air-gapped recall benchmark vs OSV-Scanner on a public 105-module project.
|
|
142
|
+
- [`docs/DATA-SOURCES.md`](docs/DATA-SOURCES.md); the public datasets fad-checker uses + their licenses.
|
|
143
|
+
- [`CHANGELOG.md`](CHANGELOG.md) · [`CLAUDE.md`](CLAUDE.md); release history · code-level orientation for contributors.
|
|
144
|
+
|
|
145
|
+
## Contributing
|
|
146
|
+
|
|
147
|
+
The most useful contribution to a young scanner is **telling it where it's wrong**: run it on a
|
|
148
|
+
real project and file a [false positive / false negative report](https://github.com/9pings/fad-checker/issues/new?template=false_positive.yml)
|
|
149
|
+
with the coordinate and the manifest snippet that produced it. Dev setup, ground rules and the
|
|
150
|
+
codec extension point → [`CONTRIBUTING.md`](CONTRIBUTING.md). Vulnerabilities in fad-checker
|
|
151
|
+
itself → [`SECURITY.md`](SECURITY.md) (please report privately).
|
|
152
|
+
|
|
153
|
+
**On AI assistance:** this codebase is written with heavy use of Claude Code; [`CLAUDE.md`](CLAUDE.md)
|
|
154
|
+
in the repo root is exactly what it looks like. The bar it's held to is the one you can check
|
|
155
|
+
yourself: **606 tests** (`npm test`), the zero-network guarantee enforced by a tripwire test and
|
|
156
|
+
reproducible under `unshare -rn`, and coverage numbers measured against a Snyk baseline rather
|
|
157
|
+
than asserted. `fad-checker` itself uses **no LLM at runtime**; findings come from public
|
|
158
|
+
vulnerability databases and deterministic parsers, and no report text is generated. Full
|
|
159
|
+
statement, including where review actually caught a bad finding →
|
|
160
|
+
[`AI_POLICY.md`](AI_POLICY.md). Where the code doesn't meet that bar, that's a bug report I want.
|
|
135
161
|
|
|
136
162
|
## License
|
|
137
163
|
|
|
138
|
-
MIT
|
|
164
|
+
MIT; see [`LICENSE`](LICENSE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Report vulnerabilities in `fad-checker` itself **privately**, via
|
|
6
|
+
[GitHub private security advisories](https://github.com/9pings/fad-checker/security/advisories/new).
|
|
7
|
+
If that isn't available to you, email `pp9Ping@gmail.com` with `fad-checker security` in the
|
|
8
|
+
subject.
|
|
9
|
+
|
|
10
|
+
Please don't open a public issue for a vulnerability. This is a single-maintainer project —
|
|
11
|
+
expect a first response within **7 days**, and a fix or a plan within **30 days** for anything
|
|
12
|
+
confirmed. Credit is given in the release notes unless you'd rather stay anonymous.
|
|
13
|
+
|
|
14
|
+
Findings *about your own dependencies* that fad-checker reported are not vulnerabilities in
|
|
15
|
+
this project — those belong in a normal issue (the
|
|
16
|
+
[false positive / false negative](https://github.com/9pings/fad-checker/issues/new?template=false_positive.yml)
|
|
17
|
+
template).
|
|
18
|
+
|
|
19
|
+
## Supported versions
|
|
20
|
+
|
|
21
|
+
Only the latest released version on npm is supported. There are no backport branches.
|
|
22
|
+
|
|
23
|
+
## Threat model
|
|
24
|
+
|
|
25
|
+
fad-checker parses **untrusted input**: manifests, lockfiles, archives and certificates from a
|
|
26
|
+
source tree you may not control. The parsing surface is where security bugs are expected, and
|
|
27
|
+
reports there are especially welcome. In particular:
|
|
28
|
+
|
|
29
|
+
- **Archive extraction** — `.jar`/`.war`/`.ear` are unzipped **in memory** (`fflate`), never to
|
|
30
|
+
disk, and nested archives are recursed to a bounded depth. Path traversal (zip-slip) has no
|
|
31
|
+
filesystem to reach, but resource exhaustion (zip bombs) is a valid report.
|
|
32
|
+
- **Certificate and key parsing** — X.509 goes through Node's built-in `crypto.X509Certificate`.
|
|
33
|
+
Key material is **classified and hashed, never decrypted**, and its contents are never written
|
|
34
|
+
to the report.
|
|
35
|
+
- **XML / YAML / TOML parsing** — `pom.xml` (xml2js), lockfiles (js-yaml, smol-toml). XXE and
|
|
36
|
+
entity-expansion reports are in scope.
|
|
37
|
+
- **Command execution** — the only subprocesses are `snyk` (opt-in `--snyk`), `retire`, and
|
|
38
|
+
`curl`/`unzip` for the CVE bundle download.
|
|
39
|
+
|
|
40
|
+
## What leaves your machine
|
|
41
|
+
|
|
42
|
+
Under `--offline`, **nothing**: no network call is made at all, only the warmed
|
|
43
|
+
`~/.fad-checker/` caches are read. This is enforced by a regression test
|
|
44
|
+
(`test/offline-guarantee.test.js`, a tripwire fetcher that throws if touched) and is
|
|
45
|
+
reproducible in a network-free namespace:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
unshare -rn node fad-checker.js -s ./proj --offline
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Online, fad-checker sends **package coordinates and versions** to the data sources listed in
|
|
52
|
+
[`docs/DATA-SOURCES.md`](docs/DATA-SOURCES.md) (OSV.dev, NVD, Maven Central, npm, Packagist,
|
|
53
|
+
PyPI, NuGet, RubyGems, proxy.golang.org, endoflife.date, FIRST.org EPSS, CISA KEV, deps.dev,
|
|
54
|
+
CIRCL hashlookup) — plus **file checksums** for committed native binaries. It never uploads
|
|
55
|
+
source code, file paths or manifest contents. If you consider your dependency list itself
|
|
56
|
+
sensitive, use the anonymized air-gapped workflow described in the README.
|
|
57
|
+
|
|
58
|
+
Configuration, including your NVD API key, lives in `~/.fad-checker/config.json` with mode
|
|
59
|
+
`0600`. Registry credentials given via `--add-repo --auth`/`--token` are stored there too —
|
|
60
|
+
they are sent only to the registry they belong to, and never appear in a report or export.
|