@cdxgen/cdxgen 13.0.1 → 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.
Files changed (155) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +125 -4
  4. package/bin/evinse.js +1 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/frameworks-list.json +86 -1
  7. package/data/remediations.json +1144 -0
  8. package/data/rules/asar-archives.yaml +1 -1
  9. package/data/rules/build-fidelity.yaml +782 -0
  10. package/data/rules/package-integrity.yaml +13 -13
  11. package/lib/audit/index.js +48 -4
  12. package/lib/audit/scoring.js +2 -8
  13. package/lib/cli/bomAssembly.js +20 -1
  14. package/lib/cli/cliOptions.js +94 -7
  15. package/lib/cli/index.js +76 -48
  16. package/lib/cli/jsBom.js +32 -3
  17. package/lib/cli/jvmBom.js +1494 -1315
  18. package/lib/cli/managedBom.js +97 -28
  19. package/lib/cli/nativeBom.js +189 -10
  20. package/lib/core/buildLedger.js +799 -0
  21. package/lib/core/deferredExit.js +141 -0
  22. package/lib/core/env.js +69 -1
  23. package/lib/core/fs.js +60 -0
  24. package/lib/core/logger.js +12 -99
  25. package/lib/core/severity.js +16 -0
  26. package/lib/core/syncFileWriter.js +106 -0
  27. package/lib/ecosystems/ecosystems.js +9 -0
  28. package/lib/ecosystems/gradleutils.js +355 -15
  29. package/lib/ecosystems/parsers-js.js +26 -0
  30. package/lib/ecosystems/parsers-jvm.js +9 -0
  31. package/lib/ecosystems/parsers-misc.js +16 -2
  32. package/lib/ecosystems/parsers-python.js +171 -35
  33. package/lib/ecosystems/parsers-rust.js +12 -2
  34. package/lib/ecosystems/pypiClassifiers.js +326 -0
  35. package/lib/ecosystems/pythonutils.js +49 -1
  36. package/lib/ecosystems/rubyutils.js +13 -0
  37. package/lib/evinser/evinser.js +30 -12
  38. package/lib/helpers/remote/dependency-track.js +81 -14
  39. package/lib/inventory/auditCategories.js +9 -0
  40. package/lib/inventory/depsUtils.js +72 -0
  41. package/lib/inventory/dosai.js +431 -22
  42. package/lib/inventory/envcontext.js +206 -18
  43. package/lib/inventory/fetchBatch.js +57 -0
  44. package/lib/inventory/jvmToolEnv.js +593 -0
  45. package/lib/inventory/osinfo.js +1 -0
  46. package/lib/inventory/toolRequirements.js +903 -0
  47. package/lib/managers/binary.js +24 -0
  48. package/lib/managers/docker.js +26 -5
  49. package/lib/server/server.js +1 -1
  50. package/lib/stages/postgen/annotationBuilder.js +97 -0
  51. package/lib/stages/postgen/auditBom.js +76 -79
  52. package/lib/stages/postgen/introspection/annotate.js +320 -0
  53. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  54. package/lib/stages/postgen/introspection/emit.js +192 -0
  55. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  56. package/lib/stages/postgen/introspection/markers.js +227 -0
  57. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  58. package/lib/stages/postgen/introspection/report.js +1208 -0
  59. package/lib/stages/postgen/introspection/score.js +1067 -0
  60. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  61. package/lib/stages/postgen/parentEdge.js +156 -0
  62. package/lib/stages/postgen/postgen.js +115 -2
  63. package/lib/stages/postgen/ruleEngine.js +17 -0
  64. package/lib/stages/pregen/envAudit.js +42 -2
  65. package/lib/stages/pregen/pregen.js +1171 -6
  66. package/lib/validator/bomValidator.js +11 -2
  67. package/package.json +14 -14
  68. package/types/lib/audit/index.d.ts.map +1 -1
  69. package/types/lib/audit/scoring.d.ts +2 -8
  70. package/types/lib/audit/scoring.d.ts.map +1 -1
  71. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  72. package/types/lib/cli/cliOptions.d.ts +2 -1
  73. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  74. package/types/lib/cli/index.d.ts +8 -0
  75. package/types/lib/cli/index.d.ts.map +1 -1
  76. package/types/lib/cli/jsBom.d.ts.map +1 -1
  77. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  78. package/types/lib/cli/managedBom.d.ts.map +1 -1
  79. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  80. package/types/lib/core/buildLedger.d.ts +272 -0
  81. package/types/lib/core/buildLedger.d.ts.map +1 -0
  82. package/types/lib/core/deferredExit.d.ts +84 -0
  83. package/types/lib/core/deferredExit.d.ts.map +1 -0
  84. package/types/lib/core/env.d.ts +22 -1
  85. package/types/lib/core/env.d.ts.map +1 -1
  86. package/types/lib/core/fs.d.ts +21 -0
  87. package/types/lib/core/fs.d.ts.map +1 -1
  88. package/types/lib/core/logger.d.ts +2 -11
  89. package/types/lib/core/logger.d.ts.map +1 -1
  90. package/types/lib/core/severity.d.ts +11 -0
  91. package/types/lib/core/severity.d.ts.map +1 -0
  92. package/types/lib/core/syncFileWriter.d.ts +33 -0
  93. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  94. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  95. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  96. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  97. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  98. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  99. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  102. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  103. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  104. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  105. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  107. package/types/lib/evinser/evinser.d.ts +5 -1
  108. package/types/lib/evinser/evinser.d.ts.map +1 -1
  109. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  110. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  111. package/types/lib/inventory/auditCategories.d.ts +8 -0
  112. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  113. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  114. package/types/lib/inventory/dosai.d.ts +20 -0
  115. package/types/lib/inventory/dosai.d.ts.map +1 -1
  116. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  117. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  118. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  119. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  120. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  121. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  122. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  123. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  124. package/types/lib/managers/binary.d.ts.map +1 -1
  125. package/types/lib/managers/docker.d.ts.map +1 -1
  126. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  127. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  128. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  129. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  130. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  131. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  132. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  133. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  134. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  135. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  136. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  137. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  138. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  139. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  140. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  141. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  142. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  144. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  145. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  146. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  147. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  148. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  149. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  150. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  151. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  152. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  153. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  154. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  155. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -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
