fad-checker 2.0.1 → 2.1.0
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 +50 -0
- package/README.md +90 -16
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +14 -1
- package/data/license-policy.json +97 -0
- package/fad-checker-report/cve-report.doc +630 -0
- package/fad-checker-report/cve-report.html +748 -0
- package/fad-checker.js +278 -53
- package/lib/cache-archive.js +3 -0
- package/lib/codecs/codec.interface.js +3 -0
- package/lib/codecs/composer/parse.js +3 -0
- package/lib/codecs/composer/registry.js +13 -5
- package/lib/codecs/composer.codec.js +3 -0
- package/lib/codecs/go/parse.js +65 -0
- package/lib/codecs/go/registry.js +74 -0
- package/lib/codecs/go.codec.js +76 -0
- package/lib/codecs/index.js +7 -2
- package/lib/codecs/maven/jar-scan.js +199 -0
- package/lib/codecs/maven.codec.js +17 -1
- package/lib/codecs/npm/collect.js +11 -0
- package/lib/codecs/npm/parse.js +3 -0
- package/lib/codecs/npm/registry.js +10 -2
- package/lib/codecs/npm.codec.js +3 -0
- package/lib/codecs/nuget/parse.js +3 -0
- package/lib/codecs/nuget/registry.js +11 -4
- package/lib/codecs/nuget.codec.js +3 -0
- package/lib/codecs/pypi/parse.js +7 -2
- package/lib/codecs/pypi/registry.js +24 -5
- package/lib/codecs/pypi.codec.js +3 -0
- package/lib/codecs/recipes.js +28 -1
- package/lib/codecs/ruby/parse.js +42 -0
- package/lib/codecs/ruby/registry.js +76 -0
- package/lib/codecs/ruby.codec.js +66 -0
- package/lib/codecs/select.js +3 -0
- package/lib/codecs/yarn.codec.js +3 -0
- package/lib/config.js +3 -0
- package/lib/core.js +3 -0
- package/lib/cpe.js +30 -5
- package/lib/csaf-export.js +159 -0
- package/lib/cve-download.js +3 -0
- package/lib/cve-match.js +12 -1
- package/lib/cve-report.js +157 -28
- package/lib/dep-record.js +15 -2
- package/lib/deps-descriptor.js +3 -0
- package/lib/epss.js +115 -0
- package/lib/gate.js +45 -0
- package/lib/json-export.js +110 -0
- package/lib/kev.js +88 -0
- package/lib/license-policy.js +110 -0
- package/lib/maven-license.js +52 -0
- package/lib/maven-repo.js +3 -0
- package/lib/maven-version.js +8 -3
- package/lib/nvd.js +13 -3
- package/lib/osv.js +68 -19
- package/lib/outdated.js +3 -0
- package/lib/priority.js +90 -0
- package/lib/purl.js +77 -0
- package/lib/retire.js +3 -0
- package/lib/sarif-export.js +134 -0
- package/lib/sbom-export.js +153 -0
- package/lib/scan-completeness.js +3 -0
- package/lib/snyk.js +3 -0
- package/lib/suppress.js +113 -0
- package/lib/transitive.js +3 -0
- package/lib/ui.js +3 -0
- package/package.json +48 -2
package/fad-checker.js
CHANGED
|
@@ -169,12 +169,27 @@ program
|
|
|
169
169
|
.option("-e, --exclude <exclude>", "regex of groupId to exclude, e.g. '^(client|private)\\.'")
|
|
170
170
|
.option("-v, --verbose", "verbose")
|
|
171
171
|
// Defaults: report + transitive + allLibs all ON. Use --no-* to disable.
|
|
172
|
-
.option("--no-report", "
|
|
172
|
+
.option("--no-report", "write NO output files at all — the scan, terminal summary and --fail-on gate still run (gate-only / CI mode)")
|
|
173
173
|
.option("--no-transitive", "skip transitive dependency resolution")
|
|
174
174
|
.option("--no-all-libs", "skip Maven Central queries (outdated check + missing-on-central check)")
|
|
175
175
|
.option("--no-osv", "skip OSV.dev (Google/GitHub aggregated Maven CVE feed)")
|
|
176
176
|
.option("--no-nvd", "skip NIST NVD enrichment of matched CVEs")
|
|
177
|
-
.option("--
|
|
177
|
+
.option("--no-epss", "skip EPSS (FIRST.org exploit-prediction) enrichment")
|
|
178
|
+
.option("--no-kev", "skip CISA KEV (known-exploited) enrichment")
|
|
179
|
+
// Output family: each --report-<type> takes an OPTIONAL path (omit → default name
|
|
180
|
+
// under --report-output). With NO --report-* flag at all, HTML + .doc are written
|
|
181
|
+
// by default. --no-report writes nothing (scan + gate only).
|
|
182
|
+
.option("--report-html [file]", "write the self-contained HTML report (default: <report-output>/cve-report.html)")
|
|
183
|
+
.option("--report-doc [file]", "write the Word-compatible .doc report (default: <report-output>/cve-report.doc)")
|
|
184
|
+
.option("--report-sbom [file]", "write a CycloneDX 1.6 SBOM, vulnerabilities inline (default: <report-output>/sbom.cdx.json)")
|
|
185
|
+
.option("--report-csaf [file]", "write a CSAF 2.0 VEX document (default: <report-output>/csaf-vex.json)")
|
|
186
|
+
.option("--report-json [file]", "write a flat machine-readable findings JSON (default: <report-output>/findings.json)")
|
|
187
|
+
.option("--report-sarif [file]", "write a SARIF 2.1.0 log for GitHub/GitLab code scanning (default: <report-output>/fad.sarif)")
|
|
188
|
+
.option("--fail-on <level>", "exit non-zero if a production finding meets <level>: low|medium|high|critical|kev|none", "none")
|
|
189
|
+
.option("--ignore <file>", "suppress findings listed in <file> (CVE ids / coords / globs, one per line)")
|
|
190
|
+
.option("--vex <file>", "ingest a CSAF VEX: suppress CVEs marked not_affected/fixed")
|
|
191
|
+
.option("--no-licenses", "skip license detection + copyleft policy check")
|
|
192
|
+
.option("--offline", "no network: use cached CVE/OSV/NVD/EPSS/KEV/POM data only")
|
|
178
193
|
.option("--set-nvd-key <key>", "save NVD API key to ~/.fad-checker/config.json (10× faster NVD enrichment)")
|
|
179
194
|
.option("--show-config", "print the persisted ~/.fad-checker/config.json")
|
|
180
195
|
.option("--export-cache <file>", "tar.gz/zip the ~/.fad-checker/ caches to <file> (excludes config.json by default)")
|
|
@@ -191,13 +206,16 @@ program
|
|
|
191
206
|
.option("--no-retire", "skip retire.js vendored-JS scan")
|
|
192
207
|
.option("--retire-refresh", "ignore retire cache and re-scan")
|
|
193
208
|
.option("--transitive-depth <n>", "max transitive depth", "6")
|
|
194
|
-
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi (default: auto = detected)", "auto")
|
|
209
|
+
.option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi,go,ruby (default: auto = detected)", "auto")
|
|
195
210
|
.option("--no-maven", "skip the Maven codec")
|
|
196
211
|
.option("--no-npm", "skip the npm codec")
|
|
197
212
|
.option("--no-yarn", "skip the Yarn codec")
|
|
198
213
|
.option("--no-nuget", "skip the NuGet (C#/.NET) codec")
|
|
199
214
|
.option("--no-composer", "skip the Composer (PHP) codec")
|
|
200
215
|
.option("--no-pypi", "skip the PyPI (Python) codec")
|
|
216
|
+
.option("--no-go", "skip the Go codec")
|
|
217
|
+
.option("--no-ruby", "skip the Ruby (Bundler) codec")
|
|
218
|
+
.option("--no-jars", "skip scanning embedded .jar/.war/.ear binaries for Maven coordinates")
|
|
201
219
|
.option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
|
|
202
220
|
.option("--repo <url...>", "extra Maven repository URL(s) to try before Maven Central. Supports https://user:pass@host/path/. Repeatable.")
|
|
203
221
|
.option("--add-repo <name>", "persist a Maven repo: --add-repo <name> <url> [--auth user:pass]")
|
|
@@ -209,6 +227,18 @@ program.parse(process.argv);
|
|
|
209
227
|
const options = program.opts();
|
|
210
228
|
const deps2Exclude = options.exclude ? new RegExp(options.exclude) : null;
|
|
211
229
|
const verbose = !!options.verbose;
|
|
230
|
+
|
|
231
|
+
// Validate --fail-on early: an unrecognised value (typo like "hgih", wrong case)
|
|
232
|
+
// must HARD-FAIL, never silently disable the CI gate.
|
|
233
|
+
if (options.failOn) {
|
|
234
|
+
const FAIL_ON_LEVELS = ["none", "low", "medium", "high", "critical", "kev"];
|
|
235
|
+
const lvl = String(options.failOn).toLowerCase();
|
|
236
|
+
if (!FAIL_ON_LEVELS.includes(lvl)) {
|
|
237
|
+
console.error(chalk.red(`❌ invalid --fail-on "${options.failOn}" — expected one of: ${FAIL_ON_LEVELS.join(", ")}`));
|
|
238
|
+
process.exit(2);
|
|
239
|
+
}
|
|
240
|
+
options.failOn = lvl;
|
|
241
|
+
}
|
|
212
242
|
// Read-only when no target is given. No need for an explicit --test flag.
|
|
213
243
|
const readOnly = !options.target;
|
|
214
244
|
|
|
@@ -223,10 +253,18 @@ if (options.src && options.importAnonymized) {
|
|
|
223
253
|
}
|
|
224
254
|
|
|
225
255
|
if (options.src && options.target) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
256
|
+
// --target is rimraf'd before being rewritten, so it must NOT overlap --src in
|
|
257
|
+
// EITHER direction: not the same dir, not a subdir of --src, and — the
|
|
258
|
+
// catastrophic case — not a PARENT of --src (which would delete the source tree
|
|
259
|
+
// and everything beside it).
|
|
260
|
+
const srcAbs = path.resolve(options.src);
|
|
261
|
+
const tgtAbs = path.resolve(options.target);
|
|
262
|
+
const relFromSrc = path.relative(srcAbs, tgtAbs); // target as seen from src
|
|
263
|
+
const relToSrc = path.relative(tgtAbs, srcAbs); // src as seen from target
|
|
264
|
+
const targetInsideSrc = !relFromSrc || (!relFromSrc.startsWith("..") && !path.isAbsolute(relFromSrc));
|
|
265
|
+
const srcInsideTarget = !relToSrc || (!relToSrc.startsWith("..") && !path.isAbsolute(relToSrc));
|
|
266
|
+
if (targetInsideSrc || srcInsideTarget) {
|
|
267
|
+
console.error(chalk.red("❌ --target must not overlap --src (it cannot be the same as, a subdirectory of, or a parent of --src) — it is deleted before being rewritten"));
|
|
230
268
|
process.exit(1);
|
|
231
269
|
}
|
|
232
270
|
}
|
|
@@ -295,7 +333,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
295
333
|
const { resolveActiveCodecs } = require("./lib/codecs/select");
|
|
296
334
|
const eco = (options.ecosystem || "auto").toLowerCase();
|
|
297
335
|
const detected = (eco === "auto") ? detectCodecs(options.src).map(c => c.id) : allCodecs().map(c => c.id);
|
|
298
|
-
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi"].filter(id => options[id] === false);
|
|
336
|
+
const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby"].filter(id => options[id] === false);
|
|
299
337
|
const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
|
|
300
338
|
const runMaven = activeIds.includes("maven");
|
|
301
339
|
const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
|
|
@@ -309,7 +347,7 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
309
347
|
const codec = getCodec(id);
|
|
310
348
|
let res;
|
|
311
349
|
try {
|
|
312
|
-
res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose });
|
|
350
|
+
res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose, scanJars: options.jars !== false, srcRoot: options.src });
|
|
313
351
|
} catch (err) {
|
|
314
352
|
console.warn(chalk.red(`❌ ${id} collect failed:`), chalk.dim(err.message));
|
|
315
353
|
continue;
|
|
@@ -322,13 +360,18 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
322
360
|
// --- Collection summary ---
|
|
323
361
|
ui.section("Collection");
|
|
324
362
|
const ecoCount = {};
|
|
325
|
-
|
|
363
|
+
let embeddedCount = 0;
|
|
364
|
+
for (const d of resolved.values()) {
|
|
365
|
+
if (d.provenance === "embedded") { embeddedCount++; continue; } // counted separately below
|
|
366
|
+
ecoCount[d.ecosystem] = (ecoCount[d.ecosystem] || 0) + 1;
|
|
367
|
+
}
|
|
326
368
|
if (runMaven) ui.ok(`${chalk.bold("Maven".padEnd(8))} ${mavenCtx ? mavenCtx.pomFiles.length + " module(s) · " : ""}${ecoCount.maven || 0} direct dep(s)`);
|
|
327
369
|
if (runNpm) ui.ok(`${chalk.bold("npm/yarn".padEnd(8))} ${ecoCount.npm || 0} dep(s)`);
|
|
328
370
|
for (const [id, n] of Object.entries(ecoCount)) {
|
|
329
371
|
if (id === "maven" || id === "npm") continue;
|
|
330
372
|
ui.ok(`${chalk.bold(((getCodec(id)?.label) || id).padEnd(8))} ${n} dep(s)`);
|
|
331
373
|
}
|
|
374
|
+
if (embeddedCount) ui.ok(`${chalk.bold("Embedded".padEnd(8))} ${embeddedCount} coord(s) in .jar/.war/.ear`);
|
|
332
375
|
if (!ecoCount.maven && !ecoCount.npm && !Object.keys(ecoCount).length) ui.warn("no dependencies found in the source tree");
|
|
333
376
|
if (collectWarnings.length) {
|
|
334
377
|
ui.warn(`${collectWarnings.length} manifest warning(s) — best-effort / no lockfile:`);
|
|
@@ -429,10 +472,12 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
|
|
|
429
472
|
else ui.info(chalk.dim(`${wrotePom} POM(s) cleanable (read-only — pass -t <dir> to write them)`));
|
|
430
473
|
}
|
|
431
474
|
|
|
432
|
-
// ----------
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
475
|
+
// ---------- Scan flow (CVE / EOL / Obsolete) ----------
|
|
476
|
+
// The scan always runs — it feeds the terminal summary, the file outputs and the
|
|
477
|
+
// CI gate. Which files get written is decided by the --report-* family inside
|
|
478
|
+
// (HTML + .doc by default; --no-report writes nothing).
|
|
479
|
+
await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
|
|
480
|
+
if (!readOnly) {
|
|
436
481
|
ui.section("Next step");
|
|
437
482
|
ui.info(`run Snyk on the cleaned tree:`);
|
|
438
483
|
console.log(" " + chalk.white(`cd ${options.target} && snyk test --json --all-projects | snyk-to-html -o ../snyk-deps-check.html`));
|
|
@@ -481,8 +526,13 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
481
526
|
const willOsv = !!options.osv;
|
|
482
527
|
const willOutdated = !!options.allLibs;
|
|
483
528
|
const willNvd = !!options.nvd;
|
|
529
|
+
const willEpss = !!options.epss;
|
|
530
|
+
const willKev = !!options.kev;
|
|
531
|
+
const willLicenses = !!options.licenses;
|
|
484
532
|
const willRetire = !!options.retire;
|
|
485
|
-
|
|
533
|
+
// License detection piggybacks on the registry passes (same fetched metadata),
|
|
534
|
+
// so it adds no progress step of its own.
|
|
535
|
+
const totalSteps = [willTransitive, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willNvd, willEpss, willKev, willRetire].filter(Boolean).length;
|
|
486
536
|
const progress = new ui.Progress(totalSteps);
|
|
487
537
|
|
|
488
538
|
if (willTransitive) {
|
|
@@ -520,6 +570,10 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
520
570
|
catch (err) { st.fail(err.message); }
|
|
521
571
|
}
|
|
522
572
|
|
|
573
|
+
// License findings accumulate from each registry pass (same fetched metadata)
|
|
574
|
+
// plus Maven's cached POMs — assessed against the copyleft policy below.
|
|
575
|
+
let licenseFindings = [];
|
|
576
|
+
|
|
523
577
|
// 3. Obsolete / deprecated — local curated list, instant (no network step).
|
|
524
578
|
let obsoleteResults = [];
|
|
525
579
|
try { obsoleteResults = outdated.checkObsoleteDeps(resolved); }
|
|
@@ -544,6 +598,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
544
598
|
const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
545
599
|
obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
|
|
546
600
|
outdatedResults = outdatedResults.concat(npmReg.outdated);
|
|
601
|
+
licenseFindings = licenseFindings.concat(npmReg.licensed || []);
|
|
547
602
|
st.done(`${npmReg.deprecated.length} deprecated, ${npmReg.outdated.length} outdated`);
|
|
548
603
|
} catch (err) { st.fail(err.message); }
|
|
549
604
|
}
|
|
@@ -556,6 +611,7 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
556
611
|
const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
|
|
557
612
|
obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
|
|
558
613
|
outdatedResults = outdatedResults.concat(reg.outdated || []);
|
|
614
|
+
licenseFindings = licenseFindings.concat(reg.licensed || []);
|
|
559
615
|
st.done(`${(reg.deprecated || []).length} deprecated, ${(reg.outdated || []).length} outdated`);
|
|
560
616
|
} catch (err) { st.fail(err.message); }
|
|
561
617
|
}
|
|
@@ -605,6 +661,40 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
605
661
|
}
|
|
606
662
|
}
|
|
607
663
|
|
|
664
|
+
// 4e. EPSS — exploit-prediction percentile for each matched CVE (FIRST.org).
|
|
665
|
+
if (willEpss) {
|
|
666
|
+
const st = progress.start("EPSS (FIRST.org)");
|
|
667
|
+
if (!cveMatches.length) { st.skip("no CVE"); }
|
|
668
|
+
else {
|
|
669
|
+
try {
|
|
670
|
+
const { enrichEpss } = require("./lib/epss");
|
|
671
|
+
await enrichEpss(cveMatches, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
|
|
672
|
+
const scored = cveMatches.filter(m => m.cve?.epssPercentile != null).length;
|
|
673
|
+
st.done(`${scored} scored`);
|
|
674
|
+
} catch (err) { st.fail(err.message); }
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 4f. CISA KEV — flag CVEs known to be exploited in the wild.
|
|
679
|
+
if (willKev) {
|
|
680
|
+
const st = progress.start("CISA KEV");
|
|
681
|
+
if (!cveMatches.length) { st.skip("no CVE"); }
|
|
682
|
+
else {
|
|
683
|
+
try {
|
|
684
|
+
const { enrichKev } = require("./lib/kev");
|
|
685
|
+
await enrichKev(cveMatches, { verbose, offline });
|
|
686
|
+
const kevd = cveMatches.filter(m => m.cve?.kev).length;
|
|
687
|
+
st.done(`${kevd} known-exploited`);
|
|
688
|
+
} catch (err) { st.fail(err.message); }
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 4g. Composite priority (KEV > EPSS-weighted CVSS). Always — cheap, pure.
|
|
693
|
+
try {
|
|
694
|
+
const { attachPriority } = require("./lib/priority");
|
|
695
|
+
attachPriority(cveMatches);
|
|
696
|
+
} catch (err) { ui.warn(`priority scoring skipped: ${err.message}`); }
|
|
697
|
+
|
|
608
698
|
// 5. retire.js — native "vendored" scanner contributed by the npm codec. Scans
|
|
609
699
|
// vendored JS files (jquery copies, bootstrap, pdf.js, …) that live in the
|
|
610
700
|
// source tree without any lockfile to back them.
|
|
@@ -649,10 +739,34 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
649
739
|
// full per-bucket list (including cpeFiltered) so the HTML report can render
|
|
650
740
|
// its "Likely false positives" appendix — only the CLI headline excludes
|
|
651
741
|
// cpeFiltered to avoid alarming on triaged-out matches.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
742
|
+
// Triage — suppress accepted-risk / false-positive findings (--ignore / --vex).
|
|
743
|
+
// Marked in place; kept in the machine exports (flagged) but dropped from the
|
|
744
|
+
// human report's active chapters and from CI gating.
|
|
745
|
+
let suppressedCount = 0;
|
|
746
|
+
if (options.ignore || options.vex) {
|
|
747
|
+
try {
|
|
748
|
+
const { parseIgnoreFile, parseVex, applySuppressions } = require("./lib/suppress");
|
|
749
|
+
const rules = [];
|
|
750
|
+
if (options.ignore) rules.push(...parseIgnoreFile(fs.readFileSync(options.ignore, "utf8")));
|
|
751
|
+
if (options.vex) rules.push(...parseVex(JSON.parse(fs.readFileSync(options.vex, "utf8"))));
|
|
752
|
+
suppressedCount = applySuppressions(cveMatches, rules);
|
|
753
|
+
const via = [options.ignore && "--ignore", options.vex && "--vex"].filter(Boolean).join(" + ");
|
|
754
|
+
if (suppressedCount) ui.info(chalk.dim(`triage: ${suppressedCount} finding(s) suppressed by ${via}`));
|
|
755
|
+
} catch (err) { ui.warn(`suppression skipped: ${err.message}`); }
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const { sortByPriority } = require("./lib/priority");
|
|
759
|
+
const isEmbedded = m => m.dep?.provenance === "embedded";
|
|
760
|
+
// Embedded-binary findings get their own chapter, so keep them out of the
|
|
761
|
+
// declared prod/dev sets (a coord that's both declared AND embedded yields two
|
|
762
|
+
// distinct records — one in each — which is the intended, audit-useful split).
|
|
763
|
+
const prodMatches = cveMatches.filter(m => !m.dep?.isDev && !m.suppressed && !isEmbedded(m));
|
|
764
|
+
const devMatches = cveMatches.filter(m => m.dep?.isDev && !m.suppressed && !isEmbedded(m));
|
|
765
|
+
const embeddedMatches = cveMatches.filter(m => isEmbedded(m) && !m.suppressed);
|
|
766
|
+
const prodActive = sortByPriority(prodMatches.filter(m => !m.cpeFiltered));
|
|
767
|
+
const devActive = sortByPriority(devMatches.filter(m => !m.cpeFiltered));
|
|
768
|
+
const embeddedActive = sortByPriority(embeddedMatches.filter(m => !m.cpeFiltered));
|
|
769
|
+
const kevCount = prodActive.filter(m => m.cve?.kev).length;
|
|
656
770
|
const cpeFilteredCount = (prodMatches.length - prodActive.length) + (devMatches.length - devActive.length);
|
|
657
771
|
|
|
658
772
|
const stats = computeStats(prodActive);
|
|
@@ -671,15 +785,26 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
671
785
|
|
|
672
786
|
ui.section("Results");
|
|
673
787
|
|
|
674
|
-
heading("CVE · production", prodActive.length, fmtStats(stats));
|
|
788
|
+
heading("CVE · production", prodActive.length, fmtStats(stats) + (kevCount ? " " + chalk.bgRed.white(` ${kevCount} KEV `) : ""));
|
|
675
789
|
for (const m of prodActive.slice(0, 12)) {
|
|
676
|
-
|
|
790
|
+
const epss = m.cve?.epssPercentile != null ? chalk.dim(` epss ${Math.round(m.cve.epssPercentile * 100)}%`) : "";
|
|
791
|
+
const kev = m.cve?.kev ? " " + chalk.bgRed.white(" KEV ") : "";
|
|
792
|
+
console.log(" " + sev(m.cve.severity)((m.cve.severity || "UNKNOWN").padEnd(8)) + " " + chalk.white(m.cve.id) + " " + chalk.dim(`${depLabel(m.dep)}:${m.dep.version}`) + epss + kev);
|
|
677
793
|
}
|
|
678
794
|
if (prodActive.length > 12) console.log(chalk.dim(` …and ${prodActive.length - 12} more (see report)`));
|
|
679
795
|
if (cpeFilteredCount) console.log(chalk.dim(` ${cpeFilteredCount} likely false positive(s) → report appendix`));
|
|
680
796
|
|
|
681
797
|
heading("CVE · dev", devActive.length, devActive.length ? fmtStats(devStats) : "");
|
|
682
798
|
|
|
799
|
+
if (embeddedActive.length) {
|
|
800
|
+
heading("CVE · embedded binaries", embeddedActive.length, fmtStats(computeStats(embeddedActive)));
|
|
801
|
+
for (const m of embeddedActive.slice(0, 8)) {
|
|
802
|
+
const top = (m.dep.manifestPaths?.[0] || "").split("!/")[0];
|
|
803
|
+
console.log(" " + sev(m.cve.severity)((m.cve.severity || "UNKNOWN").padEnd(8)) + " " + chalk.white(m.cve.id) + " " + chalk.dim(`${depLabel(m.dep)}:${m.dep.version}`) + chalk.dim(` ⊂ ${top}`));
|
|
804
|
+
}
|
|
805
|
+
if (embeddedActive.length > 8) console.log(chalk.dim(` …and ${embeddedActive.length - 8} more (see report ch.1B)`));
|
|
806
|
+
}
|
|
807
|
+
|
|
683
808
|
heading("EOL frameworks", eolResults.length);
|
|
684
809
|
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)));
|
|
685
810
|
if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
|
|
@@ -708,8 +833,27 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
708
833
|
}
|
|
709
834
|
}
|
|
710
835
|
|
|
836
|
+
// License assessment — Maven licenses come (network-free) from cached POMs;
|
|
837
|
+
// the registry passes already filled licenseFindings for the other ecosystems.
|
|
838
|
+
let licenseResults = null;
|
|
839
|
+
if (willLicenses) {
|
|
840
|
+
try {
|
|
841
|
+
if (runMaven) {
|
|
842
|
+
const { collectMavenLicenses } = require("./lib/maven-license");
|
|
843
|
+
licenseFindings = licenseFindings.concat(collectMavenLicenses(resolved));
|
|
844
|
+
}
|
|
845
|
+
const { assessLicenses } = require("./lib/license-policy");
|
|
846
|
+
licenseResults = assessLicenses(licenseFindings);
|
|
847
|
+
const flaggedN = licenseResults.flagged.length;
|
|
848
|
+
heading("Licenses", licenseResults.assessed.length, flaggedN ? chalk.yellow(`${flaggedN} to review`) : "");
|
|
849
|
+
for (const e of licenseResults.flagged.slice(0, 8)) {
|
|
850
|
+
console.log(" " + chalk.yellow((e.category).padEnd(16)) + " " + chalk.dim(`${coordOf(e.dep)}`) + " " + chalk.dim((e.ids.concat(e.raw)).join(", ") || "—"));
|
|
851
|
+
}
|
|
852
|
+
if (licenseResults.flagged.length > 8) console.log(chalk.dim(` …and ${licenseResults.flagged.length - 8} more`));
|
|
853
|
+
} catch (err) { ui.warn(`license assessment skipped: ${err.message}`); }
|
|
854
|
+
}
|
|
855
|
+
|
|
711
856
|
const reportDir = options.reportOutput || "./fad-checker-report";
|
|
712
|
-
await fs.promises.mkdir(reportDir, { recursive: true });
|
|
713
857
|
// --import-anonymized has no source tree; keep the report path-free (consistent
|
|
714
858
|
// with the anonymized descriptor it was fed).
|
|
715
859
|
const srcResolved = options.src ? path.resolve(options.src) : null;
|
|
@@ -720,37 +864,116 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
720
864
|
toolVersion: pkg.version,
|
|
721
865
|
cveDataDate,
|
|
722
866
|
};
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
],
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
867
|
+
|
|
868
|
+
// --- Output target resolution -------------------------------------------------
|
|
869
|
+
// One --report-<type> flag per output, each taking an OPTIONAL path: a string is
|
|
870
|
+
// an explicit path, `true` means "use the default name under --report-output",
|
|
871
|
+
// undefined means "not requested". If NO --report-* flag is given at all, fall
|
|
872
|
+
// back to the historical default set (HTML + .doc). --no-report suppresses ALL
|
|
873
|
+
// file outputs (the scan, terminal summary and --fail-on gate still ran).
|
|
874
|
+
const DEFAULT_NAMES = { html: "cve-report.html", doc: "cve-report.doc", sbom: "sbom.cdx.json", csaf: "csaf-vex.json", json: "findings.json", sarif: "fad.sarif" };
|
|
875
|
+
const sel = { html: options.reportHtml, doc: options.reportDoc, sbom: options.reportSbom, csaf: options.reportCsaf, json: options.reportJson, sarif: options.reportSarif };
|
|
876
|
+
const anySpecified = Object.values(sel).some(v => v !== undefined);
|
|
877
|
+
const resolveOut = key => {
|
|
878
|
+
const v = sel[key];
|
|
879
|
+
if (v === undefined) return (!anySpecified && (key === "html" || key === "doc")) ? path.join(reportDir, DEFAULT_NAMES[key]) : null;
|
|
880
|
+
return (v === true) ? path.join(reportDir, DEFAULT_NAMES[key]) : v;
|
|
881
|
+
};
|
|
882
|
+
const out = options.report === false
|
|
883
|
+
? { html: null, doc: null, sbom: null, csaf: null, json: null, sarif: null }
|
|
884
|
+
: { html: resolveOut("html"), doc: resolveOut("doc"), sbom: resolveOut("sbom"), csaf: resolveOut("csaf"), json: resolveOut("json"), sarif: resolveOut("sarif") };
|
|
885
|
+
const ensureDir = async p => { if (p) await fs.promises.mkdir(path.dirname(path.resolve(p)), { recursive: true }); };
|
|
886
|
+
|
|
887
|
+
const reportWarnings = [
|
|
888
|
+
...(suppressedCount ? [{
|
|
889
|
+
type: "suppressed",
|
|
890
|
+
count: suppressedCount,
|
|
891
|
+
message: `${suppressedCount} finding(s) suppressed via triage (--ignore/--vex) — excluded from the chapters above and from CI gating, but retained (flagged) in the JSON/SBOM/CSAF exports.`,
|
|
892
|
+
}] : []),
|
|
893
|
+
...npmWarnings,
|
|
894
|
+
...scanWarnings,
|
|
895
|
+
...(privateLibIds.length ? [{
|
|
896
|
+
type: "private-libs",
|
|
897
|
+
count: privateLibIds.length,
|
|
898
|
+
items: privateLibIds.map(id => {
|
|
899
|
+
const dep = resolved.get(id);
|
|
900
|
+
const paths = (dep?.pomPaths || []).map(p => path.relative(options.src, p));
|
|
901
|
+
return { id, manifestPaths: paths };
|
|
902
|
+
}),
|
|
903
|
+
message: `${privateLibIds.length} Maven coord(s) not found on Maven Central — they are private/internal libraries. Their CVEs (if any) cannot be detected by fad-checker; if you have an internal CVE feed, audit them separately.`,
|
|
904
|
+
}] : []),
|
|
905
|
+
];
|
|
906
|
+
|
|
907
|
+
const wrote = [];
|
|
908
|
+
if (out.html || out.doc) {
|
|
909
|
+
await ensureDir(out.html); await ensureDir(out.doc);
|
|
910
|
+
const { htmlPath, docPath } = await writeReports({
|
|
911
|
+
cveMatches: prodMatches, devCveMatches: devMatches, embeddedMatches, retireMatches,
|
|
912
|
+
eolResults, obsoleteResults, outdatedResults, licenseResults,
|
|
913
|
+
resolvedDeps: resolved, projectInfo, warnings: reportWarnings,
|
|
914
|
+
htmlPath: out.html, docPath: out.doc,
|
|
915
|
+
});
|
|
916
|
+
if (htmlPath) wrote.push(["HTML report", htmlPath]);
|
|
917
|
+
if (docPath) wrote.push(["Word .doc", docPath]);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Machine-readable exports. Use the full match set (prod + dev + cpe-filtered) so
|
|
921
|
+
// the artifacts are complete; cpeFiltered is marked as a property/flag, not dropped.
|
|
922
|
+
if (out.sbom) {
|
|
923
|
+
try {
|
|
924
|
+
const { writeCycloneDx } = require("./lib/sbom-export");
|
|
925
|
+
await ensureDir(out.sbom);
|
|
926
|
+
writeCycloneDx(resolved, cveMatches, out.sbom, { projectInfo, toolVersion: pkg.version, timestamp: projectInfo.generatedAt, licenseResults });
|
|
927
|
+
wrote.push(["CycloneDX SBOM", out.sbom]);
|
|
928
|
+
} catch (err) { ui.warn(`SBOM export failed: ${err.message}`); }
|
|
929
|
+
}
|
|
930
|
+
if (out.csaf) {
|
|
931
|
+
try {
|
|
932
|
+
const { writeCsaf } = require("./lib/csaf-export");
|
|
933
|
+
await ensureDir(out.csaf);
|
|
934
|
+
writeCsaf(resolved, cveMatches, out.csaf, { projectInfo, toolVersion: pkg.version, timestamp: projectInfo.generatedAt });
|
|
935
|
+
wrote.push(["CSAF 2.0 VEX", out.csaf]);
|
|
936
|
+
} catch (err) { ui.warn(`CSAF export failed: ${err.message}`); }
|
|
937
|
+
}
|
|
938
|
+
if (out.json) {
|
|
939
|
+
try {
|
|
940
|
+
const { writeFindings } = require("./lib/json-export");
|
|
941
|
+
await ensureDir(out.json);
|
|
942
|
+
writeFindings({ cveMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps: resolved, projectInfo, toolVersion: pkg.version }, out.json);
|
|
943
|
+
wrote.push(["Findings JSON", out.json]);
|
|
944
|
+
} catch (err) { ui.warn(`JSON export failed: ${err.message}`); }
|
|
945
|
+
}
|
|
946
|
+
if (out.sarif) {
|
|
947
|
+
try {
|
|
948
|
+
const { writeSarif } = require("./lib/sarif-export");
|
|
949
|
+
await ensureDir(out.sarif);
|
|
950
|
+
writeSarif(cveMatches.filter(m => !m.suppressed), out.sarif, { projectInfo, toolVersion: pkg.version });
|
|
951
|
+
wrote.push(["SARIF", out.sarif]);
|
|
952
|
+
} catch (err) { ui.warn(`SARIF export failed: ${err.message}`); }
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (wrote.length) {
|
|
956
|
+
ui.section("Output");
|
|
957
|
+
for (const [label, p] of wrote) ui.ok(`${label} → ${chalk.white(p)}`);
|
|
958
|
+
} else if (options.report === false) {
|
|
959
|
+
ui.info(chalk.dim("--no-report: no files written (scan + gate only)"));
|
|
960
|
+
}
|
|
753
961
|
console.log();
|
|
962
|
+
|
|
963
|
+
// CI gating — set a non-zero exit code (after all reports/exports are written)
|
|
964
|
+
// when a production finding meets the --fail-on threshold.
|
|
965
|
+
if (options.failOn && options.failOn !== "none") {
|
|
966
|
+
const { evaluateGate } = require("./lib/gate");
|
|
967
|
+
// Embedded-binary findings are real production risk → gate on them too.
|
|
968
|
+
const gate = evaluateGate([...prodActive, ...embeddedActive], options.failOn);
|
|
969
|
+
if (gate.failed) {
|
|
970
|
+
ui.section("Gate");
|
|
971
|
+
console.log(chalk.red(`✗ --fail-on ${options.failOn}: ${gate.reason}`));
|
|
972
|
+
process.exitCode = 1;
|
|
973
|
+
} else if (verbose) {
|
|
974
|
+
ui.info(chalk.dim(`--fail-on ${options.failOn}: no blocking finding`));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
754
977
|
}
|
|
755
978
|
|
|
756
979
|
/**
|
|
@@ -760,7 +983,9 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
760
983
|
*/
|
|
761
984
|
function mergeBySource(existing, additions) {
|
|
762
985
|
const byKey = new Map();
|
|
763
|
-
|
|
986
|
+
// coordKey keeps embedded-binary findings distinct from a same-g:a:v declared dep
|
|
987
|
+
// (see cve-match dedup). Falls back to g:a for any match lacking a coordKey.
|
|
988
|
+
const k = m => `${m.dep.coordKey || (m.dep.groupId + ":" + m.dep.artifactId)}:${m.dep.version}|${m.cve.id}`;
|
|
764
989
|
for (const m of existing || []) byKey.set(k(m), { ...m, source: m.source || "fad" });
|
|
765
990
|
for (const m of additions || []) {
|
|
766
991
|
const key = k(m);
|
package/lib/cache-archive.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pas de classe imposée : un codec est un objet litéral exportant ces clés.
|
|
5
5
|
* Voir docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md.
|
|
6
|
+
*
|
|
7
|
+
* @author: N.BRAUN
|
|
8
|
+
* @email: pp9ping@gmail.com
|
|
6
9
|
*/
|
|
7
10
|
const REQUIRED_KEYS = [
|
|
8
11
|
"id", "label", "osvEcosystem", "manifestNames",
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* → { package: { abandoned: bool|string, versions: { "<v>": {...} } } }
|
|
7
7
|
*
|
|
8
8
|
* Cached in ~/.fad-checker/packagist-cache.json for 24h.
|
|
9
|
+
*
|
|
10
|
+
* @author: N.BRAUN
|
|
11
|
+
* @email: pp9ping@gmail.com
|
|
9
12
|
*/
|
|
10
13
|
const fs = require("fs");
|
|
11
14
|
const path = require("path");
|
|
@@ -30,14 +33,18 @@ function cmp(a, b) {
|
|
|
30
33
|
|
|
31
34
|
// Extract {abandoned, outdated} from a Packagist `package` object.
|
|
32
35
|
function packagistToFindings(pkg, { version }) {
|
|
33
|
-
const out = { abandoned: null, outdated: null };
|
|
36
|
+
const out = { abandoned: null, outdated: null, license: null };
|
|
34
37
|
if (pkg.abandoned === true) out.abandoned = { replacement: null };
|
|
35
38
|
else if (typeof pkg.abandoned === "string") out.abandoned = { replacement: pkg.abandoned };
|
|
36
|
-
const
|
|
39
|
+
const versions = pkg.versions || {};
|
|
40
|
+
const stable = Object.keys(versions).map(v => v.replace(/^v/, "")).filter(isStable);
|
|
37
41
|
if (stable.length) {
|
|
38
42
|
const latest = stable.sort(cmp).at(-1);
|
|
39
43
|
if (latest && cmp(latest, String(version).replace(/^v/, "")) > 0) out.outdated = { latest };
|
|
40
44
|
}
|
|
45
|
+
// Packagist version objects carry a `license` array (e.g. ["MIT"]).
|
|
46
|
+
const vEntry = versions[version] || versions[`v${version}`] || versions[Object.keys(versions)[0]];
|
|
47
|
+
if (vEntry?.license && vEntry.license.length) out.license = vEntry.license;
|
|
41
48
|
return out;
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -55,7 +62,7 @@ async function fetchPackage(name, { offline }) {
|
|
|
55
62
|
async function checkComposerRegistryDeps(deps, opts = {}) {
|
|
56
63
|
const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
|
|
57
64
|
const targets = [...deps.values()].filter(d => d.ecosystem === "composer" && d.version);
|
|
58
|
-
const result = { deprecated: [], outdated: [] };
|
|
65
|
+
const result = { deprecated: [], outdated: [], licensed: [] };
|
|
59
66
|
if (!targets.length) return result;
|
|
60
67
|
const cache = loadCache();
|
|
61
68
|
const fresh = cache.meta?.fetchedAt && (Date.now() - cache.meta.fetchedAt) < CACHE_MAX_AGE_MS;
|
|
@@ -69,12 +76,13 @@ async function checkComposerRegistryDeps(deps, opts = {}) {
|
|
|
69
76
|
const pkg = await fetchPackage(name, { offline });
|
|
70
77
|
if (pkg && !pkg.error) {
|
|
71
78
|
const f = packagistToFindings(pkg, { version: t.version });
|
|
72
|
-
ex = { abandoned: f.abandoned, latest: f.outdated?.latest || null };
|
|
79
|
+
ex = { abandoned: f.abandoned, latest: f.outdated?.latest || null, license: f.license || null };
|
|
73
80
|
cache.entries[key] = ex;
|
|
74
81
|
} else {
|
|
75
|
-
ex = { abandoned: null, latest: null };
|
|
82
|
+
ex = { abandoned: null, latest: null, license: null };
|
|
76
83
|
}
|
|
77
84
|
}
|
|
85
|
+
if (ex.license) result.licensed.push({ dep: t, licenses: ex.license, source: "packagist" });
|
|
78
86
|
if (ex.abandoned) {
|
|
79
87
|
result.deprecated.push({ dep: t, severity: "MEDIUM", replacement: ex.abandoned.replacement, reason: "Package marked abandoned on Packagist", source: "packagist" });
|
|
80
88
|
if (verbose) process.stdout.write(` abandoned: ${name}@${t.version}\n`);
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Vuln scanning is OSV (ecosystem "Packagist", wired in Plan A). This codec adds
|
|
5
5
|
* collection (composer.lock, composer.json fallback), Packagist registry
|
|
6
6
|
* (abandoned + outdated), and EOL via endoflife.date.
|
|
7
|
+
*
|
|
8
|
+
* @author: N.BRAUN
|
|
9
|
+
* @email: pp9ping@gmail.com
|
|
7
10
|
*/
|
|
8
11
|
const fs = require("fs");
|
|
9
12
|
const path = require("path");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/go/parse.js — parse go.mod (authoritative for selected versions on
|
|
3
|
+
* Go ≥1.17, which lists the full pruned module graph) with go.sum as fallback.
|
|
4
|
+
*
|
|
5
|
+
* Versions are stored WITHOUT the leading "v" (OSV's Go ecosystem and our
|
|
6
|
+
* version comparisons expect bare semver; the purl layer re-adds context).
|
|
7
|
+
*
|
|
8
|
+
* @author: N.BRAUN
|
|
9
|
+
* @email: pp9ping@gmail.com
|
|
10
|
+
*/
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const { compareMavenVersions } = require("../../maven-version");
|
|
13
|
+
|
|
14
|
+
function stripV(v) { return String(v || "").replace(/^v/, ""); }
|
|
15
|
+
|
|
16
|
+
/** Parse go.mod → { module, deps: [{ name, version, scope, isDev }] }. */
|
|
17
|
+
function parseGoMod(text) {
|
|
18
|
+
const out = { module: null, deps: [] };
|
|
19
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
20
|
+
let inRequire = false;
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const addReq = (name, ver, indirect) => {
|
|
23
|
+
if (!name || !ver) return;
|
|
24
|
+
const key = `${name}@${ver}`;
|
|
25
|
+
if (seen.has(key)) return;
|
|
26
|
+
seen.add(key);
|
|
27
|
+
out.deps.push({ name, version: stripV(ver), scope: indirect ? "transitive" : "compile", isDev: false });
|
|
28
|
+
};
|
|
29
|
+
for (let raw of lines) {
|
|
30
|
+
const noComment = raw.split("//")[0].trim();
|
|
31
|
+
const indirect = /\/\/\s*indirect/.test(raw);
|
|
32
|
+
if (noComment.startsWith("module ")) { out.module = noComment.slice(7).trim(); continue; }
|
|
33
|
+
if (noComment === "require (") { inRequire = true; continue; }
|
|
34
|
+
if (inRequire && noComment === ")") { inRequire = false; continue; }
|
|
35
|
+
if (inRequire) {
|
|
36
|
+
const m = noComment.match(/^(\S+)\s+(\S+)/);
|
|
37
|
+
if (m) addReq(m[1], m[2], indirect);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const single = noComment.match(/^require\s+(\S+)\s+(\S+)/);
|
|
41
|
+
if (single) addReq(single[1], single[2], indirect);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Parse go.sum → deps (fallback when go.mod has no require list). Highest version per module. */
|
|
47
|
+
function parseGoSum(text) {
|
|
48
|
+
const byMod = new Map();
|
|
49
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
50
|
+
const m = raw.trim().match(/^(\S+)\s+(v\S+?)(\/go\.mod)?\s+h1:/);
|
|
51
|
+
if (!m) continue;
|
|
52
|
+
const name = m[1];
|
|
53
|
+
const ver = stripV(m[2]);
|
|
54
|
+
// go.sum lists every version in the module graph; keep the highest (the
|
|
55
|
+
// comment promised this but the code kept the first one encountered).
|
|
56
|
+
const prev = byMod.get(name);
|
|
57
|
+
if (!prev || compareMavenVersions(ver, prev) > 0) byMod.set(name, ver);
|
|
58
|
+
}
|
|
59
|
+
return { deps: [...byMod.entries()].map(([name, version]) => ({ name, version, scope: "transitive", isDev: false })) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseGoModFile(fp) { return parseGoMod(fs.readFileSync(fp, "utf8")); }
|
|
63
|
+
function parseGoSumFile(fp) { return parseGoSum(fs.readFileSync(fp, "utf8")); }
|
|
64
|
+
|
|
65
|
+
module.exports = { parseGoMod, parseGoSum, parseGoModFile, parseGoSumFile, stripV };
|