fad-checker 2.4.1 → 2.4.3
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/CHANGELOG.md +77 -0
- package/README.md +3 -2
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +3 -0
- package/data/cpe-coord-map.json +1 -0
- package/fad-checker.js +103 -13
- package/lib/certs/analyze.js +305 -0
- package/lib/certs/index.js +17 -0
- package/lib/certs/scan.js +67 -0
- package/lib/certs/sniff.js +29 -0
- package/lib/codecs/go/parse.js +50 -8
- package/lib/codecs/go/registry.js +3 -1
- package/lib/codecs/go.codec.js +22 -4
- package/lib/codecs/nuget/parse.js +21 -6
- package/lib/codecs/nuget/registry.js +30 -4
- package/lib/codecs/nuget.codec.js +37 -7
- package/lib/codecs/pypi/parse.js +11 -2
- package/lib/codecs/pypi/registry.js +3 -1
- package/lib/codecs/pypi.codec.js +3 -0
- package/lib/cve-download.js +102 -11
- package/lib/cve-report.js +61 -11
- package/lib/deps-descriptor.js +24 -0
- package/lib/json-export.js +4 -1
- package/lib/maven-bom.js +77 -3
- package/lib/maven-version.js +25 -16
- package/lib/osv.js +8 -4
- package/lib/provenance.js +2 -0
- package/lib/sarif-export.js +48 -2
- package/lib/transitive.js +20 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,69 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
### Fixed
|
|
9
|
+
- **External `<parent>` POMs (spring-boot-starter-parent) now backfill their managed
|
|
10
|
+
versions.** A versionless dep whose version is inherited from an external `<parent>`
|
|
11
|
+
(e.g. `spring-boot-starter-actuator` under `spring-boot-starter-parent`, whose own
|
|
12
|
+
parent `spring-boot-dependencies` holds the version table) was left unresolved and
|
|
13
|
+
**dropped from the CVE/OSV/EOL/outdated scans** — the mainline backfill (`lib/maven-bom.js`)
|
|
14
|
+
only handled `<scope>import</scope>` BOMs, never the `<parent>` case, so this failed
|
|
15
|
+
even online (the `--transitive` overlay resolved the parent but couldn't backfill the
|
|
16
|
+
primary version). `collectExternalParents()` now feeds external parents through the same
|
|
17
|
+
`effectivePom` → `backfillVersions` path as import BOMs (import BOMs win on precedence),
|
|
18
|
+
stamped `versionSource={via:"parent",…}` → report "version managed by … (parent POM)".
|
|
19
|
+
It runs in the **mainline** flow (not just `--transitive`), so the warmed cache always
|
|
20
|
+
captures the parent POMs for **offline/air-gapped** reuse. **Child property overrides are
|
|
21
|
+
honored** (Maven semantics): a project that redefines `<log4j2.version>2.17.1</log4j2.version>`
|
|
22
|
+
to patch a CVE resolves the managed coord to `2.17.1`, not the framework default
|
|
23
|
+
(`collectPropertyOverrides()` → `effectivePom`'s new `propertyOverrides`; import-BOM-managed
|
|
24
|
+
coords are correctly left un-overridden). The "missing parent POM — potentially private"
|
|
25
|
+
warning now **partitions** parents fad resolves from Maven Central/cache (public) from the
|
|
26
|
+
truly-unresolvable ones (private), instead of flagging every external parent as suspect.
|
|
27
|
+
- **Anonymized descriptor closes the versionless-Spring-Boot round-trip in one exchange.**
|
|
28
|
+
The `fad-deps/1` descriptor now carries a `maven` hints block (`externalParents[]` +
|
|
29
|
+
`importBoms[]` coords + version `propertyOverrides{}`) so a no-source-tree online
|
|
30
|
+
`--import-anonymized` run can resolve the versionless deps' versions and warm their
|
|
31
|
+
`coord+version`-keyed CVE caches — previously that needed a second air-gapped exchange.
|
|
32
|
+
Only public coords + version strings travel (a private parent listed is a harmless online
|
|
33
|
+
no-op); the source tree never leaves the enclave.
|
|
34
|
+
- **CVE-index recall: real-world pre-2023 records were dropped (incl. Log4Shell).**
|
|
35
|
+
`isMavenRelevant()` only accepted CVE 5.x records carrying machine-readable Maven
|
|
36
|
+
metadata (`packageName`/`collectionURL`/`versionType:"maven"`) or an EXACT-match
|
|
37
|
+
known vendor — but CNAs publish legal-entity vendors ("Apache Software Foundation")
|
|
38
|
+
and display products ("Apache Log4j2"), so **CVE-2021-44228 was absent from the
|
|
39
|
+
index** and an offline scan of `log4j-core:2.14.0` reported no Log4Shell. The filter
|
|
40
|
+
now tokenises vendors, strips leading vendor words from products, and consults the
|
|
41
|
+
curated `data/cpe-coord-map.json` — which also **backfills `packageName`** on
|
|
42
|
+
product-only records so they match at tier-1. `versions[].changes[]` timelines
|
|
43
|
+
(how 44228 encodes its affected windows) are expanded into plain affected windows,
|
|
44
|
+
placeholder bounds (`lessThan:"log4j-core*"`, `"*"`, `"unspecified"`) no longer
|
|
45
|
+
poison comparisons (fail-closed preserved), and `fixVersion` picks the **highest**
|
|
46
|
+
version-like upper bound instead of the first (multi-branch advisories suggested a
|
|
47
|
+
downgrade). Rebuilt index: **6 589 → 15 236 CVE (+131 %)**. Regression-tested against
|
|
48
|
+
the real cvelistV5 record (`test/fixtures/cve-samples/cve-2021-44228-real.json`).
|
|
49
|
+
- **OSV offline: warmed cache older than the 12 h TTL was silently discarded.**
|
|
50
|
+
`--offline` now bypasses the OSV cache TTL (same rule as the NVD cache): on an
|
|
51
|
+
air-gapped box the warmed cache is the only source, and expiring it reported
|
|
52
|
+
"0 OSV vulns" for every ecosystem. Online behaviour is unchanged.
|
|
53
|
+
- **Go: `replace` directives are now applied** (module→module replaces rewrite the
|
|
54
|
+
scanned coordinate — a `replace` downgrade was invisible; directory replaces are
|
|
55
|
+
dropped with a chapter-0 `local-replace` warning), and **pre-1.17 modules merge the
|
|
56
|
+
`go.sum` graph** (their `go.mod` lists direct deps only — transitives were skipped).
|
|
57
|
+
- **PyPI: `pip-compile --generate-hashes` output parsed correctly.** The trailing
|
|
58
|
+
`\` of hash-pinned lines (and inline ` --hash=…` options) made every dep of such a
|
|
59
|
+
requirements file silently skipped. `uv.lock` no longer inventories the project's
|
|
60
|
+
own virtual/editable package. Same-name deps pinned to different versions across
|
|
61
|
+
files now ALL land in `versions[]` (every distinct version scanned, as Maven does).
|
|
62
|
+
- **NuGet: `Directory.Packages.props` is resolved by walking UP the tree** (MSBuild
|
|
63
|
+
semantics — nearest wins). Before, only the csproj's own directory was searched, so
|
|
64
|
+
a root-level CPM solution collected **zero** deps. Also: `VersionOverride` support,
|
|
65
|
+
exact-range pins `[1.2.3]` accepted as concrete, distinct resolved versions across
|
|
66
|
+
projects/TFMs all scanned, and **paged registration indexes** (Newtonsoft.Json-class
|
|
67
|
+
packages) have the needed pages fetched instead of returning empty findings.
|
|
68
|
+
- **Registry caches (go/pypi/nuget): offline runs no longer re-stamp cache freshness**
|
|
69
|
+
(a stale cache would then look fresh to the next online run and skip its refetch).
|
|
70
|
+
|
|
8
71
|
### Changed
|
|
9
72
|
- **Report chapters reorganised into a two-level hierarchy.** Related chapters are now
|
|
10
73
|
grouped under six **root chapters**, each whose header carries a breakdown count:
|
|
@@ -18,6 +81,20 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
18
81
|
(roots + indented sub-chapters).
|
|
19
82
|
|
|
20
83
|
### Added
|
|
84
|
+
- **Certificate & key-material scanner (report chapter 2.4).** A new standalone scanner
|
|
85
|
+
(`lib/certs/`, on by default, `--no-certs` to disable) walks the source tree for
|
|
86
|
+
committed cryptographic material and surfaces it in a dedicated report chapter, the
|
|
87
|
+
JSON export (`certificates` array + `summary.certificates`/`certPrivateKeys`) and SARIF
|
|
88
|
+
(`FAD-*` rules). It detects **X.509 certificates** (PEM/DER, parsed with Node's built-in
|
|
89
|
+
`crypto.X509Certificate`) and flags **expired**, **expiring** (within `--cert-expiry-days`,
|
|
90
|
+
default 90), **weak key** (RSA<2048 / weak EC curve), **weak signature** (MD5/SHA1) and
|
|
91
|
+
**self-signed**; **private & public keys** — every key explicitly labelled **private**
|
|
92
|
+
(a committed secret → critical) or **public** (low) — across PEM (PKCS#1/8/SEC1),
|
|
93
|
+
**OpenSSH of every algorithm** (RSA/DSA/ECDSA/Ed25519 incl. FIDO `-sk`), PuTTY `.ppk`,
|
|
94
|
+
PGP and one-line SSH (`*.pub`, `authorized_keys`, `known_hosts`); and **keystores**
|
|
95
|
+
(JKS/JCEKS by magic byte, PKCS#12 by extension). Detection is by extension **and**
|
|
96
|
+
conventional SSH filename. **100% offline** — no network, no decryption — inventory-only
|
|
97
|
+
(does not affect the `--fail-on` gate).
|
|
21
98
|
- **Scan-provenance manifest + Methodology chapter (audit reproducibility).** Every
|
|
22
99
|
report now carries a provenance manifest — tool version, run mode (offline/online),
|
|
23
100
|
the findings-affecting configuration, and the **freshness of every data source**
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
> **F**abulous **A**utonomous **D**ependency **C**hecker<br>
|
|
9
9
|
> AKA **F**uckin' **A**utonomous **D**ependency **C**hecker<br>
|
|
10
10
|
|
|
11
|
-
`fad-checker` audits **Maven · Gradle · npm · Yarn · pnpm · Composer · PyPI · NuGet · Go · Ruby**, vendored JavaScript
|
|
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 — 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
12
|
|
|
13
13
|
🌐 **[Project site & docs →](https://9pings.github.io/fad-checker/)**
|
|
14
14
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
22
|
- **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).
|
|
23
|
+
- **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.
|
|
23
24
|
- **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)
|
|
24
25
|
- **CVE, merged & prioritised** — CVEProject + OSV.dev + NVD, CPE/version cross-checked to cut false positives, ranked **CISA KEV → EPSS → CVSS**.
|
|
25
26
|
- **Per-module Maven version mediation** — recovers vulnerable transitive versions that a global `<dependencyManagement>` pin hides in another module (lifted Snyk-corroborated coverage **156 → 181** on a real 25-module reactor, finding CVEs a single Snyk scan missed).
|
|
@@ -62,7 +63,7 @@ The report is organised into **root chapters** (each grouping related sub-chapte
|
|
|
62
63
|
| **0. Warnings** *(top)* | local heuristics | Missing lockfiles, unresolved Maven versions (BOM-managed), private libs not on Maven Central |
|
|
63
64
|
| **Δ. 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 |
|
|
64
65
|
| **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) |
|
|
65
|
-
| **2. Unmanaged / unversioned components** | deps.dev + CIRCL (by checksum), retire.js | **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* |
|
|
66
|
+
| **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 |
|
|
66
67
|
| **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) |
|
|
67
68
|
| **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 |
|
|
68
69
|
| **5. Fix Recommendations** | computed | Per-ecosystem pin recipes: Maven `<dependencyManagement>`, Gradle `constraints { }`, npm `overrides`, yarn `resolutions`, `composer require`, `pip install`, `dotnet add package` |
|
|
@@ -6,7 +6,7 @@ _fad_check_complete() {
|
|
|
6
6
|
COMPREPLY=()
|
|
7
7
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
8
8
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
9
|
-
opts="--src --target --exclude --verbose --no-report --no-transitive --no-all-libs --no-osv --no-nvd --no-epss --no-kev --licenses --no-retire --retire-refresh --report-output --report-html --report-doc --report-sbom --report-csaf --report-json --report-sarif --fail-on --fail-on-new --baseline --no-checksums --ignore --vex --ignore-test --cve-refresh --cve-offline --snyk --transitive-depth --offline --ecosystem --no-maven --no-npm --no-yarn --no-nuget --no-composer --no-pypi --no-go --no-ruby --no-jars --no-js --set-nvd-key --show-config --completion --help --version -s -t -e -v"
|
|
9
|
+
opts="--src --target --exclude --verbose --no-report --no-transitive --no-all-libs --no-osv --no-nvd --no-epss --no-kev --licenses --no-retire --retire-refresh --report-output --report-html --report-doc --report-sbom --report-csaf --report-json --report-sarif --fail-on --fail-on-new --baseline --no-checksums --ignore --vex --ignore-test --cve-refresh --cve-offline --snyk --transitive-depth --offline --ecosystem --no-maven --no-npm --no-yarn --no-nuget --no-composer --no-pypi --no-go --no-ruby --no-jars --no-binaries --no-certs --cert-expiry-days --no-js --set-nvd-key --show-config --completion --help --version -s -t -e -v"
|
|
10
10
|
case "$prev" in
|
|
11
11
|
--src|-s|--target|-t|--report-output)
|
|
12
12
|
COMPREPLY=( $(compgen -d -- "$cur") )
|
|
@@ -24,6 +24,9 @@ _fad_check() {
|
|
|
24
24
|
'--report-sarif[write SARIF 2.1.0 log]::file:_files'
|
|
25
25
|
'--no-report[write no output files (gate-only)]'
|
|
26
26
|
'--no-jars[skip embedded .jar/.war/.ear scanning]'
|
|
27
|
+
'--no-binaries[skip committed native-binary scanning]'
|
|
28
|
+
'--no-certs[skip certificate / key-material scanning]'
|
|
29
|
+
'--cert-expiry-days[warn on certs expiring within N days (default 90)]:days:'
|
|
27
30
|
'--no-go[skip the Go codec]'
|
|
28
31
|
'--no-ruby[skip the Ruby codec]'
|
|
29
32
|
'--fail-on[CI gate level]:level:(none low medium high critical kev)'
|
package/data/cpe-coord-map.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
"byVendorProduct": {
|
|
5
5
|
"apache:log4j": ["log4j:log4j", "org.apache.logging.log4j:log4j-core", "org.apache.logging.log4j:log4j-api"],
|
|
6
|
+
"apache:log4j2": ["org.apache.logging.log4j:log4j-core"],
|
|
6
7
|
"apache:log4j-core": ["org.apache.logging.log4j:log4j-core"],
|
|
7
8
|
"apache:log4j-api": ["org.apache.logging.log4j:log4j-api"],
|
|
8
9
|
"apache:struts": ["org.apache.struts:struts2-core"],
|
package/fad-checker.js
CHANGED
|
@@ -301,6 +301,8 @@ program
|
|
|
301
301
|
.option("--no-ruby", "skip the Ruby (Bundler) codec")
|
|
302
302
|
.option("--no-binaries", "skip scanning committed native binaries (.dll/.exe/.so/.dylib)")
|
|
303
303
|
.option("--no-jars", "skip scanning embedded .jar/.war/.ear binaries for Maven coordinates")
|
|
304
|
+
.option("--no-certs", "skip scanning committed certificates, private/public keys (PEM/SSH/PuTTY/PGP) and keystores")
|
|
305
|
+
.option("--cert-expiry-days <n>", "warn on certificates expiring within N days", "90")
|
|
304
306
|
.option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
|
|
305
307
|
.option("--repo <eco=url...>", "extra registry as <ecosystem>=<url> (e.g. npm=https://npm.acme/) tried before the public one. Repeatable. Supports https://user:pass@host/.")
|
|
306
308
|
.option("--add-repo <eco>", "persist a registry: --add-repo <ecosystem> <name> <url> [--auth user:pass] [--token TOK]")
|
|
@@ -524,11 +526,24 @@ async function timedPhase(label, fn) {
|
|
|
524
526
|
let imported;
|
|
525
527
|
try { imported = deserializeDeps(descriptor); }
|
|
526
528
|
catch (e) { console.error(chalk.red(`❌ invalid descriptor: ${e.message}`)); process.exit(1); }
|
|
527
|
-
const { resolved, activeIds, runMaven, runNpm } = imported;
|
|
529
|
+
const { resolved, activeIds, runMaven, runNpm, externalParents = [], importBoms = [], propertyOverrides = {} } = imported;
|
|
528
530
|
ui.section("Anonymized descriptor");
|
|
529
531
|
ui.ok(`imported ${chalk.bold(resolved.size)} dep(s) across ${activeIds.join(", ") || "—"}`);
|
|
530
532
|
if (options.offline) ui.warn("--offline: caches won't warm; only useful to re-render from an already-warm cache");
|
|
531
533
|
if (!resolved.size) { ui.warn("descriptor has no dependencies — nothing to scan"); process.exit(0); }
|
|
534
|
+
// Replay the external-parent / import-BOM backfill from the descriptor's carried hints.
|
|
535
|
+
// Workflow B has no source tree here, so the mainline store-based backfill can't run —
|
|
536
|
+
// without this, versionless deps (spring-boot-starter-*) stay unresolved and their CVE
|
|
537
|
+
// caches never warm, forcing a SECOND air-gapped exchange. Online only (needs the POMs).
|
|
538
|
+
if (runMaven && !options.offline && (externalParents.length || importBoms.length)) {
|
|
539
|
+
const { resolveBomManagedVersions, backfillVersions } = require("./lib/maven-bom");
|
|
540
|
+
const base = { repos: mavenRepos, offline: options.offline, verbose, effCache: new Map() };
|
|
541
|
+
const mgmt = await resolveBomManagedVersions(importBoms, base);
|
|
542
|
+
const parentMgmt = await resolveBomManagedVersions(externalParents, { ...base, via: "parent", propertyOverrides });
|
|
543
|
+
for (const [k, v] of parentMgmt) if (!mgmt.has(k)) mgmt.set(k, v);
|
|
544
|
+
const filled = backfillVersions(resolved, mgmt);
|
|
545
|
+
if (filled) ui.ok(`backfilled ${chalk.bold(filled)} version(s) from ${externalParents.length} parent(s) + ${importBoms.length} BOM(s) in the descriptor`);
|
|
546
|
+
}
|
|
532
547
|
// Warm retire signatures (online) so --export-cache carries them for offline JS scanning.
|
|
533
548
|
if (runNpm && !options.offline && options.retire !== false) {
|
|
534
549
|
const { warmRetireSignatures } = require("./lib/retire");
|
|
@@ -623,8 +638,14 @@ async function timedPhase(label, fn) {
|
|
|
623
638
|
// --- Anonymized phase 1: export a descriptor and exit (no network, no report) ---
|
|
624
639
|
if (options.exportAnonymized) {
|
|
625
640
|
const { serializeDeps } = require("./lib/deps-descriptor");
|
|
641
|
+
const { collectExternalParents, collectImportBoms, collectPropertyOverrides } = require("./lib/maven-bom");
|
|
626
642
|
const pkgVersion = require("./package.json").version;
|
|
627
|
-
|
|
643
|
+
// Carry the Maven resolution hints so a no-source-tree online warm run (Phase 2) can
|
|
644
|
+
// resolve the versionless deps' versions and warm their CVE caches in ONE exchange.
|
|
645
|
+
const externalParents = mavenCtx?.store ? collectExternalParents(mavenCtx.store) : [];
|
|
646
|
+
const importBoms = mavenCtx?.propsByPom ? collectImportBoms(mavenCtx.propsByPom) : [];
|
|
647
|
+
const propertyOverrides = mavenCtx?.store ? collectPropertyOverrides(mavenCtx.store) : {};
|
|
648
|
+
const descriptor = serializeDeps(resolved, { generator: `fad-checker ${pkgVersion}`, externalParents, importBoms, propertyOverrides });
|
|
628
649
|
try { fs.writeFileSync(options.exportAnonymized, JSON.stringify(descriptor, null, 2) + "\n"); }
|
|
629
650
|
catch (e) { console.error(chalk.red(`❌ could not write --export-anonymized file: ${e.message}`)); process.exit(1); }
|
|
630
651
|
const ecoSummary = Object.entries(descriptor.summary.byEcosystem).map(([k, v]) => `${k}:${v}`).join(", ");
|
|
@@ -689,9 +710,29 @@ async function timedPhase(label, fn) {
|
|
|
689
710
|
return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
|
|
690
711
|
});
|
|
691
712
|
if (missingParents.length) {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
713
|
+
// A parent absent from the source tree is EXTERNAL, not necessarily private. fad
|
|
714
|
+
// resolves a PUBLIC one (spring-boot-starter-parent, …) from Maven Central / warmed
|
|
715
|
+
// cache and backfills its managed versions — so only classify as "likely private"
|
|
716
|
+
// the ones it genuinely can't resolve. effectivePom is cache-first + offline-aware,
|
|
717
|
+
// and warms the cache the BOM/parent backfill reuses later in the report flow.
|
|
718
|
+
const { effectivePom } = require("./lib/transitive");
|
|
719
|
+
const resolvable = [], unresolved = [];
|
|
720
|
+
await Promise.all(missingParents.map(async id => {
|
|
721
|
+
const [g, a, v] = id.split(":");
|
|
722
|
+
let eff = null;
|
|
723
|
+
if (g && a && v) { try { eff = await effectivePom(g, a, v, { repos: mavenRepos, offline: options.offline }); } catch { eff = null; } }
|
|
724
|
+
(eff && eff.depMgmt && eff.depMgmt.length ? resolvable : unresolved).push(id);
|
|
725
|
+
}));
|
|
726
|
+
if (resolvable.length) {
|
|
727
|
+
ui.ok(`${resolvable.length} external parent POM(s) resolved from Maven Central${options.offline ? " (cache)" : ""} — managed versions backfilled`);
|
|
728
|
+
for (const id of resolvable.slice(0, 10)) ui.info(chalk.green(id));
|
|
729
|
+
if (resolvable.length > 10) ui.info(chalk.dim(`…and ${resolvable.length - 10} more`));
|
|
730
|
+
}
|
|
731
|
+
if (unresolved.length) {
|
|
732
|
+
ui.warn(`${unresolved.length} parent POM(s) not resolvable — likely private${options.offline ? " (or not in the warmed cache)" : ""}; Snyk will fail on these:`);
|
|
733
|
+
for (const id of unresolved.slice(0, 10)) ui.info(chalk.yellow(id));
|
|
734
|
+
if (unresolved.length > 10) ui.info(chalk.dim(`…and ${unresolved.length - 10} more`));
|
|
735
|
+
}
|
|
695
736
|
} else {
|
|
696
737
|
ui.ok("no missing Maven parent POMs");
|
|
697
738
|
}
|
|
@@ -826,7 +867,14 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
826
867
|
.map(b => ({ groupId: b.group, artifactId: b.name, version: b.version }))
|
|
827
868
|
.filter(b => b.groupId && b.artifactId && b.version);
|
|
828
869
|
const allBoms = importBoms.concat(gradleBoms);
|
|
829
|
-
|
|
870
|
+
// External <parent> POMs (e.g. spring-boot-starter-parent) manage versionless declared
|
|
871
|
+
// deps via their own inherited depMgmt (spring-boot-dependencies). core.js only follows
|
|
872
|
+
// LOCAL parents, so feed the external ones through the SAME backfill as import BOMs. This
|
|
873
|
+
// runs in the MAINLINE flow (not just the --transitive overlay) so the warmed cache always
|
|
874
|
+
// captures the parent POMs for offline reuse.
|
|
875
|
+
const externalParents = (runMaven && mavenStore)
|
|
876
|
+
? require("./lib/maven-bom").collectExternalParents(mavenStore) : [];
|
|
877
|
+
const willBom = allBoms.length > 0 || externalParents.length > 0;
|
|
830
878
|
const willOsv = !!options.osv;
|
|
831
879
|
// Local OSV DB import (Maven): offline-complete OSV recall, independent of the per-dep
|
|
832
880
|
// OSV.dev cache. Opt-in (downloads ~9 MB once); then matches online or offline.
|
|
@@ -837,20 +885,35 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
837
885
|
const willKev = !!options.kev;
|
|
838
886
|
const willLicenses = !!options.licenses;
|
|
839
887
|
const willRetire = !!options.retire;
|
|
888
|
+
// Committed crypto material (certs / keys / keystores) — local file scan, no network.
|
|
889
|
+
const willCerts = options.certs !== false && !!options.src;
|
|
890
|
+
const certExpiryDays = parseInt(options.certExpiryDays, 10) || 90;
|
|
840
891
|
// Identify committed native binaries by checksum (deps.dev + CIRCL) when present.
|
|
841
892
|
const willBinaryId = [...resolved.values()].some(d => d.provenance === "binary");
|
|
842
893
|
// License detection piggybacks on the registry passes (same fetched metadata),
|
|
843
894
|
// so it adds no progress step of its own.
|
|
844
|
-
const totalSteps = [willBom, willTransitive, willOverlay, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willOsvDb, willNvd, willEpss, willKev, willRetire, willBinaryId].filter(Boolean).length;
|
|
895
|
+
const totalSteps = [willBom, willTransitive, willOverlay, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willOsvDb, willNvd, willEpss, willKev, willRetire, willCerts, willBinaryId].filter(Boolean).length;
|
|
845
896
|
const progress = new ui.Progress(totalSteps);
|
|
846
897
|
|
|
847
898
|
if (willBom) {
|
|
848
|
-
const st = progress.start("BOM version resolution (Maven Central)");
|
|
899
|
+
const st = progress.start("BOM / parent version resolution (Maven Central)");
|
|
849
900
|
try {
|
|
850
901
|
const { resolveBomManagedVersions, backfillVersions } = require("./lib/maven-bom");
|
|
851
|
-
const
|
|
902
|
+
const effCache = new Map();
|
|
903
|
+
const base = { repos: mavenRepos, offline, verbose, effCache };
|
|
904
|
+
// Import BOMs first: a local <scope>import</scope> declaration wins over the
|
|
905
|
+
// versions inherited from an external parent (Maven precedence). Import BOMs
|
|
906
|
+
// resolve in their own context — the project's property overrides do NOT reach
|
|
907
|
+
// them (Maven), so they get no propertyOverrides.
|
|
908
|
+
const mgmt = await resolveBomManagedVersions(allBoms, base);
|
|
909
|
+
// External parents second: fill only coords the BOMs didn't already manage, and
|
|
910
|
+
// honor the project's own <properties> overrides (e.g. a patched <log4j2.version>)
|
|
911
|
+
// so the version reflects the classpath, not the framework default.
|
|
912
|
+
const propertyOverrides = require("./lib/maven-bom").collectPropertyOverrides(mavenStore);
|
|
913
|
+
const parentMgmt = await resolveBomManagedVersions(externalParents, { ...base, via: "parent", propertyOverrides });
|
|
914
|
+
for (const [k, v] of parentMgmt) if (!mgmt.has(k)) mgmt.set(k, v);
|
|
852
915
|
const filled = backfillVersions(resolved, mgmt);
|
|
853
|
-
st.done(`${filled} dep version(s) from ${allBoms.length}
|
|
916
|
+
st.done(`${filled} dep version(s) from ${allBoms.length} BOM(s) + ${externalParents.length} parent(s)`);
|
|
854
917
|
} catch (err) { st.fail(err.message); }
|
|
855
918
|
}
|
|
856
919
|
|
|
@@ -1107,6 +1170,21 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1107
1170
|
}
|
|
1108
1171
|
}
|
|
1109
1172
|
|
|
1173
|
+
// 5b. Certificate / key-material scan — committed certs, private/public keys
|
|
1174
|
+
// (PEM, OpenSSH every algorithm, PuTTY, PGP, SSH one-liners) and keystores.
|
|
1175
|
+
// Pure local file walk: no network, so it runs the same online or offline.
|
|
1176
|
+
let certFindings = [];
|
|
1177
|
+
if (willCerts) {
|
|
1178
|
+
const st = progress.start("Certificates & keys");
|
|
1179
|
+
try {
|
|
1180
|
+
const { scanCertificates } = require("./lib/certs");
|
|
1181
|
+
certFindings = scanCertificates(options.src, { srcRoot: options.src, excludePath, defaultExcludes, expiryDays: certExpiryDays });
|
|
1182
|
+
const priv = certFindings.filter(c => c.kind === "private-key").length;
|
|
1183
|
+
const expired = certFindings.filter(c => c.issues.some(i => i.type === "cert-expired")).length;
|
|
1184
|
+
st.done(`${certFindings.length} item(s)${priv ? ` · ${priv} private key(s)` : ""}${expired ? ` · ${expired} expired` : ""}`);
|
|
1185
|
+
} catch (err) { st.fail(err.message); }
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1110
1188
|
// 6. Snyk (optional)
|
|
1111
1189
|
let snykMatches = [];
|
|
1112
1190
|
if (options.snyk) {
|
|
@@ -1235,6 +1313,18 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1235
1313
|
}
|
|
1236
1314
|
}
|
|
1237
1315
|
|
|
1316
|
+
if (certFindings.length) {
|
|
1317
|
+
const privN = certFindings.filter(c => c.kind === "private-key").length;
|
|
1318
|
+
const expiredN = certFindings.filter(c => c.issues.some(i => i.type === "cert-expired")).length;
|
|
1319
|
+
heading("Certificates & keys", certFindings.length, [privN ? chalk.red(`${privN} private key`) : null, expiredN ? chalk.yellow(`${expiredN} expired`) : null].filter(Boolean).join(" "));
|
|
1320
|
+
for (const c of certFindings.slice(0, 10)) {
|
|
1321
|
+
const vis = c.keyVisibility ? chalk.dim(`[${c.keyVisibility}]`) : "";
|
|
1322
|
+
const label = c.kind === "certificate" ? `${c.subject || "?"}` : `${c.algorithm || ""} ${c.kind}`.trim();
|
|
1323
|
+
console.log(" " + sev(c.severity.toUpperCase())((c.severity || "?").padEnd(8)) + " " + chalk.white(path.basename(String(c.path))) + " " + vis + " " + chalk.dim(label));
|
|
1324
|
+
}
|
|
1325
|
+
if (certFindings.length > 10) console.log(chalk.dim(` …and ${certFindings.length - 10} more (see report ch.2.4)`));
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1238
1328
|
heading("EOL frameworks", eolResults.length);
|
|
1239
1329
|
for (const e of eolResults.slice(0, 8)) console.log(" " + chalk.yellow(e.product.padEnd(18)) + " " + chalk.dim(`${coordOf(e.dep)}:${e.dep.version}`) + " " + chalk.dim(e.eol === true ? "EOL" : String(e.eol)) + definedInOf(e.dep));
|
|
1240
1330
|
if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
|
|
@@ -1379,7 +1469,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1379
1469
|
if (out.html || out.doc) {
|
|
1380
1470
|
await ensureDir(out.html); await ensureDir(out.doc);
|
|
1381
1471
|
const { htmlPath, docPath } = await writeReports({
|
|
1382
|
-
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches, vendoredJsInventory,
|
|
1472
|
+
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches, vendoredJsInventory, certFindings,
|
|
1383
1473
|
eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs,
|
|
1384
1474
|
resolvedDeps: resolved, projectInfo, warnings: reportWarnings, parsedManifests, diff,
|
|
1385
1475
|
htmlPath: out.html, docPath: out.doc,
|
|
@@ -1410,7 +1500,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1410
1500
|
try {
|
|
1411
1501
|
const { writeFindings } = require("./lib/json-export");
|
|
1412
1502
|
await ensureDir(out.json);
|
|
1413
|
-
writeFindings({ cveMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version, typosquats, diff: jsonDiff }, out.json);
|
|
1503
|
+
writeFindings({ cveMatches, retireMatches, vendoredJsInventory, certFindings, eolResults, obsoleteResults, outdatedResults, licenseResults, excludedDirs, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version, typosquats, diff: jsonDiff }, out.json);
|
|
1414
1504
|
wrote.push(["Findings JSON", out.json]);
|
|
1415
1505
|
} catch (err) { ui.warn(`JSON export failed: ${err.message}`); }
|
|
1416
1506
|
}
|
|
@@ -1418,7 +1508,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1418
1508
|
try {
|
|
1419
1509
|
const { writeSarif } = require("./lib/sarif-export");
|
|
1420
1510
|
await ensureDir(out.sarif);
|
|
1421
|
-
writeSarif(cveMatches.filter(m => !m.suppressed), out.sarif, { projectInfo, toolVersion: pkg.version });
|
|
1511
|
+
writeSarif(cveMatches.filter(m => !m.suppressed), out.sarif, { projectInfo, toolVersion: pkg.version, certFindings });
|
|
1422
1512
|
wrote.push(["SARIF", out.sarif]);
|
|
1423
1513
|
} catch (err) { ui.warn(`SARIF export failed: ${err.message}`); }
|
|
1424
1514
|
}
|