- // continue regardless of error
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 (rootPath && safeExistsSync(join(rootPath, findGradleFile))) {
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
- // continue regardless of error
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: `Selected Gradle command from GRADLE_CMD (${gradleCmd}).`,
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
- // continue regardless of error
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
- // continue regardless of error
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 (rootPath && safeExistsSync(join(rootPath, findMavenFile))) {
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
- // continue regardless of error
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: readEnvironmentVariable("MVN_CMD")
1020
- ? "MVN_CMD"
1021
- : "MAVEN_CMD",
1345
+ selectedSource: pinnedToolSelected
1346
+ ? "pinned-cli"
1347
+ : readEnvironmentVariable("MVN_CMD")
1348
+ ? "MVN_CMD"
1349
+ : "MAVEN_CMD",
1022
1350
  tool: "maven",
1023
1351
  },
1024
- reason: `Selected Maven command from environment (${mavenCmd}).`,
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
  }
@@ -5,6 +5,7 @@ import { build } from "@cdxgen/cdx-purl";
5
5
  import { parseEDNString } from "edn-data";
6
6
 
7
7
  import { readEnvironmentVariable } from "../core/activity.js";
8
+ import { recordDegradation } from "../core/buildLedger.js";
8
9
  import { includeMavenTestScope } from "../core/env.js";
9
10
  import { thoughtLog } from "../core/logger.js";
10
11
  import { applyPurl } from "../inventory/purl.js";
@@ -309,6 +310,14 @@ export function parseMavenTreeJson(rawOutput, pomFile) {
309
310
  try {
310
311
  rootNode = JSON.parse(rawOutput);
311
312
  } catch (_err) {
313
+ recordDegradation("jvm.maven.tree-unparseable", {
314
+ ecosystem: "java",
315
+ tool: "maven",
316
+ impact: "transitive-deps",
317
+ path: pomFile,
318
+ detail:
319
+ "The maven dependency:tree JSON output could not be parsed, so the text output is parsed instead.",
320
+ });
312
321
  thoughtLog("Unable to parse Maven dependency:tree JSON output");
313
322
  return {};
314
323
  }
@@ -8,7 +8,6 @@ import {
8
8
  relative,
9
9
  resolve,
10
10
  } from "node:path";
11
- import process from "node:process";
12
11
 
13
12
  import { build, Purl } from "@cdxgen/cdx-purl";
14
13
  import { parse as _load } from "yaml";
@@ -18,6 +17,8 @@ import {
18
17
  DEBUG_MODE,
19
18
  readEnvironmentVariable,
20
19
  } from "../core/activity.js";
20
+ import { recordDegradation } from "../core/buildLedger.js";
21
+ import { deferFailOnError } from "../core/deferredExit.js";
21
22
  import { shouldFetchLicense } from "../core/env.js";
22
23
  import {
23
24
  safeExistsSync,
@@ -2652,6 +2653,15 @@ export function executePodCommand(parameters, path, options) {
2652
2653
  );
2653
2654
  }
2654
2655
  if (readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true") {
2656
+ recordDegradation("cocoa.pods.missing", {
2657
+ ecosystem: "cocoa",
2658
+ tool: "pod",
2659
+ impact: "transitive-deps",
2660
+ command: `pod ${parameters.join(" ")}`,
2661
+ exitCode: typeof result.status === "number" ? result.status : undefined,
2662
+ detail:
2663
+ "The pod command did not succeed, so the cocoapods dependency graph was not resolved.",
2664
+ });
2655
2665
  console.log(
2656
2666
  "Consider using the cdxgen container image (`ghcr.io/cdxgen/cdxgen`), which includes cocoapods and additional build tools.",
2657
2667
  );
@@ -2667,7 +2677,11 @@ export function executePodCommand(parameters, path, options) {
2667
2677
  if (result.stderr) {
2668
2678
  console.log(result.stderr);
2669
2679
  }
2670
- options.failOnError && process.exit(1);
2680
+ deferFailOnError(options, {
2681
+ ecosystem: "cocoa",
2682
+ tool: "pod",
2683
+ detail: "the pod command failed",
2684
+ });
2671
2685
  }
2672
2686
  }
2673
2687
  return result;