fad-checker 2.1.2 → 2.2.1
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 +37 -0
- package/README.md +75 -87
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +1 -0
- package/fad-checker.js +211 -49
- package/lib/codecs/binary/scan.js +71 -0
- package/lib/codecs/binary/sniff.js +37 -0
- package/lib/codecs/binary.codec.js +54 -0
- package/lib/codecs/composer.codec.js +7 -3
- package/lib/codecs/go/registry.js +19 -11
- package/lib/codecs/go.codec.js +7 -3
- package/lib/codecs/index.js +9 -4
- package/lib/codecs/maven/jar-scan.js +8 -3
- package/lib/codecs/maven.codec.js +4 -2
- package/lib/codecs/npm/parse.js +5 -2
- package/lib/codecs/npm/registry.js +29 -18
- package/lib/codecs/npm.codec.js +7 -5
- package/lib/codecs/nuget.codec.js +7 -3
- package/lib/codecs/pypi/registry.js +16 -10
- package/lib/codecs/pypi.codec.js +7 -3
- package/lib/codecs/recipes.js +9 -1
- package/lib/codecs/ruby/registry.js +16 -10
- package/lib/codecs/ruby.codec.js +7 -3
- package/lib/config.js +34 -23
- package/lib/core.js +9 -5
- package/lib/cve-match.js +3 -2
- package/lib/cve-report.js +121 -25
- package/lib/dep-record.js +12 -9
- package/lib/embedded.js +57 -0
- package/lib/hash-id.js +78 -0
- package/lib/json-export.js +15 -2
- package/lib/options-env.js +113 -0
- package/lib/outdated.js +19 -7
- package/lib/parallel-walk.js +5 -2
- package/lib/path-filter.js +58 -0
- package/lib/registries.js +112 -0
- package/lib/retire.js +158 -7
- package/lib/scan-completeness.js +22 -7
- package/lib/ui.js +4 -3
- package/lib/unmanaged.js +82 -0
- package/package.json +1 -1
package/lib/scan-completeness.js
CHANGED
|
@@ -40,22 +40,37 @@ function detectScanCompletenessWarnings(resolvedDeps, opts = {}) {
|
|
|
40
40
|
// BOMs themselves: a present-and-correct BOM is not a problem.
|
|
41
41
|
const unresolved = [];
|
|
42
42
|
for (const d of resolvedDeps.values()) {
|
|
43
|
-
|
|
43
|
+
// Maven-only: the unresolved-version case (external BOM / out-of-tree
|
|
44
|
+
// property) is specific to Maven. Other ecosystems pin via lockfile, and
|
|
45
|
+
// native binaries (provenance:"binary") are identified by checksum, not a
|
|
46
|
+
// version — they belong to the Unmanaged/vendored-binaries chapter, NOT
|
|
47
|
+
// here. Including them mislabelled e.g. libeay32.dll as a Maven dep.
|
|
48
|
+
if (d.ecosystem !== "maven") continue;
|
|
49
|
+
if (d.provenance === "binary") continue;
|
|
44
50
|
if (d.scope === "import") continue; // BOM-pointer entries themselves: not a dep
|
|
45
51
|
if (!d.version) unresolved.push({ ...d, reason: "no-version" });
|
|
46
52
|
else if (/\$\{[^}]+\}/.test(String(d.version))) unresolved.push({ ...d, reason: "unresolved-property" });
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
if (unresolved.length) {
|
|
50
|
-
// Dedupe by g:a (same coord may appear in several modules)
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
// Dedupe by g:a (same coord may appear in several modules), MERGING the
|
|
57
|
+
// manifest paths so the report can show WHERE each unresolved dep is declared.
|
|
58
|
+
// Items are objects ({ id, manifestPaths }) — renderWarningItems() renders the
|
|
59
|
+
// "defined in: <pom.xml>" line for that shape (same as the private-libs warning).
|
|
60
|
+
const byKey = new Map();
|
|
53
61
|
for (const d of unresolved) {
|
|
54
62
|
const k = `${d.groupId}:${d.artifactId}`;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
const paths = (d.manifestPaths && d.manifestPaths.length) ? d.manifestPaths
|
|
64
|
+
: (d.pomPaths && d.pomPaths.length) ? d.pomPaths
|
|
65
|
+
: [];
|
|
66
|
+
let entry = byKey.get(k);
|
|
67
|
+
if (!entry) {
|
|
68
|
+
entry = { id: `${k}${d.version ? " (" + d.version + ")" : " (no version resolved)"}`, manifestPaths: [] };
|
|
69
|
+
byKey.set(k, entry);
|
|
70
|
+
}
|
|
71
|
+
for (const p of paths) if (!entry.manifestPaths.includes(p)) entry.manifestPaths.push(p);
|
|
58
72
|
}
|
|
73
|
+
const items = [...byKey.values()];
|
|
59
74
|
warnings.push({
|
|
60
75
|
type: "unresolved-versions",
|
|
61
76
|
count: items.length,
|
package/lib/ui.js
CHANGED
|
@@ -17,11 +17,12 @@ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
|
|
|
17
17
|
const TITLE_A = "fad-checker";
|
|
18
18
|
const TITLE_B = "Autonomous Dependency Checker";
|
|
19
19
|
|
|
20
|
-
function banner() {
|
|
21
|
-
const
|
|
20
|
+
function banner(version) {
|
|
21
|
+
const ver = version ? `v${version}` : "";
|
|
22
|
+
const raw = `${TITLE_A} ${ver} · ${TITLE_B}`.replace(" ", " ");
|
|
22
23
|
const bar = "─".repeat(raw.length + 2);
|
|
23
24
|
console.log(chalk.cyan(`\n╭${bar}╮`));
|
|
24
|
-
console.log(chalk.cyan("│ ") + chalk.bold.white(TITLE_A) + chalk.cyan(" · ") + chalk.whiteBright(TITLE_B) + chalk.cyan(" │"));
|
|
25
|
+
console.log(chalk.cyan("│ ") + chalk.bold.white(TITLE_A) + (ver ? " " + chalk.dim(ver) : "") + chalk.cyan(" · ") + chalk.whiteBright(TITLE_B) + chalk.cyan(" │"));
|
|
25
26
|
console.log(chalk.cyan(`╰${bar}╯`));
|
|
26
27
|
}
|
|
27
28
|
|
package/lib/unmanaged.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/unmanaged.js — enrich unmanaged (hash-bearing) records with online identity
|
|
3
|
+
* + an integrity classification.
|
|
4
|
+
*
|
|
5
|
+
* integrity:
|
|
6
|
+
* "pristine" — deps.dev matched: file is byte-identical to a PUBLISHED package
|
|
7
|
+
* artifact (so it's unmodified, and ought to be a managed dep).
|
|
8
|
+
* "known-good" — CIRCL matched: a known OS/distro/CDN/NSRL file.
|
|
9
|
+
* "unknown" — no source recognises the hash (suspicious / vendored unknown).
|
|
10
|
+
*
|
|
11
|
+
* Records carrying a declared coordinate (embedded jars) gain a "modified" status in
|
|
12
|
+
* a later refinement; Plan 2 covers the hash-bearing binary records.
|
|
13
|
+
*
|
|
14
|
+
* @author: N.BRAUN
|
|
15
|
+
* @email: pp9ping@gmail.com
|
|
16
|
+
*/
|
|
17
|
+
const { lookupHash, loadCache, saveCache } = require("./hash-id");
|
|
18
|
+
|
|
19
|
+
async function enrichUnmanaged(resolved, opts = {}) {
|
|
20
|
+
const { fetcher, offline = false, cache, onProgress } = opts;
|
|
21
|
+
const targets = [...resolved.values()].filter(d => d.hashes && (d.hashes.sha1 || d.hashes.sha256));
|
|
22
|
+
const summary = { total: targets.length, identified: 0, pristine: 0, knownGood: 0, unknown: 0, malicious: 0 };
|
|
23
|
+
if (!targets.length) return summary;
|
|
24
|
+
const entries = cache || loadCache();
|
|
25
|
+
let done = 0;
|
|
26
|
+
for (const d of targets) {
|
|
27
|
+
const id = await lookupHash(d.hashes, { fetcher, offline, cache: entries });
|
|
28
|
+
d.identity = id || null;
|
|
29
|
+
if (!id) d.integrity = "unknown";
|
|
30
|
+
else if (id.source === "deps.dev") d.integrity = "pristine";
|
|
31
|
+
else d.integrity = "known-good";
|
|
32
|
+
if (id) summary.identified++;
|
|
33
|
+
if (d.integrity === "pristine") summary.pristine++;
|
|
34
|
+
else if (d.integrity === "known-good") summary.knownGood++;
|
|
35
|
+
else summary.unknown++;
|
|
36
|
+
if (id?.knownMalicious) summary.malicious++;
|
|
37
|
+
if (onProgress) onProgress(++done, targets.length);
|
|
38
|
+
}
|
|
39
|
+
if (!cache && !offline) saveCache(entries);
|
|
40
|
+
return summary;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normName(s) {
|
|
44
|
+
return String(s || "").toLowerCase()
|
|
45
|
+
.replace(/\.(dll|exe|so|dylib)(\.\d+)*$/, "") // drop binary extension (+ soname version)
|
|
46
|
+
.replace(/^lib/, "") // libssl → ssl
|
|
47
|
+
.replace(/[-_.]?\d[\d.]*$/, "") // trailing version
|
|
48
|
+
.replace(/[^a-z0-9]/g, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Lenient compare of a filename to an online identity name (last :/ segment). */
|
|
52
|
+
function nameMatches(declared, identityName) {
|
|
53
|
+
const a = normName(declared);
|
|
54
|
+
const b = normName(String(identityName).split(/[:/]/).pop());
|
|
55
|
+
if (!a || !b) return true; // can't compare → don't raise a false alarm
|
|
56
|
+
return a.includes(b) || b.includes(a);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Turn the unmanaged (hash-bearing) records into an inventory with derived signals. */
|
|
60
|
+
function buildInventory(resolved) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const d of resolved.values()) {
|
|
63
|
+
if (!d.hashes || !(d.provenance === "binary" || d.provenance === "embedded")) continue;
|
|
64
|
+
const identity = d.identity || null;
|
|
65
|
+
out.push({
|
|
66
|
+
path: d.manifestPaths?.[0] || d.declaredName || null,
|
|
67
|
+
declaredName: d.declaredName || d.name || null,
|
|
68
|
+
provenance: d.provenance,
|
|
69
|
+
hashes: d.hashes,
|
|
70
|
+
identity,
|
|
71
|
+
integrity: d.integrity || "unknown",
|
|
72
|
+
noOnlineInfo: !identity,
|
|
73
|
+
shouldBeManaged: !!(identity && identity.ecosystem),
|
|
74
|
+
nameMismatch: !!(identity && identity.name && !nameMatches(d.declaredName || d.name, identity.name)),
|
|
75
|
+
knownMalicious: !!(identity && identity.knownMalicious),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
out.sort((a, b) => String(a.path).localeCompare(String(b.path)));
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { enrichUnmanaged, buildInventory, nameMatches };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fad-checker",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Scan ALL Maven, npm, Yarn, Composer, Python, C#/.NET, Go & Ruby dependencies — plus embedded JARs (fat-jars/war/ear) — in a source tree ONE SHOT without mvn/python/etc — CVE (EPSS/KEV-prioritised), EOL, obsolete, outdated & licenses, with SBOM/CSAF/SARIF/JSON exports, CI gating and fix recos",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sca",
|