@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.
Files changed (170) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -18,6 +18,7 @@ import {
18
18
  recordActivity,
19
19
  recordSymlinkResolution,
20
20
  } from "../core/activity.js";
21
+ import { recordDegradation } from "../core/buildLedger.js";
21
22
  import { extractPathEnv } from "../core/env.js";
22
23
  import {
23
24
  getTmpDir,
@@ -37,7 +38,13 @@ import {
37
38
  extractToolRefs,
38
39
  } from "../inventory/evidenceUtils.js";
39
40
  import { createGtfoBinsProperties } from "../inventory/gtfobins.js";
40
- import { OS_DISTRO_ALIAS, readOsRelease } from "../inventory/osinfo.js";
41
+ import {
42
+ canonicalDistroQualifier,
43
+ isCleanDistroQualifier,
44
+ OS_DISTRO_ALIAS,
45
+ OS_NAMESPACE_ALIAS,
46
+ readOsRelease,
47
+ } from "../inventory/osinfo.js";
41
48
  import {
42
49
  collectExecutables,
43
50
  collectSharedLibs,
@@ -57,13 +64,6 @@ import { getDirs } from "./containerutils.js";
57
64
 
58
65
  const isWin = _platform() === "win32";
59
66
  const OS_PURL_TYPES = ["deb", "apk", "rpm", "alpm", "qpkg"];
60
- // Canonical distro vendor namespace. Trivy emits pkg:rpm/alma/... but the
61
- // vulnerability feeds (AppThreat vuln-list / dep-scan) key AlmaLinux under
62
- // "almalinux"; align so the purls match. Rocky is emitted as "rocky" already.
63
- const OS_DISTRO_NAMESPACE_ALIAS = {
64
- alma: "almalinux",
65
- "rocky-linux": "rocky",
66
- };
67
67
  const pluginRuntime = setPluginsPathEnv(resolveCdxgenPlugins());
68
68
  const platform = pluginRuntime.platform;
69
69
  const CDXGEN_PLUGINS_DIR = pluginRuntime.pluginsDir;
@@ -555,6 +555,173 @@ export function executeSourcekitten(args) {
555
555
  return undefined;
556
556
  }
557
557
 
558
+ /**
559
+ * Canonicalise the distro vendor namespace of an OS package component
560
+ * produced by trivy, regardless of whether trivy expressed the vendor via the
561
+ * purl namespace or the component group (it is inconsistent between
562
+ * packages). e.g. pkg:rpm/alma/... -> pkg:rpm/almalinux/... and
563
+ * pkg:rpm/cbl-mariner/... -> pkg:rpm/azure-linux/... so the purls match the
564
+ * vulnerability feeds. Also aligns the ``distro`` qualifier vendor prefix
565
+ * (alma-9.8 -> almalinux-9.8, mariner-2.0 -> azure-linux-2.0), including
566
+ * spellings that disagree with the namespace, and rebuilds ``group`` and
567
+ * ``bom-ref`` from the rewritten purl.
568
+ *
569
+ * The namespace table is OS_NAMESPACE_ALIAS from lib/inventory/osinfo.js so
570
+ * the trivy path and the os-release path (getDistroInfo) cannot drift apart.
571
+ *
572
+ * A `distro` qualifier that is not purl-safe (trivy emits Amazon Linux as
573
+ * `amazon-2023.12.20260727+(Amazon Linux)`) is replaced with the os-release
574
+ * derived value, or dropped when there is none: an unusable qualifier
575
+ * invalidates the purl and fails validation for the whole BOM.
576
+ *
577
+ * @param {Object} comp trivy OS package component (mutated in place)
578
+ * @param {Object} purlObj parsed PackageURL of comp.purl (mutated in place)
579
+ * @param {string} name component package name (basename form)
580
+ * @param {string} group component group derived from the trivy component name
581
+ * @param {string} [distroId] canonical os-release distro id, e.g. "amazon-2023"
582
+ * @returns {string} the canonicalised group
583
+ */
584
+ export function canonicaliseOsPackageNamespace(
585
+ comp,
586
+ purlObj,
587
+ name,
588
+ group,
589
+ distroId,
590
+ ) {
591
+ if (!purlObj || !OS_PURL_TYPES.includes(purlObj.type)) {
592
+ return group;
593
+ }
594
+ const os_ns = purlObj.namespace || group || "";
595
+ const os_ns_alias = OS_NAMESPACE_ALIAS[os_ns];
596
+ const distroQual = purlObj.qualifiers?.distro;
597
+ let canonicalDistroQual = distroQual;
598
+ if (os_ns_alias && distroQual?.startsWith(`${os_ns}-`)) {
599
+ canonicalDistroQual = `${os_ns_alias}-${distroQual.slice(os_ns.length + 1)}`;
600
+ }
601
+ canonicalDistroQual = canonicalDistroQualifier(canonicalDistroQual);
602
+ if (canonicalDistroQual && !isCleanDistroQualifier(canonicalDistroQual)) {
603
+ canonicalDistroQual = isCleanDistroQualifier(distroId)
604
+ ? distroId
605
+ : undefined;
606
+ }
607
+ if (!os_ns_alias && canonicalDistroQual === distroQual) {
608
+ return group;
609
+ }
610
+ group = os_ns_alias || purlObj.namespace || group || "";
611
+ comp.group = group;
612
+ purlObj.namespace = group;
613
+ if (canonicalDistroQual !== distroQual) {
614
+ if (canonicalDistroQual) {
615
+ purlObj.qualifiers.distro = canonicalDistroQual;
616
+ } else {
617
+ delete purlObj.qualifiers.distro;
618
+ }
619
+ }
620
+ comp.purl = build({
621
+ type: purlObj.type,
622
+ namespace: purlObj.namespace || null,
623
+ name: name,
624
+ version: purlObj.version || null,
625
+ qualifiers: purlObj.qualifiers || null,
626
+ subpath: purlObj.subpath || null,
627
+ });
628
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
629
+ return group;
630
+ }
631
+
632
+ /**
633
+ * Repair a `distro` qualifier that cannot survive purl parsing, in place.
634
+ *
635
+ * trivy stamps Amazon Linux with the full pretty version — the qualifier
636
+ * arrives as `distro=amazon-2023.12.20260727+%28Amazon+Linux%29`, whose spaces
637
+ * and parentheses `Purl.parse` rejects outright ("Invalid character in
638
+ * qualifier distro"). The component then bypasses every canonicalisation below
639
+ * (they all need a parsed purl) and reaches the BOM verbatim, where schema
640
+ * validation fails and the *entire* SBOM is discarded — an Amazon Linux scan
641
+ * produces no output at all today.
642
+ *
643
+ * The release is already known from os-release, so the unusable value is
644
+ * swapped for it; with no usable replacement the qualifier is dropped, since a
645
+ * missing qualifier costs one matching hint while an invalid one costs the
646
+ * whole document.
647
+ *
648
+ * @param {Object} comp component whose purl may carry an unusable qualifier
649
+ * @param {string} [distroId] canonical os-release distro id, e.g. "amazon-2023"
650
+ * @returns {Object} the same component
651
+ */
652
+ export function repairOsComponentPurl(comp, distroId) {
653
+ const purl = comp?.purl;
654
+ if (!purl?.includes("distro=")) {
655
+ return comp;
656
+ }
657
+ const match = /([?&]distro=)([^&]*)/.exec(purl);
658
+ if (!match) {
659
+ return comp;
660
+ }
661
+ let decoded = match[2];
662
+ try {
663
+ decoded = decodeURIComponent(match[2].replace(/\+/g, " "));
664
+ } catch (_err) {
665
+ // A value that cannot even be decoded is definitely not purl-safe
666
+ }
667
+ if (isCleanDistroQualifier(decoded)) {
668
+ return comp;
669
+ }
670
+ const replacement = isCleanDistroQualifier(distroId)
671
+ ? `${match[1]}${distroId}`
672
+ : "";
673
+ comp.purl = purl.replace(match[0], replacement).replace(/\?&/, "?");
674
+ if (comp.purl.endsWith("?")) {
675
+ comp.purl = comp.purl.slice(0, -1);
676
+ }
677
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
678
+ return comp;
679
+ }
680
+
681
+ /**
682
+ * Drop source-package shadow components that duplicate a real installed
683
+ * package, in place.
684
+ *
685
+ * A shadow is emitted whenever an rpm/deb declares a source name different
686
+ * from its own (bzip2-libs -> bzip2) so that scanners keying on source names
687
+ * still match. When the source package is *itself* installed at the same
688
+ * version, the shadow's purl is identical to the real component's and the two
689
+ * collapse in trimComponents() — which keeps whichever was seen first and
690
+ * never merges `tags`. Seen-first is trivy's listing order, so the outcome is
691
+ * arbitrary: when the shadow wins, a genuinely installed package is reported
692
+ * with `tags: ["source"]` and identity confidence 0, the very marker the
693
+ * shadow carries so consumers can filter shadows out. That turns a real
694
+ * package into a filtered-away one.
695
+ *
696
+ * Dropping the redundant shadow is loss-free: it duplicates the real
697
+ * component's purl (so nothing that matches on purl changes) and its bom-ref
698
+ * is byte-identical, so no dependency reference can dangle.
699
+ *
700
+ * @param {Array} pkgList component list, mutated in place
701
+ * @returns {Array} the same list, for convenience
702
+ */
703
+ export function dropRedundantSourceComponents(pkgList) {
704
+ if (!Array.isArray(pkgList) || !pkgList.length) {
705
+ return pkgList;
706
+ }
707
+ const installedPurls = new Set();
708
+ for (const comp of pkgList) {
709
+ if (comp?.purl && !comp?.tags?.includes("source")) {
710
+ installedPurls.add(comp.purl);
711
+ }
712
+ }
713
+ if (!installedPurls.size) {
714
+ return pkgList;
715
+ }
716
+ for (let i = pkgList.length - 1; i >= 0; i--) {
717
+ const comp = pkgList[i];
718
+ if (comp?.tags?.includes("source") && installedPurls.has(comp.purl)) {
719
+ pkgList.splice(i, 1);
720
+ }
721
+ }
722
+ return pkgList;
723
+ }
724
+
558
725
  /**
559
726
  * Get the packages installed in the container image filesystem.
560
727
  *
@@ -613,6 +780,16 @@ export async function getOSPackages(src, imageConfig, options = {}) {
613
780
  .filter(Boolean),
614
781
  ),
615
782
  ).sort();
783
+ if (binPaths.length) {
784
+ recordDegradation("binary.paths.inferred", {
785
+ ecosystem: "generic",
786
+ tool: "trivy",
787
+ impact: "none",
788
+ path: src,
789
+ detail:
790
+ "The image config carried no PATH, so executable directories were inferred by scanning the filesystem.",
791
+ });
792
+ }
616
793
  if (DEBUG_MODE && binPaths.length) {
617
794
  console.log(
618
795
  `Falling back to inferred binary paths for ${src}: ${binPaths.join(", ")}`,
@@ -721,9 +898,14 @@ export async function getOSPackages(src, imageConfig, options = {}) {
721
898
  }
722
899
  }
723
900
  break;
901
+ case "alpaquita":
902
+ purl_type = "apk";
903
+ break;
724
904
  default:
725
905
  if (distro_id_like.includes("debian")) {
726
906
  purl_type = "deb";
907
+ } else if (distro_id_like.includes("alpine")) {
908
+ purl_type = "apk";
727
909
  } else if (
728
910
  distro_id_like.includes("rhel") ||
729
911
  distro_id_like.includes("centos") ||
@@ -739,6 +921,11 @@ export async function getOSPackages(src, imageConfig, options = {}) {
739
921
  distro_codename = OS_DISTRO_ALIAS[distro_id];
740
922
  }
741
923
  }
924
+ // Canonicalise the distro qualifier vendor (e.g. "mariner-2.0" ->
925
+ // "azure-linux-2.0") so every spelling of one release yields a single
926
+ // qualifier value. This runs before distro_id is used to overwrite
927
+ // trivy's own qualifier below.
928
+ distro_id = canonicalDistroQualifier(distro_id);
742
929
  const tmpDependencies = {};
743
930
  tools = enrichToolComponents(
744
931
  (Array.isArray(tmpBom?.metadata?.tools)
@@ -768,42 +955,23 @@ export async function getOSPackages(src, imageConfig, options = {}) {
768
955
  comp.group = group;
769
956
  comp.name = name;
770
957
  const modularityLabel = extractModularityLabel(comp);
958
+ repairOsComponentPurl(comp, distro_id);
771
959
  try {
772
960
  purlObj = Purl.parse(comp.purl);
773
961
  purlObj.qualifiers = purlObj.qualifiers || {};
774
962
  } catch (_err) {
775
963
  // continue regardless of error
776
964
  }
777
- // Canonicalise the distro vendor namespace for every OS component,
778
- // regardless of whether trivy expressed the vendor via the purl
779
- // namespace or the component group (it is inconsistent between
780
- // packages). e.g. pkg:rpm/alma/... -> pkg:rpm/almalinux/... so the
781
- // purls match the vulnerability feeds. Also aligns the ``distro``
782
- // qualifier vendor prefix (alma-9.8 -> almalinux-9.8).
783
- if (purlObj && OS_PURL_TYPES.includes(purlObj.type)) {
784
- const os_ns = purlObj.namespace || group || "";
785
- const os_ns_alias = OS_DISTRO_NAMESPACE_ALIAS[os_ns];
786
- if (os_ns_alias) {
787
- group = os_ns_alias;
788
- comp.group = os_ns_alias;
789
- purlObj.namespace = os_ns_alias;
790
- const distroQual = purlObj.qualifiers?.distro;
791
- if (distroQual?.startsWith(`${os_ns}-`)) {
792
- purlObj.qualifiers.distro = `${os_ns_alias}-${distroQual.slice(
793
- os_ns.length + 1,
794
- )}`;
795
- }
796
- comp.purl = build({
797
- type: purlObj.type,
798
- namespace: os_ns_alias || null,
799
- name: name,
800
- version: purlObj.version || null,
801
- qualifiers: purlObj.qualifiers || null,
802
- subpath: purlObj.subpath || null,
803
- });
804
- comp["bom-ref"] = decodeURIComponent(comp.purl);
805
- }
806
- }
965
+ // Canonicalise the distro vendor namespace (see
966
+ // canonicaliseOsPackageNamespace) before any later bom-ref
967
+ // rebuild so the rewritten purl, group and bom-ref stay in sync.
968
+ group = canonicaliseOsPackageNamespace(
969
+ comp,
970
+ purlObj,
971
+ name,
972
+ group,
973
+ distro_id,
974
+ );
807
975
  if (
808
976
  group === "" &&
809
977
  purlObj &&
@@ -812,8 +980,7 @@ export async function getOSPackages(src, imageConfig, options = {}) {
812
980
  try {
813
981
  if (purlObj?.namespace && purlObj.namespace !== "") {
814
982
  group =
815
- OS_DISTRO_NAMESPACE_ALIAS[purlObj.namespace] ||
816
- purlObj.namespace;
983
+ OS_NAMESPACE_ALIAS[purlObj.namespace] || purlObj.namespace;
817
984
  comp.group = group;
818
985
  purlObj.namespace = group;
819
986
  }
@@ -1107,6 +1274,7 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1107
1274
  }
1108
1275
  }
1109
1276
  }
1277
+ dropRedundantSourceComponents(pkgList);
1110
1278
  const rootfsRepositoryInventory = await collectRootfsRepositoryInventory(
1111
1279
  src,
1112
1280
  options,
@@ -3144,6 +3312,14 @@ export function getDotnetSlices(src, slicesFile) {
3144
3312
  "You must install or update .NET to run this application",
3145
3313
  )
3146
3314
  ) {
3315
+ recordDegradation("dotnet.sdk.missing", {
3316
+ ecosystem: "csharp",
3317
+ tool: "dotnet",
3318
+ impact: "components",
3319
+ command: `dosai ${args.join(" ")}`,
3320
+ detail:
3321
+ "dosai reported that no .NET SDK is installed, so dotnet-related evidence for this image is incomplete.",
3322
+ });
3147
3323
  console.log(
3148
3324
  "Dotnet SDK is not installed. Please use the cdxgen dotnet container images to generate slices for this project.",
3149
3325
  );
@@ -21,6 +21,7 @@ import {
21
21
  recordDecisionActivity,
22
22
  recordSensitiveFileRead,
23
23
  } from "../core/activity.js";
24
+ import { deferFailOnError } from "../core/deferredExit.js";
24
25
  import { extractPathEnv } from "../core/env.js";
25
26
  import {
26
27
  getAllFiles,
@@ -1407,7 +1408,11 @@ export const extractTar = async (fullImageName, dir, options) => {
1407
1408
  } else if (DEBUG_MODE) {
1408
1409
  console.log(err.code, "is not handled yet in extractTar method.");
1409
1410
  }
1410
- options.failOnError && process.exit(1);
1411
+ deferFailOnError(options, {
1412
+ ecosystem: "container",
1413
+ tool: "docker",
1414
+ detail: "extracting the image archive failed",
1415
+ });
1411
1416
  return false;
1412
1417
  }
1413
1418
  };
@@ -1598,10 +1603,18 @@ export const exportArchive = async (fullImageName, options = {}) => {
1598
1603
  return exportData;
1599
1604
  }
1600
1605
  console.log(`Unable to extract image archive to ${tempDir}`);
1601
- options.failOnError && process.exit(1);
1606
+ deferFailOnError(options, {
1607
+ ecosystem: "container",
1608
+ tool: "docker",
1609
+ detail: "the image archive could not be extracted",
1610
+ });
1602
1611
  } catch (_err) {
1603
1612
  // ignore
1604
- options.failOnError && process.exit(1);
1613
+ deferFailOnError(options, {
1614
+ ecosystem: "container",
1615
+ tool: "docker",
1616
+ detail: "the image archive could not be extracted",
1617
+ });
1605
1618
  }
1606
1619
  return undefined;
1607
1620
  };
@@ -1686,7 +1699,11 @@ export const extractFromManifest = async (
1686
1699
  }
1687
1700
  } else {
1688
1701
  console.log(err);
1689
- options.failOnError && process.exit(1);
1702
+ deferFailOnError(options, {
1703
+ ecosystem: "container",
1704
+ tool: "docker",
1705
+ detail: "reading the image manifest failed",
1706
+ });
1690
1707
  }
1691
1708
  }
1692
1709
  }
@@ -1713,7 +1730,11 @@ export const extractFromManifest = async (
1713
1730
  ? join(allLayersExplodedDir, lastLayerConfig.config.WorkingDir)
1714
1731
  : "";
1715
1732
  } catch (_err) {
1716
- options.failOnError && process.exit(1);
1733
+ deferFailOnError(options, {
1734
+ ecosystem: "container",
1735
+ tool: "docker",
1736
+ detail: "reading the image manifest failed",
1737
+ });
1717
1738
  }
1718
1739
  }
1719
1740
  }
@@ -520,7 +520,7 @@ const start = (options) => {
520
520
  ensureAiProvenanceProperties(bomNSData.bomJson, reqOptions);
521
521
  await ensureAiOversightProperties(bomNSData.bomJson, reqOptions);
522
522
  }
523
- bomNSData = postProcess(
523
+ bomNSData = await postProcess(
524
524
  bomNSData,
525
525
  { ...reqOptions, executeOsQuery },
526
526
  srcDir,
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shared assembly of CycloneDX document-level annotations.
3
+ *
4
+ * Two postgen features attach findings to the BOM document as annotations
5
+ * whose `subjects` is the BOM serial number: the BOM audit's rule findings and
6
+ * the build introspection's remediations. Both describe the document rather
7
+ * than a component, both are annotated by the cdxgen tool component from
8
+ * metadata, and both carry the document's own timestamp because an annotation
9
+ * describes the document it is attached to. The structured facts travel as
10
+ * `name`/`value` pairs rendered into the annotation text, one `:evidence:<key>`
11
+ * entry per evidence item.
12
+ */
13
+
14
+ import { DEBUG_MODE } from "../../core/activity.js";
15
+ import { getTimestamp } from "../../core/fs.js";
16
+ import { buildAnnotationText } from "../../helpers/annotationFormatter.js";
17
+
18
+ /**
19
+ * Find the cdxgen tool component that annotates the document.
20
+ *
21
+ * @param {Object} bomJson CycloneDX BOM
22
+ * @returns {Object[]} The cdxgen tool components, empty when metadata carries none.
23
+ */
24
+ function findCdxgenAnnotator(bomJson) {
25
+ return (
26
+ bomJson?.metadata?.tools?.components?.filter((c) => c.name === "cdxgen") ||
27
+ []
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Flatten an evidence object into `:evidence:<key>` property entries. Nested
33
+ * values are serialized as JSON so every property value stays a string.
34
+ *
35
+ * @param {string} propertyNamePrefix Property namespace, e.g. "cdx:audit".
36
+ * @param {Object} evidence Arbitrary key/value evidence facts.
37
+ * @returns {{name: string, value: string}[]} Property entries.
38
+ */
39
+ export function evidenceProperties(propertyNamePrefix, evidence) {
40
+ const properties = [];
41
+ if (evidence && typeof evidence === "object") {
42
+ for (const [key, value] of Object.entries(evidence)) {
43
+ const propValue =
44
+ typeof value === "object" ? JSON.stringify(value) : String(value);
45
+ properties.push({
46
+ name: `${propertyNamePrefix}:evidence:${key}`,
47
+ value: propValue,
48
+ });
49
+ }
50
+ }
51
+ return properties;
52
+ }
53
+
54
+ /**
55
+ * Build one document-level annotation. Returns undefined when the BOM carries
56
+ * no cdxgen tool component to annotate with; the caller decides whether that
57
+ * is worth a warning.
58
+ *
59
+ * @param {Object} params Annotation inputs.
60
+ * @param {Object} params.bomJson CycloneDX BOM the annotation describes.
61
+ * @param {string} params.message Leading annotation sentence.
62
+ * @param {{name: string, value: string}[]} params.properties Structured facts rendered into the annotation text.
63
+ * @returns {Object|undefined} CycloneDX annotation, or undefined without an annotator.
64
+ */
65
+ export function buildDocumentAnnotation({ bomJson, message, properties }) {
66
+ const cdxgenAnnotator = findCdxgenAnnotator(bomJson);
67
+ if (!cdxgenAnnotator.length) {
68
+ return undefined;
69
+ }
70
+ return {
71
+ subjects: [bomJson.serialNumber],
72
+ annotator: {
73
+ component: cdxgenAnnotator[0],
74
+ },
75
+ // An annotation describes the document it is attached to, so it carries
76
+ // that document's timestamp rather than the moment the annotation was
77
+ // built.
78
+ timestamp: bomJson.metadata?.timestamp || getTimestamp(),
79
+ text: buildAnnotationText(message, properties),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Warn that a document could not be annotated because it carries no cdxgen
85
+ * tool component. Suppressed outside debug mode, where the missing annotator
86
+ * is an expected shape for foreign BOMs.
87
+ *
88
+ * @param {string} feature Feature that wanted to annotate.
89
+ * @returns {void}
90
+ */
91
+ export function warnMissingAnnotator(feature) {
92
+ if (DEBUG_MODE) {
93
+ console.warn(
94
+ `Cannot create ${feature} annotations: cdxgen tool component not found in metadata`,
95
+ );
96
+ }
97
+ }