@cdxgen/cdxgen 13.0.0 → 13.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/README.md +26 -13
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +140 -8
- package/bin/convert.js +7 -0
- package/bin/evinse.js +1 -1
- package/bin/hbom.js +13 -4
- package/bin/tracebom.js +9 -1
- package/data/component-tags.json +8 -1
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1144 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/cli/bomAssembly.js +83 -15
- package/lib/cli/cliOptions.js +124 -7
- package/lib/cli/index.js +76 -48
- package/lib/cli/jsBom.js +32 -3
- package/lib/cli/jvmBom.js +1494 -1315
- package/lib/cli/managedBom.js +97 -28
- package/lib/cli/nativeBom.js +189 -10
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +69 -1
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +76 -25
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/ui.js +46 -2
- package/lib/ecosystems/ecosystems.js +9 -0
- package/lib/ecosystems/gradleutils.js +355 -15
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-misc.js +16 -2
- package/lib/ecosystems/parsers-python.js +171 -35
- package/lib/ecosystems/parsers-rust.js +12 -2
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/utils.js +9 -1
- package/lib/evinser/evinser.js +30 -12
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/auditCategories.js +9 -0
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +431 -22
- package/lib/inventory/envcontext.js +211 -19
- package/lib/inventory/fetchBatch.js +57 -0
- package/lib/inventory/jvmToolEnv.js +593 -0
- package/lib/inventory/osinfo.js +123 -16
- package/lib/inventory/table.js +9 -5
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +216 -40
- package/lib/managers/docker.js +26 -5
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +227 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +115 -2
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +14 -14
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +14 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +22 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +14 -1
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/ui.d.ts +1 -1
- package/types/lib/core/ui.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +33 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +20 -0
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/utils.d.ts +4 -4
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +5 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +8 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +20 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +9 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts +83 -2
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/table.d.ts +4 -1
- package/types/lib/inventory/table.d.ts.map +1 -1
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts +71 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +2 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/lib/core/ui.js
CHANGED
|
@@ -17,6 +17,7 @@ import process from "node:process";
|
|
|
17
17
|
import { formatWithOptions, inspect } from "node:util";
|
|
18
18
|
|
|
19
19
|
import { readEnvironmentVariable } from "./activity.js";
|
|
20
|
+
import { diagnosticStream, TRACE_MODE, traceLog } from "./logger.js";
|
|
20
21
|
import { stringWidth, stripAnsi } from "./text.js";
|
|
21
22
|
|
|
22
23
|
// Frame interval, and the floor on how often detail/progress updates redraw.
|
|
@@ -94,7 +95,11 @@ const PHASE_OUTCOMES = {
|
|
|
94
95
|
* @returns {{stream: object, interactive: boolean, color: boolean, unicode: boolean, glyphs: object, columns: () => number, rows: () => number}} Capability snapshot
|
|
95
96
|
*/
|
|
96
97
|
export function detectCapabilities(overrides = {}) {
|
|
97
|
-
|
|
98
|
+
// `CDXGEN_LOG_STREAM=stdout` is the documented escape hatch for callers that
|
|
99
|
+
// scraped v12's stdout. Resolving the destination here rather than pinning
|
|
100
|
+
// stderr is what makes it apply to the region, the banners and the tables
|
|
101
|
+
// instead of only to the thought and trace logs.
|
|
102
|
+
const stream = overrides.stream || diagnosticStream;
|
|
98
103
|
const env = overrides.env;
|
|
99
104
|
const read = (name) => (env ? env[name] : readEnvironmentVariable(name));
|
|
100
105
|
const isSet = (name) => {
|
|
@@ -282,7 +287,7 @@ function formatArgs(args) {
|
|
|
282
287
|
* Create a live-region controller.
|
|
283
288
|
*
|
|
284
289
|
* @param {object} [options] Controller options
|
|
285
|
-
* @param {object} [options.stream] Writable stream (default
|
|
290
|
+
* @param {object} [options.stream] Writable stream (default: the diagnostic stream, stderr unless `CDXGEN_LOG_STREAM=stdout`)
|
|
286
291
|
* @param {number|string} [options.level] Verbosity ladder 0..3
|
|
287
292
|
* @param {("text"|"json")} [options.format] Log serialization format
|
|
288
293
|
* @param {boolean} [options.noProgress] Force-disable the live region
|
|
@@ -538,6 +543,40 @@ export function createUi(options = {}) {
|
|
|
538
543
|
}
|
|
539
544
|
}
|
|
540
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Mirror a phase transition onto the trace stream.
|
|
548
|
+
*
|
|
549
|
+
* The live region is a terminal affordance, so a piped consumer such as the
|
|
550
|
+
* cdxui terminal UI would otherwise have to scrape formatted lines to learn
|
|
551
|
+
* what cdxgen is doing. Emitting the phase model structurally lets it render
|
|
552
|
+
* its own progress. `progress` transitions are throttled to the frame
|
|
553
|
+
* interval so a per-file loop costs one event per frame, not one per file.
|
|
554
|
+
*
|
|
555
|
+
* @param {object} handle Phase handle
|
|
556
|
+
* @param {string} state One of started, progress, succeeded, failed, skipped
|
|
557
|
+
*/
|
|
558
|
+
function tracePhase(handle, state) {
|
|
559
|
+
if (!TRACE_MODE) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const at = now();
|
|
563
|
+
if (state === "progress") {
|
|
564
|
+
if (at - (handle.tracedAt || 0) < REFRESH_MS) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
handle.tracedAt = at;
|
|
568
|
+
}
|
|
569
|
+
traceLog("phase", {
|
|
570
|
+
phase: handle.label,
|
|
571
|
+
state,
|
|
572
|
+
detail: handle.detailText ?? undefined,
|
|
573
|
+
note: handle.note ?? undefined,
|
|
574
|
+
done: handle.total > 0 ? handle.done : undefined,
|
|
575
|
+
total: handle.total > 0 ? handle.total : undefined,
|
|
576
|
+
elapsedMs: at - handle.startedAt,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
541
580
|
function commitPhase(phase) {
|
|
542
581
|
const outcome = PHASE_OUTCOMES[phase.status] || PHASE_OUTCOMES.succeeded;
|
|
543
582
|
const glyph = caps.glyphs[phase.status] || caps.glyphs.succeeded;
|
|
@@ -568,6 +607,7 @@ export function createUi(options = {}) {
|
|
|
568
607
|
startedAt: now(),
|
|
569
608
|
note: null,
|
|
570
609
|
committed: false,
|
|
610
|
+
tracedAt: 0,
|
|
571
611
|
|
|
572
612
|
/**
|
|
573
613
|
* Replace the trailing detail text on this phase's row.
|
|
@@ -578,6 +618,7 @@ export function createUi(options = {}) {
|
|
|
578
618
|
detail(text) {
|
|
579
619
|
if (!this.committed) {
|
|
580
620
|
this.detailText = text ? `${text}` : null;
|
|
621
|
+
tracePhase(this, "progress");
|
|
581
622
|
requestRender();
|
|
582
623
|
}
|
|
583
624
|
return this;
|
|
@@ -594,6 +635,7 @@ export function createUi(options = {}) {
|
|
|
594
635
|
if (!this.committed) {
|
|
595
636
|
this.done = Number(done) || 0;
|
|
596
637
|
this.total = Number(total) || 0;
|
|
638
|
+
tracePhase(this, "progress");
|
|
597
639
|
requestRender();
|
|
598
640
|
}
|
|
599
641
|
return this;
|
|
@@ -631,6 +673,7 @@ export function createUi(options = {}) {
|
|
|
631
673
|
},
|
|
632
674
|
};
|
|
633
675
|
running.push(handle);
|
|
676
|
+
tracePhase(handle, "started");
|
|
634
677
|
startTimer();
|
|
635
678
|
requestRender(true);
|
|
636
679
|
return handle;
|
|
@@ -643,6 +686,7 @@ export function createUi(options = {}) {
|
|
|
643
686
|
handle.committed = true;
|
|
644
687
|
handle.status = status;
|
|
645
688
|
handle.note = note ? `${note}` : null;
|
|
689
|
+
tracePhase(handle, status);
|
|
646
690
|
const index = running.indexOf(handle);
|
|
647
691
|
if (index >= 0) {
|
|
648
692
|
running.splice(index, 1);
|
|
@@ -80,6 +80,10 @@ import {
|
|
|
80
80
|
} from "../inventory/spdx.js";
|
|
81
81
|
import { extractLicenseText, extractRepoUrl } from "../parsers/htmlExtract.js";
|
|
82
82
|
import { xml2js } from "../parsers/xml.js";
|
|
83
|
+
import {
|
|
84
|
+
applyPypiClassifierMetadata,
|
|
85
|
+
applyPypiModuleNames,
|
|
86
|
+
} from "./pypiClassifiers.js";
|
|
83
87
|
import {
|
|
84
88
|
collectCargoRegistryProvenanceProperties,
|
|
85
89
|
collectNpmRegistryProvenanceProperties,
|
|
@@ -950,6 +954,10 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
950
954
|
continue;
|
|
951
955
|
}
|
|
952
956
|
try {
|
|
957
|
+
// Module names are derived from the distribution name and the curated alias
|
|
958
|
+
// map, so they are available without a registry round trip and are attached
|
|
959
|
+
// before the skip below rather than after it.
|
|
960
|
+
applyPypiModuleNames(p);
|
|
953
961
|
// If the package name has a url or already includes license and version skip it
|
|
954
962
|
if (p.name.includes("https") || (p.license && p.version)) {
|
|
955
963
|
cdepList.push(p);
|
|
@@ -1011,6 +1019,7 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
1011
1019
|
}
|
|
1012
1020
|
}
|
|
1013
1021
|
}
|
|
1022
|
+
applyPypiClassifierMetadata(p, body.info.classifiers);
|
|
1014
1023
|
}
|
|
1015
1024
|
if (body.info.license) {
|
|
1016
1025
|
const licenseId = findLicenseId(body.info.license);
|
|
@@ -10,13 +10,33 @@ import {
|
|
|
10
10
|
readEnvironmentVariable,
|
|
11
11
|
recordDecisionActivity,
|
|
12
12
|
} from "../core/activity.js";
|
|
13
|
+
import {
|
|
14
|
+
commandOutputText,
|
|
15
|
+
isLedgerEnabled,
|
|
16
|
+
LEDGER_EVENT_IMPACTS,
|
|
17
|
+
LEDGER_EVENT_KINDS,
|
|
18
|
+
recordDegradation,
|
|
19
|
+
recordLedgerEvent,
|
|
20
|
+
} from "../core/buildLedger.js";
|
|
13
21
|
import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
|
|
14
22
|
import { thoughtLog } from "../core/logger.js";
|
|
15
23
|
import { isWin } from "../core/paths.js";
|
|
16
24
|
import { collectJarNS } from "../inventory/deps.js";
|
|
25
|
+
import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
|
|
17
26
|
import { applyPurl } from "../inventory/purl.js";
|
|
18
27
|
import { parsePkgJson } from "./parsers-js.js";
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* True when the pre-generation stage pinned an exact JVM build tool version
|
|
31
|
+
* from the CLI (e.g. `-t maven3.9.9`). Project wrappers must not override
|
|
32
|
+
* such an explicit pin.
|
|
33
|
+
*/
|
|
34
|
+
function isJvmToolPinned() {
|
|
35
|
+
return ["true", "1"].includes(
|
|
36
|
+
readEnvironmentVariable("CDXGEN_JVM_TOOL_PINNED"),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
/**
|
|
21
41
|
* Method to return the gradle command to use.
|
|
22
42
|
*
|
|
@@ -25,19 +45,21 @@ import { parsePkgJson } from "./parsers-js.js";
|
|
|
25
45
|
*/
|
|
26
46
|
export function getGradleCommand(srcPath, rootPath) {
|
|
27
47
|
let gradleCmd = "gradle";
|
|
48
|
+
const pinnedToolSelected = isJvmToolPinned();
|
|
28
49
|
|
|
29
50
|
let findGradleFile = "gradlew";
|
|
30
51
|
if (platform() === "win32") {
|
|
31
52
|
findGradleFile = "gradlew.bat";
|
|
32
53
|
}
|
|
33
54
|
|
|
34
|
-
if (safeExistsSync(join(srcPath, findGradleFile))) {
|
|
55
|
+
if (!pinnedToolSelected && safeExistsSync(join(srcPath, findGradleFile))) {
|
|
35
56
|
// Use local gradle wrapper if available
|
|
36
57
|
// Enable execute permission
|
|
37
58
|
try {
|
|
38
59
|
chmodSync(join(srcPath, findGradleFile), 0o775);
|
|
39
60
|
} catch (_e) {
|
|
40
|
-
//
|
|
61
|
+
// The wrapper stays un-chmod'ed; the spawn below still reports any
|
|
62
|
+
// permission failure as a normal command failure.
|
|
41
63
|
}
|
|
42
64
|
gradleCmd = resolve(join(srcPath, findGradleFile));
|
|
43
65
|
recordDecisionActivity(gradleCmd, {
|
|
@@ -48,12 +70,17 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
48
70
|
},
|
|
49
71
|
reason: `Selected project-local Gradle wrapper ${gradleCmd}.`,
|
|
50
72
|
});
|
|
51
|
-
} else if (
|
|
73
|
+
} else if (
|
|
74
|
+
!pinnedToolSelected &&
|
|
75
|
+
rootPath &&
|
|
76
|
+
safeExistsSync(join(rootPath, findGradleFile))
|
|
77
|
+
) {
|
|
52
78
|
// Check if the root directory has a wrapper script
|
|
53
79
|
try {
|
|
54
80
|
chmodSync(join(rootPath, findGradleFile), 0o775);
|
|
55
81
|
} catch (_e) {
|
|
56
|
-
//
|
|
82
|
+
// The wrapper stays un-chmod'ed; the spawn below still reports any
|
|
83
|
+
// permission failure as a normal command failure.
|
|
57
84
|
}
|
|
58
85
|
gradleCmd = resolve(join(rootPath, findGradleFile));
|
|
59
86
|
recordDecisionActivity(gradleCmd, {
|
|
@@ -69,10 +96,12 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
69
96
|
recordDecisionActivity(gradleCmd, {
|
|
70
97
|
metadata: {
|
|
71
98
|
decisionType: "path-resolution",
|
|
72
|
-
selectedSource: "GRADLE_CMD",
|
|
99
|
+
selectedSource: pinnedToolSelected ? "pinned-cli" : "GRADLE_CMD",
|
|
73
100
|
tool: "gradle",
|
|
74
101
|
},
|
|
75
|
-
reason:
|
|
102
|
+
reason: pinnedToolSelected
|
|
103
|
+
? `Selected Gradle command ${gradleCmd} pinned via the CLI project type.`
|
|
104
|
+
: `Selected Gradle command from GRADLE_CMD (${gradleCmd}).`,
|
|
76
105
|
});
|
|
77
106
|
} else if (readEnvironmentVariable("GRADLE_HOME")) {
|
|
78
107
|
gradleCmd = join(readEnvironmentVariable("GRADLE_HOME"), "bin", "gradle");
|
|
@@ -93,6 +122,16 @@ export function getGradleCommand(srcPath, rootPath) {
|
|
|
93
122
|
},
|
|
94
123
|
reason: "Falling back to Gradle from PATH.",
|
|
95
124
|
});
|
|
125
|
+
// A Gradle from PATH is a sound outcome whenever its version satisfies
|
|
126
|
+
// the wrapper pin, so the fallback is recorded as costing nothing.
|
|
127
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.FALLBACK_ENGAGED, {
|
|
128
|
+
ecosystem: "java",
|
|
129
|
+
tool: "gradle",
|
|
130
|
+
source: "PATH",
|
|
131
|
+
impact: LEDGER_EVENT_IMPACTS.NONE,
|
|
132
|
+
detail:
|
|
133
|
+
"No wrapper, GRADLE_CMD, or GRADLE_HOME was found; Gradle will run from PATH.",
|
|
134
|
+
});
|
|
96
135
|
}
|
|
97
136
|
return gradleCmd;
|
|
98
137
|
}
|
|
@@ -368,6 +407,9 @@ export function executeParallelGradleProperties(
|
|
|
368
407
|
shell: isWin,
|
|
369
408
|
});
|
|
370
409
|
if (result.status !== 0 || result.error) {
|
|
410
|
+
recordGradleInvocationFailure(result, {
|
|
411
|
+
command: `${basename(gradleCmd)} ${gradleArg.join(" ")}`,
|
|
412
|
+
});
|
|
371
413
|
if (readEnvironmentVariable("CDXGEN_IN_CONTAINER") === "true") {
|
|
372
414
|
thoughtLog(
|
|
373
415
|
"Gradle build has failed. Perhaps the user is using the wrong container image?",
|
|
@@ -420,6 +462,279 @@ export function executeParallelGradleProperties(
|
|
|
420
462
|
return "";
|
|
421
463
|
}
|
|
422
464
|
|
|
465
|
+
/** First class-file major a JDK maps from: class file 52 is Java 8. */
|
|
466
|
+
const CLASS_FILE_MAJOR_OFFSET = 44;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Causes already reported this run, so a properties invocation and a
|
|
470
|
+
* dependencies invocation failing for the same reason record the diagnosis
|
|
471
|
+
* once.
|
|
472
|
+
*
|
|
473
|
+
* @type {Set<string>}
|
|
474
|
+
*/
|
|
475
|
+
const reportedGradleFailureCauses = new Set();
|
|
476
|
+
|
|
477
|
+
/** Whether an invocation failure was recorded this run. */
|
|
478
|
+
let gradleInvocationFailed = false;
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Forget the causes diagnosed so far, so a process that generates more than
|
|
482
|
+
* one BOM diagnoses each run on its own.
|
|
483
|
+
*
|
|
484
|
+
* @returns {void}
|
|
485
|
+
*/
|
|
486
|
+
export function resetGradleFailureCauses() {
|
|
487
|
+
reportedGradleFailureCauses.clear();
|
|
488
|
+
gradleInvocationFailed = false;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Whether a gradle invocation has already failed outright this run, so a
|
|
493
|
+
* caller can skip reporting a consequence of that failure as its own
|
|
494
|
+
* degradation.
|
|
495
|
+
*
|
|
496
|
+
* @returns {boolean} True once an invocation failure has been recorded.
|
|
497
|
+
*/
|
|
498
|
+
export function hasGradleInvocationFailure() {
|
|
499
|
+
return gradleInvocationFailed;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Extract the Java major from an `Unsupported class file major version N`
|
|
504
|
+
* message: the class file the running JVM refused, which names the JDK that
|
|
505
|
+
* is too new for this Gradle.
|
|
506
|
+
*
|
|
507
|
+
* @param {string} text Combined gradle output.
|
|
508
|
+
* @returns {number|undefined} The refused Java major, when named.
|
|
509
|
+
*/
|
|
510
|
+
function refusedJavaMajorFromMessage(text) {
|
|
511
|
+
const marker = "Unsupported class file major version ";
|
|
512
|
+
const start = text.indexOf(marker);
|
|
513
|
+
if (start === -1) {
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
const digits = text
|
|
517
|
+
.slice(start + marker.length)
|
|
518
|
+
.split(/\s|\.|,|\r|\n/)[0]
|
|
519
|
+
.trim();
|
|
520
|
+
const major = Number.parseInt(digits, 10);
|
|
521
|
+
return Number.isNaN(major) ? undefined : major - CLASS_FILE_MAJOR_OFFSET;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Extract the Java pair the modern launcher refusal names: the JVM major
|
|
526
|
+
* Gradle demands and, when the message says so, the one the build is
|
|
527
|
+
* configured to use. Anything the message does not clearly name stays
|
|
528
|
+
* undefined — the demanded major is substituted into an install command, so
|
|
529
|
+
* it must be a bare Java major and nothing else.
|
|
530
|
+
*
|
|
531
|
+
* @param {string} text Combined gradle output.
|
|
532
|
+
* @returns {{wanted: number, found: number|undefined}|undefined} The required and running Java majors, when the refusal names them.
|
|
533
|
+
*/
|
|
534
|
+
function javaPairFromLauncherDemand(text) {
|
|
535
|
+
const demandMarker = "Gradle requires JVM ";
|
|
536
|
+
const demandStart = text.indexOf(demandMarker);
|
|
537
|
+
if (demandStart === -1) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
const wantedDigits = text
|
|
541
|
+
.slice(demandStart + demandMarker.length)
|
|
542
|
+
.split(/\s|\.|,|\r|\n/)[0]
|
|
543
|
+
.trim();
|
|
544
|
+
const wanted = Number.parseInt(wantedDigits, 10);
|
|
545
|
+
if (Number.isNaN(wanted)) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
const foundMarker = "configured to use JVM ";
|
|
549
|
+
const foundStart = text.indexOf(foundMarker);
|
|
550
|
+
let found;
|
|
551
|
+
if (foundStart !== -1) {
|
|
552
|
+
const foundDigits = text
|
|
553
|
+
.slice(foundStart + foundMarker.length)
|
|
554
|
+
.split(/\s|\.|,|\r|\n/)[0]
|
|
555
|
+
.trim();
|
|
556
|
+
const parsed = Number.parseInt(foundDigits, 10);
|
|
557
|
+
if (!Number.isNaN(parsed)) {
|
|
558
|
+
found = parsed;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { wanted, found };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Extract the Java pair an `UnsupportedClassVersionError` names: the class
|
|
566
|
+
* file version that was compiled for a newer JDK and the version the running
|
|
567
|
+
* JDK recognises, so the event can say "needs Java A, found Java B".
|
|
568
|
+
*
|
|
569
|
+
* @param {string} text Combined gradle output.
|
|
570
|
+
* @returns {{wanted: number, found: number}|undefined} The required and running Java majors.
|
|
571
|
+
*/
|
|
572
|
+
function javaPairFromClassVersionError(text) {
|
|
573
|
+
if (!text.includes("UnsupportedClassVersionError")) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
const compiledMarker = "class file version ";
|
|
577
|
+
const compiledStart = text.indexOf(compiledMarker);
|
|
578
|
+
const limitMarker = "recognizes class file versions up to ";
|
|
579
|
+
const limitStart = text.indexOf(limitMarker);
|
|
580
|
+
if (compiledStart === -1 || limitStart === -1) {
|
|
581
|
+
return { wanted: Number.NaN, found: Number.NaN };
|
|
582
|
+
}
|
|
583
|
+
const wanted = Number.parseInt(
|
|
584
|
+
text.slice(compiledStart + compiledMarker.length).trim(),
|
|
585
|
+
10,
|
|
586
|
+
);
|
|
587
|
+
const found = Number.parseInt(
|
|
588
|
+
text.slice(limitStart + limitMarker.length).trim(),
|
|
589
|
+
10,
|
|
590
|
+
);
|
|
591
|
+
return {
|
|
592
|
+
wanted: wanted - CLASS_FILE_MAJOR_OFFSET,
|
|
593
|
+
found: found - CLASS_FILE_MAJOR_OFFSET,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Record why a gradle invocation failed. Every failure also records the
|
|
599
|
+
* `jvm.gradle.invocation-failed` degradation so the reflection can rank the
|
|
600
|
+
* repair; version-incompatibility causes additionally record a
|
|
601
|
+
* `tool.mismatch` event naming the versions, which is the fact the report
|
|
602
|
+
* cannot derive from an empty BOM alone. Diagnosis events are recorded once
|
|
603
|
+
* per cause per run; the degradation dedupes in the reflection by id.
|
|
604
|
+
*
|
|
605
|
+
* @param {Object} result The spawnSync result of the failed invocation.
|
|
606
|
+
* @param {Object} context Call-site facts.
|
|
607
|
+
* @param {string} [context.command] The redacted command that was attempted.
|
|
608
|
+
* @param {string} [context.gradleVersion] The gradle version in play, when the caller knows it.
|
|
609
|
+
* @returns {void}
|
|
610
|
+
*/
|
|
611
|
+
export function recordGradleInvocationFailure(result, context = {}) {
|
|
612
|
+
if (!isLedgerEnabled()) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (!result || (result.status === 0 && !result.error)) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const output = `${result.stderr || ""}\n${result.stdout || ""}`;
|
|
619
|
+
const exitCode =
|
|
620
|
+
typeof result.status === "number" ? result.status : undefined;
|
|
621
|
+
let causeDetail = "the gradle invocation failed";
|
|
622
|
+
|
|
623
|
+
// The JDK is too new for this Gradle: the build script or the launcher
|
|
624
|
+
// refuses its class files. Gradle's own compatibility table names the
|
|
625
|
+
// minimum release that runs on the refused Java major.
|
|
626
|
+
const refusedJavaMajor = refusedJavaMajorFromMessage(output);
|
|
627
|
+
// Gradle's own modern refusal, which names the JVM it demands before any
|
|
628
|
+
// class file is loaded.
|
|
629
|
+
const launcherDemand =
|
|
630
|
+
refusedJavaMajor === undefined
|
|
631
|
+
? javaPairFromLauncherDemand(output)
|
|
632
|
+
: undefined;
|
|
633
|
+
if (refusedJavaMajor !== undefined) {
|
|
634
|
+
const minimumGradle = minimumGradleVersionForJava(refusedJavaMajor);
|
|
635
|
+
const causeKey = `java-too-new:${refusedJavaMajor}`;
|
|
636
|
+
if (!reportedGradleFailureCauses.has(causeKey)) {
|
|
637
|
+
reportedGradleFailureCauses.add(causeKey);
|
|
638
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
|
|
639
|
+
ecosystem: "java",
|
|
640
|
+
tool: "gradle",
|
|
641
|
+
wanted: minimumGradle ? `${minimumGradle}` : undefined,
|
|
642
|
+
found: context.gradleVersion,
|
|
643
|
+
source: "invocation",
|
|
644
|
+
detail: minimumGradle
|
|
645
|
+
? `Gradle cannot run on Java ${refusedJavaMajor} and refused its class files; Gradle ${minimumGradle} or higher supports it.`
|
|
646
|
+
: `Gradle cannot run on Java ${refusedJavaMajor} and refused its class files.`,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
causeDetail = `gradle cannot run on Java ${refusedJavaMajor}`;
|
|
650
|
+
} else if (launcherDemand) {
|
|
651
|
+
// The demand names the JDK Gradle needs; the JVM that answered it is
|
|
652
|
+
// the one the build was configured to use.
|
|
653
|
+
const causeKey = `jvm-demanded:${launcherDemand.wanted}:${launcherDemand.found}`;
|
|
654
|
+
if (!reportedGradleFailureCauses.has(causeKey)) {
|
|
655
|
+
reportedGradleFailureCauses.add(causeKey);
|
|
656
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
|
|
657
|
+
ecosystem: "java",
|
|
658
|
+
tool: "java",
|
|
659
|
+
wanted: `${launcherDemand.wanted}`,
|
|
660
|
+
found:
|
|
661
|
+
launcherDemand.found === undefined
|
|
662
|
+
? undefined
|
|
663
|
+
: `${launcherDemand.found}`,
|
|
664
|
+
source: "invocation",
|
|
665
|
+
detail:
|
|
666
|
+
launcherDemand.found === undefined
|
|
667
|
+
? `Gradle requires Java ${launcherDemand.wanted} or later to run.`
|
|
668
|
+
: `Gradle requires Java ${launcherDemand.wanted} or later to run; the active JVM is Java ${launcherDemand.found}.`,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
causeDetail = "the gradle launcher requires a newer JDK";
|
|
672
|
+
} else {
|
|
673
|
+
// The opposite skew: Gradle (or its wrapper) is compiled for a newer JDK
|
|
674
|
+
// than the running one, so the JVM refuses to load it.
|
|
675
|
+
const javaPair = javaPairFromClassVersionError(output);
|
|
676
|
+
if (javaPair) {
|
|
677
|
+
const causeKey = `jvm-too-old:${javaPair.wanted}:${javaPair.found}`;
|
|
678
|
+
if (!reportedGradleFailureCauses.has(causeKey)) {
|
|
679
|
+
reportedGradleFailureCauses.add(causeKey);
|
|
680
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISMATCH, {
|
|
681
|
+
ecosystem: "java",
|
|
682
|
+
tool: "java",
|
|
683
|
+
wanted: Number.isNaN(javaPair.wanted)
|
|
684
|
+
? undefined
|
|
685
|
+
: `${javaPair.wanted}`,
|
|
686
|
+
found: Number.isNaN(javaPair.found) ? undefined : `${javaPair.found}`,
|
|
687
|
+
source: "invocation",
|
|
688
|
+
detail: `The gradle launcher requires Java ${Number.isNaN(javaPair.wanted) ? "a newer release" : javaPair.wanted}; the active JDK is older and refused its class files.`,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
causeDetail = "the gradle launcher requires a newer JDK";
|
|
692
|
+
} else if (output.includes("JAVA_HOME is set to an invalid directory")) {
|
|
693
|
+
const causeKey = "invalid-java-home";
|
|
694
|
+
if (!reportedGradleFailureCauses.has(causeKey)) {
|
|
695
|
+
reportedGradleFailureCauses.add(causeKey);
|
|
696
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.TOOL_MISSING, {
|
|
697
|
+
ecosystem: "java",
|
|
698
|
+
tool: "java",
|
|
699
|
+
source: "JAVA_HOME",
|
|
700
|
+
detail:
|
|
701
|
+
"JAVA_HOME names a directory that does not hold a JDK, so the gradle launcher refused to start.",
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
causeDetail = "JAVA_HOME does not point at a JDK";
|
|
705
|
+
} else if (
|
|
706
|
+
output.includes("Could not start Gradle Daemon") ||
|
|
707
|
+
output.includes("Daemon could not be started") ||
|
|
708
|
+
output.includes("could not install Gradle distribution")
|
|
709
|
+
) {
|
|
710
|
+
const causeKey = "daemon-start";
|
|
711
|
+
if (!reportedGradleFailureCauses.has(causeKey)) {
|
|
712
|
+
reportedGradleFailureCauses.add(causeKey);
|
|
713
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
|
|
714
|
+
ecosystem: "java",
|
|
715
|
+
tool: "gradle",
|
|
716
|
+
impact: LEDGER_EVENT_IMPACTS.VERSIONS,
|
|
717
|
+
detail:
|
|
718
|
+
"The gradle daemon did not start, so no dependency resolution ran.",
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
causeDetail = "the gradle daemon did not start";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
gradleInvocationFailed = true;
|
|
726
|
+
recordDegradation("jvm.gradle.invocation-failed", {
|
|
727
|
+
ecosystem: "java",
|
|
728
|
+
tool: "gradle",
|
|
729
|
+
impact: LEDGER_EVENT_IMPACTS.TRANSITIVE_DEPS,
|
|
730
|
+
command: context.command,
|
|
731
|
+
exitCode,
|
|
732
|
+
detail: causeDetail,
|
|
733
|
+
causeDetail,
|
|
734
|
+
outputExcerpt: commandOutputText(result),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
423
738
|
/**
|
|
424
739
|
* Method to resolve dependencies from a gradle output
|
|
425
740
|
*
|
|
@@ -583,6 +898,8 @@ export async function parseGradleDep(
|
|
|
583
898
|
});
|
|
584
899
|
bomRef = decodeURIComponent(purl);
|
|
585
900
|
} catch {
|
|
901
|
+
// A purl that cannot be built keeps a name-only bom-ref so the
|
|
902
|
+
// component still lands in the graph.
|
|
586
903
|
purl = undefined;
|
|
587
904
|
bomRef = name.replace(/^:/, "");
|
|
588
905
|
}
|
|
@@ -724,6 +1041,7 @@ export async function buildObjectForGradleModule(name, metadata) {
|
|
|
724
1041
|
});
|
|
725
1042
|
applyPurl(component, purl);
|
|
726
1043
|
} catch {
|
|
1044
|
+
// The module component keeps its name identity without a purl.
|
|
727
1045
|
applyPurl(component, null);
|
|
728
1046
|
}
|
|
729
1047
|
}
|
|
@@ -896,7 +1214,8 @@ export function getMillCommand(srcPath) {
|
|
|
896
1214
|
try {
|
|
897
1215
|
chmodSync(join(srcPath, millCmd), 0o775);
|
|
898
1216
|
} catch (_e) {
|
|
899
|
-
//
|
|
1217
|
+
// The mill wrapper stays un-chmod'ed; the spawn below still reports any
|
|
1218
|
+
// permission failure as a normal command failure.
|
|
900
1219
|
}
|
|
901
1220
|
millCmd = resolve(join(srcPath, millCmd));
|
|
902
1221
|
}
|
|
@@ -914,6 +1233,7 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
914
1233
|
// Check if the wrapper script is both available and functional
|
|
915
1234
|
let isWrapperReady = false;
|
|
916
1235
|
let isWrapperFound = false;
|
|
1236
|
+
const pinnedToolSelected = isJvmToolPinned();
|
|
917
1237
|
let findMavenFile = "mvnw";
|
|
918
1238
|
let mavenWrapperCmd = null;
|
|
919
1239
|
if (platform() === "win32") {
|
|
@@ -926,13 +1246,14 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
926
1246
|
}
|
|
927
1247
|
}
|
|
928
1248
|
|
|
929
|
-
if (safeExistsSync(join(srcPath, findMavenFile))) {
|
|
1249
|
+
if (!pinnedToolSelected && safeExistsSync(join(srcPath, findMavenFile))) {
|
|
930
1250
|
// Use local maven wrapper if available
|
|
931
1251
|
// Enable execute permission
|
|
932
1252
|
try {
|
|
933
1253
|
chmodSync(join(srcPath, findMavenFile), 0o775);
|
|
934
1254
|
} catch (_e) {
|
|
935
|
-
//
|
|
1255
|
+
// The wrapper stays un-chmod'ed; the spawn below still reports any
|
|
1256
|
+
// permission failure as a normal command failure.
|
|
936
1257
|
}
|
|
937
1258
|
mavenWrapperCmd = resolve(join(srcPath, findMavenFile));
|
|
938
1259
|
isWrapperFound = true;
|
|
@@ -944,12 +1265,17 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
944
1265
|
},
|
|
945
1266
|
reason: `Found Maven wrapper candidate ${mavenWrapperCmd}.`,
|
|
946
1267
|
});
|
|
947
|
-
} else if (
|
|
1268
|
+
} else if (
|
|
1269
|
+
!pinnedToolSelected &&
|
|
1270
|
+
rootPath &&
|
|
1271
|
+
safeExistsSync(join(rootPath, findMavenFile))
|
|
1272
|
+
) {
|
|
948
1273
|
// Check if the root directory has a wrapper script
|
|
949
1274
|
try {
|
|
950
1275
|
chmodSync(join(rootPath, findMavenFile), 0o775);
|
|
951
1276
|
} catch (_e) {
|
|
952
|
-
//
|
|
1277
|
+
// The wrapper stays un-chmod'ed; the spawn below still reports any
|
|
1278
|
+
// permission failure as a normal command failure.
|
|
953
1279
|
}
|
|
954
1280
|
mavenWrapperCmd = resolve(join(rootPath, findMavenFile));
|
|
955
1281
|
isWrapperFound = true;
|
|
@@ -1016,12 +1342,16 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
1016
1342
|
recordDecisionActivity(mavenCmd, {
|
|
1017
1343
|
metadata: {
|
|
1018
1344
|
decisionType: "path-resolution",
|
|
1019
|
-
selectedSource:
|
|
1020
|
-
? "
|
|
1021
|
-
: "
|
|
1345
|
+
selectedSource: pinnedToolSelected
|
|
1346
|
+
? "pinned-cli"
|
|
1347
|
+
: readEnvironmentVariable("MVN_CMD")
|
|
1348
|
+
? "MVN_CMD"
|
|
1349
|
+
: "MAVEN_CMD",
|
|
1022
1350
|
tool: "maven",
|
|
1023
1351
|
},
|
|
1024
|
-
reason:
|
|
1352
|
+
reason: pinnedToolSelected
|
|
1353
|
+
? `Selected Maven command ${mavenCmd} pinned via the CLI project type.`
|
|
1354
|
+
: `Selected Maven command from environment (${mavenCmd}).`,
|
|
1025
1355
|
});
|
|
1026
1356
|
} else if (readEnvironmentVariable("MAVEN_HOME")) {
|
|
1027
1357
|
mavenCmd = join(readEnvironmentVariable("MAVEN_HOME"), "bin", "mvn");
|
|
@@ -1042,6 +1372,16 @@ export function getMavenCommand(srcPath, rootPath) {
|
|
|
1042
1372
|
},
|
|
1043
1373
|
reason: "Falling back to Maven from PATH.",
|
|
1044
1374
|
});
|
|
1375
|
+
// A Maven from PATH is a sound outcome whenever its version satisfies
|
|
1376
|
+
// the wrapper pin, so the fallback is recorded as costing nothing.
|
|
1377
|
+
recordLedgerEvent(LEDGER_EVENT_KINDS.FALLBACK_ENGAGED, {
|
|
1378
|
+
ecosystem: "java",
|
|
1379
|
+
tool: "maven",
|
|
1380
|
+
source: "PATH",
|
|
1381
|
+
impact: LEDGER_EVENT_IMPACTS.NONE,
|
|
1382
|
+
detail:
|
|
1383
|
+
"No wrapper, MVN_CMD/MAVEN_CMD, or MAVEN_HOME was found; Maven will run from PATH.",
|
|
1384
|
+
});
|
|
1045
1385
|
}
|
|
1046
1386
|
}
|
|
1047
1387
|
return mavenCmd;
|
|
@@ -14,6 +14,7 @@ import ssri from "ssri";
|
|
|
14
14
|
import { parse as _load, parseAllDocuments } from "yaml";
|
|
15
15
|
|
|
16
16
|
import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
|
|
17
|
+
import { recordDegradation } from "../core/buildLedger.js";
|
|
17
18
|
import { shouldFetchPackageMetadata } from "../core/env.js";
|
|
18
19
|
import { safeExistsSync } from "../core/fs.js";
|
|
19
20
|
import { analyzeSuspiciousJsFile } from "../inventory/analyzer.js";
|
|
@@ -1485,6 +1486,14 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
1485
1486
|
tree = await arb.loadVirtual();
|
|
1486
1487
|
}
|
|
1487
1488
|
} catch (e) {
|
|
1489
|
+
recordDegradation("js.lockfile-version", {
|
|
1490
|
+
ecosystem: "npm",
|
|
1491
|
+
tool: "npm",
|
|
1492
|
+
impact: "transitive-deps",
|
|
1493
|
+
path: pkgLockFile,
|
|
1494
|
+
detail:
|
|
1495
|
+
"The package-lock.json could not be parsed with the current npm dependency-resolution semantics, so the legacy parser is used instead.",
|
|
1496
|
+
});
|
|
1488
1497
|
console.log(e);
|
|
1489
1498
|
console.log(
|
|
1490
1499
|
`Unable to parse ${pkgLockFile} without legacy peer dependencies. Retrying ...`,
|
|
@@ -1499,6 +1508,15 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
|
|
|
1499
1508
|
});
|
|
1500
1509
|
tree = await arb.loadVirtual();
|
|
1501
1510
|
} catch (e) {
|
|
1511
|
+
recordDegradation("js.lockfile-version", {
|
|
1512
|
+
ecosystem: "npm",
|
|
1513
|
+
tool: "npm",
|
|
1514
|
+
kind: "evidence.degraded",
|
|
1515
|
+
impact: "transitive-deps",
|
|
1516
|
+
path: pkgLockFile,
|
|
1517
|
+
detail:
|
|
1518
|
+
"The package-lock.json could not be parsed in either npm resolution mode, so the resulting SBOM is incomplete.",
|
|
1519
|
+
});
|
|
1502
1520
|
console.log(e);
|
|
1503
1521
|
console.log(
|
|
1504
1522
|
`Unable to parse ${pkgLockFile} in legacy and non-legacy mode. The resulting SBOM would be incomplete.`,
|
|
@@ -3104,6 +3122,14 @@ export async function parsePnpmLock(
|
|
|
3104
3122
|
try {
|
|
3105
3123
|
yamlObj = yamlObj[yamlObj.length - 1].toJS();
|
|
3106
3124
|
} catch (_e) {
|
|
3125
|
+
recordDegradation("js.pnpm-lock-unparseable", {
|
|
3126
|
+
ecosystem: "npm",
|
|
3127
|
+
tool: "pnpm",
|
|
3128
|
+
impact: "transitive-deps",
|
|
3129
|
+
path: pnpmLock,
|
|
3130
|
+
detail:
|
|
3131
|
+
"The pnpm-lock.yaml could not be parsed, so no locked versions were captured from it.",
|
|
3132
|
+
});
|
|
3107
3133
|
console.log(`Unable to parse the pnpm lock file ${pnpmLock}.`);
|
|
3108
3134
|
return {};
|
|
3109
3135
|
}
|