@cdxgen/cdxgen 13.4.0 → 13.5.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 (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -6,7 +6,15 @@ import {
6
6
  statSync,
7
7
  } from "node:fs";
8
8
  import { platform as _platform, homedir } from "node:os";
9
- import { basename, dirname, extname, join, relative, resolve } from "node:path";
9
+ import {
10
+ basename,
11
+ dirname,
12
+ extname,
13
+ isAbsolute,
14
+ join,
15
+ relative,
16
+ resolve,
17
+ } from "node:path";
10
18
  import process from "node:process";
11
19
 
12
20
  import { build, Purl } from "@cdxgen/cdx-purl";
@@ -33,23 +41,41 @@ import {
33
41
  import { mapWithConcurrency } from "../core/parallel.js";
34
42
  import { isValidDriveRoot } from "../core/paths.js";
35
43
  import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
44
+ import {
45
+ CertificateCollector,
46
+ classifyCryptoFile,
47
+ } from "../inventory/certificates.js";
36
48
  import { createContainerRiskProperties } from "../inventory/containerRisk.js";
37
49
  import {
38
50
  attachIdentityTools,
39
51
  extractToolRefs,
40
52
  } from "../inventory/evidenceUtils.js";
53
+ import {
54
+ PRE_1_18_VERSION,
55
+ readGoBuildVersion,
56
+ } from "../inventory/goBuildInfo.js";
41
57
  import { createGtfoBinsProperties } from "../inventory/gtfobins.js";
42
58
  import {
43
59
  canonicalDistroQualifier,
60
+ getDistroInfo,
44
61
  isCleanDistroQualifier,
45
62
  OS_DISTRO_ALIAS,
46
63
  OS_NAMESPACE_ALIAS,
47
64
  readOsRelease,
48
65
  } from "../inventory/osinfo.js";
66
+ import { isRealArchitecture } from "../inventory/osPackagePurl.js";
49
67
  import {
50
68
  collectExecutables,
51
69
  collectSharedLibs,
52
70
  } from "../inventory/osPackageResolver.js";
71
+ import {
72
+ attachPhpBuild,
73
+ classifyPhpSapi,
74
+ createPhpRuntimeInventory,
75
+ MAX_PHP_PROBES,
76
+ packageStoreOf,
77
+ readPhpBuildVersion,
78
+ } from "../inventory/phpBuildInfo.js";
53
79
  import {
54
80
  resolveCdxgenPlugins,
55
81
  resolvePluginBinary,
@@ -60,11 +86,13 @@ import {
60
86
  parsePurlLoose,
61
87
  tryBuildPurl,
62
88
  } from "../inventory/purl.js";
89
+ import { collectRootfsCertificates } from "../inventory/rootfsCerts.js";
63
90
  import {
64
91
  adjustLicenseInformation,
65
92
  findLicenseId,
66
93
  isSpdxLicenseExpression,
67
94
  } from "../inventory/spdx.js";
95
+ import { isValidIdnEmail } from "../parsers/email.js";
68
96
  import { getDirs } from "./containerutils.js";
69
97
 
70
98
  const isWin = _platform() === "win32";
@@ -654,7 +682,35 @@ export function canonicaliseOsPackageNamespace(
654
682
  group,
655
683
  distroId,
656
684
  ) {
657
- if (!purlObj || !OS_PURL_TYPES.includes(purlObj.type)) {
685
+ if (!purlObj) {
686
+ return group;
687
+ }
688
+ // trivy invents a `bottlerocket` purl type for Bottlerocket host packages
689
+ // (trivy pkg/purl/purl.go, packageurlTypeBottlerocket); the purl spec has
690
+ // no such type — @cdxgen/cdx-purl has no generated type class for it, and
691
+ // no vulnerability feed stores under it. Rewrite to the form cdxgen's own
692
+ // os-release path produces for a Bottlerocket host: type rpm, namespace
693
+ // bottlerocket (the os-release ID; OS_NAMESPACE_ALIAS needs no entry).
694
+ // The distro qualifier trivy stamps (bottlerocket-<os version>) survives
695
+ // unchanged. The caller's group is returned as for any other rpm distro
696
+ // without an alias, so getOSPackages still records the rpm type and the
697
+ // distro in allTypes (oci:image:componentTypes).
698
+ if (purlObj.type === "bottlerocket") {
699
+ purlObj.type = "rpm";
700
+ purlObj.namespace = "bottlerocket";
701
+ comp.group = "bottlerocket";
702
+ comp.purl = build({
703
+ type: purlObj.type,
704
+ namespace: purlObj.namespace,
705
+ name: name,
706
+ version: purlObj.version || null,
707
+ qualifiers: purlObj.qualifiers || null,
708
+ subpath: purlObj.subpath || null,
709
+ });
710
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
711
+ return group;
712
+ }
713
+ if (!OS_PURL_TYPES.includes(purlObj.type)) {
658
714
  return group;
659
715
  }
660
716
  const os_ns = purlObj.namespace || group || "";
@@ -923,80 +979,20 @@ export async function getOSPackages(src, imageConfig, options = {}) {
923
979
  if (DEBUG_MODE) {
924
980
  console.log(osReleaseData);
925
981
  }
926
- // distro_name must be a real release codename/token (bookworm, jammy,
927
- // rhel-9, alpine-3.19), never a distro *display name*. VERSION_CODENAME is
928
- // authoritative when present; otherwise the RHEL/CentOS product-name
929
- // fallbacks are only accepted when they resolve through OS_DISTRO_ALIAS
930
- // (e.g. "red hat enterprise linux 9" -> "rhel-9"). Display names such as
931
- // "Rocky Linux" or "AlmaLinux" have no alias entry and are dropped so we
932
- // never emit a bogus distro_name qualifier (RPM distros track packages
933
- // flat, without a release segment, so folding a display name breaks
934
- // downstream vuln-db matching).
935
- let distro_codename = (
936
- osReleaseData["VERSION_CODENAME"] || ""
937
- ).toLowerCase();
938
- if (!distro_codename) {
939
- const distro_display_name = (
940
- osReleaseData["CENTOS_MANTISBT_PROJECT"] ||
941
- osReleaseData["REDHAT_BUGZILLA_PRODUCT"] ||
942
- osReleaseData["REDHAT_SUPPORT_PRODUCT"] ||
943
- ""
944
- ).toLowerCase();
945
- if (distro_display_name && OS_DISTRO_ALIAS[distro_display_name]) {
946
- distro_codename = OS_DISTRO_ALIAS[distro_display_name];
947
- }
948
- }
949
- let distro_id = osReleaseData["ID"] || "";
950
- const distro_id_like = osReleaseData["ID_LIKE"] || "";
951
- let purl_type = "rpm";
952
- switch (distro_id) {
953
- case "debian":
954
- case "ubuntu":
955
- case "pop":
956
- purl_type = "deb";
957
- break;
958
- case "sles":
959
- case "suse":
960
- case "opensuse":
961
- purl_type = "rpm";
962
- break;
963
- case "alpine":
964
- purl_type = "apk";
965
- if (osReleaseData.VERSION_ID) {
966
- const versionParts = osReleaseData["VERSION_ID"].split(".");
967
- if (versionParts.length >= 2) {
968
- distro_codename = `alpine-${versionParts[0]}.${versionParts[1]}`;
969
- }
970
- }
971
- break;
972
- case "alpaquita":
973
- purl_type = "apk";
974
- break;
975
- default:
976
- if (distro_id_like.includes("debian")) {
977
- purl_type = "deb";
978
- } else if (distro_id_like.includes("alpine")) {
979
- purl_type = "apk";
980
- } else if (
981
- distro_id_like.includes("rhel") ||
982
- distro_id_like.includes("centos") ||
983
- distro_id_like.includes("fedora")
984
- ) {
985
- purl_type = "rpm";
986
- }
987
- break;
988
- }
989
- if (osReleaseData["VERSION_ID"]) {
990
- distro_id = `${distro_id}-${osReleaseData["VERSION_ID"]}`;
991
- if (OS_DISTRO_ALIAS[distro_id]) {
992
- distro_codename = OS_DISTRO_ALIAS[distro_id];
993
- }
994
- }
995
- // Canonicalise the distro qualifier vendor (e.g. "mariner-2.0" ->
996
- // "azure-linux-2.0") so every spelling of one release yields a single
997
- // qualifier value. This runs before distro_id is used to overwrite
998
- // trivy's own qualifier below.
999
- distro_id = canonicalDistroQualifier(distro_id);
982
+ // The distro of the rootfs is decided in getDistroInfo, which an OBOM run
983
+ // on the same system reads too, so a package gets the same qualifiers
984
+ // from either scan. distro_name is a real release codename/token
985
+ // (bookworm, jammy, rhel, alpine-3.24), never a distro *display name*:
986
+ // "Rocky Linux" or "AlmaLinux" is dropped, since RPM distros track
987
+ // packages flat and a display name there breaks downstream vuln-db
988
+ // matching. distro is ID-VERSION_ID with the vendor canonicalised (e.g.
989
+ // "mariner-2.0" -> "azure-linux-2.0"), so every spelling of one release
990
+ // yields a single qualifier value; it overwrites trivy's own qualifier
991
+ // below.
992
+ const distroInfo = getDistroInfo(src);
993
+ let distro_codename = distroInfo.distroName;
994
+ const distro_id = distroInfo.distroId;
995
+ const purl_type = distroInfo.purlType;
1000
996
  const tmpDependencies = {};
1001
997
  tools = enrichToolComponents(
1002
998
  (Array.isArray(tmpBom?.metadata?.tools)
@@ -1030,6 +1026,27 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1030
1026
  try {
1031
1027
  purlObj = Purl.parse(comp.purl);
1032
1028
  purlObj.qualifiers = purlObj.qualifiers || {};
1029
+ if (
1030
+ OS_PURL_TYPES.includes(purlObj.type) &&
1031
+ "arch" in purlObj.qualifiers &&
1032
+ !isRealArchitecture(purlObj.qualifiers.arch)
1033
+ ) {
1034
+ // trivy writes arch=None for a package with no architecture,
1035
+ // such as an rpm's gpg-pubkey; a package has the architecture
1036
+ // it has, or none, as in an OBOM.
1037
+ delete purlObj.qualifiers.arch;
1038
+ comp.purl = build({
1039
+ type: purlObj.type,
1040
+ namespace: purlObj.namespace || null,
1041
+ name: name,
1042
+ version: purlObj.version || null,
1043
+ qualifiers: Object.keys(purlObj.qualifiers).length
1044
+ ? purlObj.qualifiers
1045
+ : null,
1046
+ subpath: purlObj.subpath || null,
1047
+ });
1048
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
1049
+ }
1033
1050
  } catch (_err) {
1034
1051
  // continue regardless of error
1035
1052
  }
@@ -1115,16 +1132,20 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1115
1132
  if (distro_codename !== "") {
1116
1133
  allTypes.add(distro_codename);
1117
1134
  allTypes.add(purlObj.namespace);
1118
- comp.purl = build({
1119
- type: purlObj.type,
1120
- namespace: purlObj.namespace || null,
1121
- name: name,
1122
- version: purlObj.version || null,
1123
- qualifiers: purlObj.qualifiers || null,
1124
- subpath: purlObj.subpath || null,
1125
- });
1126
- comp["bom-ref"] = decodeURIComponent(comp.purl);
1127
1135
  }
1136
+ // Rebuilt whether or not the distro has a codename: the distro
1137
+ // stamped above is ID-VERSION_ID for every distro, and a purl
1138
+ // left as trivy wrote it keeps trivy's own spelling (a bare
1139
+ // 20230201 for a Wolfi image).
1140
+ comp.purl = build({
1141
+ type: purlObj.type,
1142
+ namespace: purlObj.namespace || null,
1143
+ name: name,
1144
+ version: purlObj.version || null,
1145
+ qualifiers: purlObj.qualifiers || null,
1146
+ subpath: purlObj.subpath || null,
1147
+ });
1148
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
1128
1149
  } catch (_err) {
1129
1150
  // continue regardless of error
1130
1151
  }
@@ -1404,9 +1425,36 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1404
1425
  "/etc/ld.so.conf",
1405
1426
  "/etc/ld.so.conf.d/*.conf",
1406
1427
  ownedFilePaths,
1428
+ // Apache loads libphp*.so from a modules directory that is two levels
1429
+ // below /usr, which the default one-level globs miss.
1430
+ [
1431
+ "**/usr/lib/apache2/modules/libphp*.so",
1432
+ "**/usr/lib/httpd/modules/libphp*.so",
1433
+ "**/usr/lib64/httpd/modules/libphp*.so",
1434
+ ],
1407
1435
  ),
1408
1436
  "shared_library",
1409
1437
  );
1438
+ // Go executables carry the toolchain they were built with in their
1439
+ // buildinfo header. The stdlib is statically linked, so that version
1440
+ // decides whether the binary applies Go's CVE-2026-111943 distrust list.
1441
+ const goBuildInventory = collectGoBuildInfo(executables, src);
1442
+ if (goBuildInventory.stdlibComponents.length) {
1443
+ pkgList.push(...goBuildInventory.stdlibComponents);
1444
+ }
1445
+ if (goBuildInventory.dependencies.length) {
1446
+ dependenciesList.push(...goBuildInventory.dependencies);
1447
+ }
1448
+ // Source-built PHP (the Docker Official image, FrankenPHP and custom
1449
+ // /usr/local builds) is unpackaged, so its version is only visible in the
1450
+ // binaries and the installed php_version.h.
1451
+ const phpBuildInventory = collectPhpBuildInfo(executables, sharedLibs, src);
1452
+ if (phpBuildInventory.runtimeComponents.length) {
1453
+ pkgList.push(...phpBuildInventory.runtimeComponents);
1454
+ }
1455
+ if (phpBuildInventory.dependencies.length) {
1456
+ dependenciesList.push(...phpBuildInventory.dependencies);
1457
+ }
1410
1458
  return {
1411
1459
  osPackages: pkgList,
1412
1460
  osPackageFiles,
@@ -1545,17 +1593,24 @@ function mergeOrganizationalEntityField(component, fieldName, entityName) {
1545
1593
  return { applied: false, represented: false };
1546
1594
  }
1547
1595
 
1548
- function parseOrganizationalContact(value) {
1596
+ /**
1597
+ * Split a `Name <address>` maintainer string into an `organizationalContact`.
1598
+ *
1599
+ * @param {string} value Maintainer or supplier string
1600
+ * @returns {{name: string, email?: string}|undefined} The contact, if any
1601
+ */
1602
+ export function parseOrganizationalContact(value) {
1549
1603
  const normalizedValue = `${value || ""}`.trim();
1550
1604
  if (!normalizedValue) {
1551
1605
  return undefined;
1552
1606
  }
1553
1607
  const match = normalizedValue.match(/^([^<>]+?)\s*<([^<>\s]+@[^<>\s]+)>$/);
1554
1608
  if (match) {
1555
- return {
1556
- name: match[1].trim(),
1557
- email: match[2].trim(),
1558
- };
1609
+ const name = match[1].trim();
1610
+ const email = match[2].trim();
1611
+ // `organizationalContact.email` is an `idn-email`; an address that is not
1612
+ // one (say `a@b@c`) would fail schema validation, so keep only the name.
1613
+ return isValidIdnEmail(email) ? { name, email } : { name };
1559
1614
  }
1560
1615
  return { name: normalizedValue };
1561
1616
  }
@@ -1644,13 +1699,34 @@ function promoteTrivyOsPackageIdentity(component, trivyMetadata) {
1644
1699
  async function collectRootfsRepositoryInventory(basePath, options = {}) {
1645
1700
  let trustedKeyComponents = [];
1646
1701
  let refsByPath = new Map();
1702
+ let trustMaterialDependencies = [];
1647
1703
  if (isCycloneDxComponentTypeEnabled("cryptographic-asset", options)) {
1648
1704
  ({ components: trustedKeyComponents, refsByPath } =
1649
1705
  await collectTrustedKeyComponents(basePath));
1650
- trustedKeyComponents = applyTrustMaterialEnhancements(
1706
+ ({
1707
+ components: trustedKeyComponents,
1708
+ dependencies: trustMaterialDependencies,
1709
+ } = applyTrustMaterialEnhancements(
1651
1710
  trustedKeyComponents,
1652
1711
  collectTrustInspectorRootfsInventory(basePath),
1653
- );
1712
+ basePath,
1713
+ ));
1714
+ if (!TRUSTINSPECTOR_BIN) {
1715
+ // No plugin: walk the same locations in JavaScript. With the plugin
1716
+ // present its rootfs report is the only producer, so nothing is
1717
+ // counted twice.
1718
+ const fallback = collectRootfsCertificates(basePath);
1719
+ const knownRefs = new Set(trustedKeyComponents.map((c) => c["bom-ref"]));
1720
+ for (const component of fallback.components) {
1721
+ if (!knownRefs.has(component["bom-ref"])) {
1722
+ knownRefs.add(component["bom-ref"]);
1723
+ trustedKeyComponents.push(component);
1724
+ }
1725
+ }
1726
+ trustMaterialDependencies = trustMaterialDependencies.concat(
1727
+ fallback.dependencies,
1728
+ );
1729
+ }
1654
1730
  }
1655
1731
  refsByPath = new Map(
1656
1732
  trustedKeyComponents
@@ -1692,6 +1768,7 @@ async function collectRootfsRepositoryInventory(basePath, options = {}) {
1692
1768
  });
1693
1769
  }
1694
1770
  }
1771
+ dependenciesList.push(...trustMaterialDependencies);
1695
1772
  return { components, dependenciesList };
1696
1773
  }
1697
1774
 
@@ -1877,7 +1954,9 @@ function executeTrustInspector(args, activity) {
1877
1954
  if (DEBUG_MODE) {
1878
1955
  console.log("Executing", TRUSTINSPECTOR_BIN, args.join(" "));
1879
1956
  }
1880
- const result = safeSpawnSync(TRUSTINSPECTOR_BIN, args);
1957
+ const result = safeSpawnSync(TRUSTINSPECTOR_BIN, args, {
1958
+ timeout: 10 * 60 * 1000,
1959
+ });
1881
1960
  if (result?.status !== 0 || result?.error) {
1882
1961
  if (DEBUG_MODE && (result?.stdout || result?.stderr)) {
1883
1962
  console.error(result.stdout, result.stderr);
@@ -1991,91 +2070,54 @@ function createTrustMaterialComponent(material) {
1991
2070
  ? [{ name: "cdx:crypto:expiresAt", value: material.expiresAt }]
1992
2071
  : []),
1993
2072
  ]);
1994
- let component;
1995
2073
  if (material.kind === "certificate") {
1996
- component = {
1997
- "bom-ref": `crypto/certificate/${encodeURIComponent(material.name || normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.serial || material.expiresAt || "unknown"}`,
1998
- name: material.name || basename(normalizedPath),
1999
- type: "cryptographic-asset",
2000
- version:
2001
- material.sha256 ||
2002
- material.serial ||
2003
- material.expiresAt ||
2004
- "configured",
2005
- hashes,
2006
- description: material.subject || normalizedPath,
2007
- cryptoProperties: {
2008
- assetType: "certificate",
2009
- algorithmProperties: {
2010
- executionEnvironment: "unknown",
2011
- implementationPlatform: "unknown",
2012
- },
2013
- certificateProperties: {
2014
- serialNumber: material.serial || undefined,
2015
- subjectName: material.subject || undefined,
2016
- issuerName: material.issuer || undefined,
2017
- notValidBefore: material.createdAt || undefined,
2018
- notValidAfter: material.expiresAt || undefined,
2019
- certificateFormat: material.format || "X.509",
2020
- certificateFileExtension: material.fileExtension || undefined,
2021
- fingerprint: material.fingerprint
2022
- ? { alg: "SHA-256", content: material.fingerprint }
2023
- : undefined,
2024
- },
2074
+ // Certificates are built by the shared collector in
2075
+ // applyTrustMaterialEnhancements; this legacy builder no longer produces
2076
+ // them, so a certificate material reaching here is skipped.
2077
+ return undefined;
2078
+ }
2079
+ const component = {
2080
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.keyId || "unknown"}`,
2081
+ name: material.name || basename(normalizedPath),
2082
+ type: "cryptographic-asset",
2083
+ version: material.sha256 || material.keyId || normalizedPath,
2084
+ hashes,
2085
+ cryptoProperties: {
2086
+ assetType: "related-crypto-material",
2087
+ relatedCryptoMaterialProperties: {
2088
+ type: "public-key",
2089
+ id:
2090
+ material.keyId ||
2091
+ material.fingerprint ||
2092
+ material.sha256 ||
2093
+ normalizedPath,
2094
+ state: trustMaterialState(material),
2025
2095
  },
2026
- properties: uniqueProperties(
2027
- sharedProperties.concat(
2028
- material.trustDomain
2029
- ? [{ name: "cdx:crypto:trustDomain", value: material.trustDomain }]
2030
- : [],
2031
- ),
2032
- ),
2033
- };
2034
- } else {
2035
- component = {
2036
- "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.keyId || "unknown"}`,
2037
- name: material.name || basename(normalizedPath),
2038
- type: "cryptographic-asset",
2039
- version: material.sha256 || material.keyId || normalizedPath,
2040
- hashes,
2041
- cryptoProperties: {
2042
- assetType: "related-crypto-material",
2043
- relatedCryptoMaterialProperties: {
2044
- type: "public-key",
2045
- id:
2046
- material.keyId ||
2047
- material.fingerprint ||
2048
- material.sha256 ||
2049
- normalizedPath,
2050
- state: trustMaterialState(material),
2096
+ },
2097
+ properties: uniqueProperties(
2098
+ sharedProperties.concat([
2099
+ {
2100
+ name: "cdx:crypto:trustDomain",
2101
+ value: material.trustDomain || deriveTrustedKeyDomain(normalizedPath),
2051
2102
  },
2052
- },
2053
- properties: uniqueProperties(
2054
- sharedProperties.concat([
2055
- {
2056
- name: "cdx:crypto:trustDomain",
2057
- value:
2058
- material.trustDomain || deriveTrustedKeyDomain(normalizedPath),
2059
- },
2060
- { name: "cdx:crypto:keyPath", value: normalizedPath },
2061
- {
2062
- name: "cdx:crypto:fileExtension",
2063
- value:
2064
- material.fileExtension ||
2065
- extname(normalizedPath).replace(/^\./, "") ||
2066
- "gpg",
2067
- },
2068
- ...(material?.keyId
2069
- ? [{ name: "cdx:crypto:keyId", value: material.keyId }]
2070
- : []),
2071
- ...(material?.userIds || []).map((value) => ({
2072
- name: "cdx:crypto:userId",
2073
- value,
2074
- })),
2075
- ]),
2076
- ),
2077
- };
2078
- }
2103
+ { name: "cdx:crypto:keyPath", value: normalizedPath },
2104
+ {
2105
+ name: "cdx:crypto:fileExtension",
2106
+ value:
2107
+ material.fileExtension ||
2108
+ extname(normalizedPath).replace(/^\./, "") ||
2109
+ "gpg",
2110
+ },
2111
+ ...(material?.keyId
2112
+ ? [{ name: "cdx:crypto:keyId", value: material.keyId }]
2113
+ : []),
2114
+ ...(material?.userIds || []).map((value) => ({
2115
+ name: "cdx:crypto:userId",
2116
+ value,
2117
+ })),
2118
+ ]),
2119
+ ),
2120
+ };
2079
2121
  attachIdentityTools(component, trustInspectorToolRefs());
2080
2122
  return component;
2081
2123
  }
@@ -2100,9 +2142,73 @@ function enhanceComponentFromTrustMaterial(component, material) {
2100
2142
  return component;
2101
2143
  }
2102
2144
 
2103
- function applyTrustMaterialEnhancements(components, materials) {
2145
+ function createKeystoreComponent(material) {
2146
+ const normalizedPath = normalizeContainerPath(material?.path);
2147
+ if (!normalizedPath) {
2148
+ return undefined;
2149
+ }
2150
+ const readable = (material?.properties || []).find(
2151
+ (property) => property.name === "cdx:crypto:keystoreReadable",
2152
+ )?.value;
2153
+ const component = {
2154
+ "bom-ref": `crypto/related-crypto-material/keystore/${encodeURIComponent(normalizedPath)}`,
2155
+ name: basename(normalizedPath),
2156
+ type: "cryptographic-asset",
2157
+ cryptoProperties: {
2158
+ assetType: "related-crypto-material",
2159
+ relatedCryptoMaterialProperties: {
2160
+ type: "other",
2161
+ ...(material?.format ? { format: material.format } : {}),
2162
+ },
2163
+ },
2164
+ evidence: { occurrences: [{ location: normalizedPath }] },
2165
+ properties: [
2166
+ { name: "internal:SrcFile", value: normalizedPath },
2167
+ ...(material?.sourceType
2168
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2169
+ : []),
2170
+ ...(readable
2171
+ ? [{ name: "cdx:crypto:keystoreReadable", value: readable }]
2172
+ : []),
2173
+ ],
2174
+ };
2175
+ attachIdentityTools(component, trustInspectorToolRefs());
2176
+ return component;
2177
+ }
2178
+
2179
+ // Private keys are classified from the PEM header line only and never
2180
+ // decoded (PLAN section 1.7): the format travels, no key material does.
2181
+ function createPrivateKeyComponent(material) {
2182
+ const normalizedPath = normalizeContainerPath(material?.path);
2183
+ if (!normalizedPath) {
2184
+ return undefined;
2185
+ }
2186
+ const component = {
2187
+ "bom-ref": `crypto/related-crypto-material/private-key/${encodeURIComponent(normalizedPath)}`,
2188
+ name: basename(normalizedPath),
2189
+ type: "cryptographic-asset",
2190
+ cryptoProperties: {
2191
+ assetType: "related-crypto-material",
2192
+ relatedCryptoMaterialProperties: {
2193
+ type: "private-key",
2194
+ ...(material?.format ? { format: material.format } : {}),
2195
+ },
2196
+ },
2197
+ evidence: { occurrences: [{ location: normalizedPath }] },
2198
+ properties: [
2199
+ { name: "internal:SrcFile", value: normalizedPath },
2200
+ ...(material?.sourceType
2201
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2202
+ : []),
2203
+ ],
2204
+ };
2205
+ attachIdentityTools(component, trustInspectorToolRefs());
2206
+ return component;
2207
+ }
2208
+
2209
+ function applyTrustMaterialEnhancements(components, materials, basePath) {
2104
2210
  if (!materials?.length) {
2105
- return components || [];
2211
+ return { components: components || [], dependencies: [] };
2106
2212
  }
2107
2213
  const componentList = [...(components || [])];
2108
2214
  const bySrcFile = new Map();
@@ -2119,6 +2225,8 @@ function applyTrustMaterialEnhancements(components, materials) {
2119
2225
  }
2120
2226
  }
2121
2227
  for (const material of materials) {
2228
+ // GPG-style keyring materials (report-1 `kind: public-key`) enhance the
2229
+ // components the native readers already produced.
2122
2230
  const srcFile = normalizeContainerPath(material?.path);
2123
2231
  const existing = bySrcFile.get(srcFile);
2124
2232
  if (
@@ -2129,17 +2237,95 @@ function applyTrustMaterialEnhancements(components, materials) {
2129
2237
  enhanceComponentFromTrustMaterial(existing, material);
2130
2238
  continue;
2131
2239
  }
2132
- const component = createTrustMaterialComponent(material);
2240
+ if (material?.kind === "public-key") {
2241
+ const component = createTrustMaterialComponent(material);
2242
+ if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
2243
+ continue;
2244
+ }
2245
+ seenRefs.add(component["bom-ref"]);
2246
+ componentList.push(component);
2247
+ if (srcFile) {
2248
+ bySrcFile.set(srcFile, component);
2249
+ }
2250
+ }
2251
+ }
2252
+ // Certificates, their SPKI public keys, algorithm components and verified
2253
+ // issuer edges come from the shared collector; keystores and private keys
2254
+ // are their own components.
2255
+ const collector = new CertificateCollector();
2256
+ collector.addTrustInspectorMaterials(materials);
2257
+ const { components: cryptoComponents, dependencies } = collector.build();
2258
+ for (const material of materials) {
2259
+ if (material?.kind === "keystore") {
2260
+ cryptoComponents.push(createKeystoreComponent(material));
2261
+ } else if (material?.kind === "private-key") {
2262
+ cryptoComponents.push(createPrivateKeyComponent(material));
2263
+ }
2264
+ }
2265
+ // A keystore the plugin could read is reported as the certificates it
2266
+ // holds; give it the keystore component the unreadable ones get and link
2267
+ // it to those certificates.
2268
+ if (basePath) {
2269
+ const certMaterialByPath = new Map();
2270
+ for (const material of materials) {
2271
+ if (material?.kind === "certificate" && material.path) {
2272
+ certMaterialByPath.set(normalizeContainerPath(material.path), material);
2273
+ }
2274
+ }
2275
+ for (const [path, material] of certMaterialByPath) {
2276
+ let classification;
2277
+ try {
2278
+ const absolute = join(basePath, path.replace(/^\/+/, ""));
2279
+ // The path comes from the plugin report: it must stay inside the
2280
+ // scanned root, symlinks included.
2281
+ const realRoot = realpathSync(basePath);
2282
+ const realFile = realpathSync(absolute);
2283
+ const inside = relative(realRoot, realFile);
2284
+ if (inside.startsWith("..") || isAbsolute(inside)) {
2285
+ continue;
2286
+ }
2287
+ const stats = lstatSync(absolute);
2288
+ if (!stats.isFile() || stats.size > 16 * 1024 * 1024) {
2289
+ continue;
2290
+ }
2291
+ classification = classifyCryptoFile(readFileSync(absolute));
2292
+ } catch (_err) {
2293
+ continue;
2294
+ }
2295
+ if (classification.kind !== "keystore") {
2296
+ continue;
2297
+ }
2298
+ const keystore = createKeystoreComponent({
2299
+ path,
2300
+ format: classification.format,
2301
+ sourceType: material.sourceType,
2302
+ });
2303
+ cryptoComponents.push(keystore);
2304
+ const held = cryptoComponents
2305
+ .filter(
2306
+ (component) =>
2307
+ component.cryptoProperties?.assetType === "certificate" &&
2308
+ component.evidence?.occurrences?.some(
2309
+ (occurrence) =>
2310
+ normalizeContainerPath(occurrence.location) === path,
2311
+ ),
2312
+ )
2313
+ .map((component) => component["bom-ref"])
2314
+ .sort();
2315
+ if (held.length) {
2316
+ dependencies.push({ ref: keystore["bom-ref"], dependsOn: held });
2317
+ }
2318
+ }
2319
+ }
2320
+ for (const component of cryptoComponents) {
2133
2321
  if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
2134
2322
  continue;
2135
2323
  }
2136
2324
  seenRefs.add(component["bom-ref"]);
2325
+ attachIdentityTools(component, trustInspectorToolRefs());
2137
2326
  componentList.push(component);
2138
- if (srcFile) {
2139
- bySrcFile.set(srcFile, component);
2140
- }
2141
2327
  }
2142
- return componentList;
2328
+ return { components: componentList, dependencies };
2143
2329
  }
2144
2330
 
2145
2331
  function collectTrustInspectorRootfsInventory(basePath) {
@@ -3299,10 +3485,37 @@ export function enrichOSComponentsWithTrustData(components = []) {
3299
3485
  if (hostComponents.length) {
3300
3486
  mergedComponents.push(...hostComponents);
3301
3487
  }
3488
+ // report-2 host mode enumerates the platform trust stores as materials;
3489
+ // they flow through the shared certificate model like any other producer.
3490
+ const hostCollector = new CertificateCollector();
3491
+ hostCollector.addTrustInspectorMaterials(hostData?.materials);
3492
+ const { components: hostMaterialComponents } = hostCollector.build();
3493
+ for (const material of hostData?.materials || []) {
3494
+ if (material?.kind === "keystore") {
3495
+ const component = createKeystoreComponent(material);
3496
+ if (component) {
3497
+ hostMaterialComponents.push(component);
3498
+ }
3499
+ } else if (material?.kind === "private-key") {
3500
+ const component = createPrivateKeyComponent(material);
3501
+ if (component) {
3502
+ hostMaterialComponents.push(component);
3503
+ }
3504
+ }
3505
+ }
3506
+ for (const component of hostMaterialComponents) {
3507
+ if (!component?.["bom-ref"]) {
3508
+ continue;
3509
+ }
3510
+ attachIdentityTools(component, trustInspectorToolRefs());
3511
+ mergedComponents.push(component);
3512
+ }
3302
3513
  return {
3303
3514
  components: mergedComponents,
3304
3515
  tools:
3305
- pathInspectionCandidates.length || hostComponents.length
3516
+ pathInspectionCandidates.length ||
3517
+ hostComponents.length ||
3518
+ hostMaterialComponents.length
3306
3519
  ? getPluginToolComponents(["trustinspector"])
3307
3520
  : [],
3308
3521
  };
@@ -3473,6 +3686,139 @@ export function getBinaryBom(src, binaryBomFile, deepMode) {
3473
3686
  return true;
3474
3687
  }
3475
3688
 
3689
+ // The Go standard library has no module path, so `pkg:golang/stdlib@v<x>` is
3690
+ // not a purl cdx-purl will build (the same reason trivy's stdlib purl is
3691
+ // dropped further up). The bom-ref below is the decoded form of that string,
3692
+ // exactly the ref trivy-produced stdlib components already carry, so both
3693
+ // paths land on one component. Trivy reports the stdlib version without the
3694
+ // `go` prefix (`v1.27.2`) while buildinfo keeps it (`go1.27.2`).
3695
+ function goStdlibRef(goVersion) {
3696
+ // "go1.26.10 X:boringcrypto" names the toolchain go1.26.10.
3697
+ return `pkg:golang/stdlib@v${goVersion.split(/\s/)[0].replace(/^go/, "")}`;
3698
+ }
3699
+
3700
+ /**
3701
+ * Read the Go buildinfo version of each executable file component and link it
3702
+ * to (or create) the matching `pkg:golang/stdlib` component. Mutates the
3703
+ * components in place by adding `cdx:go:buildVersion`.
3704
+ *
3705
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3706
+ * @param {string} basePath extracted rootfs the paths are relative to
3707
+ * @returns {{stdlibComponents: Object[], dependencies: Object[]}}
3708
+ */
3709
+ function collectGoBuildInfo(executables, basePath) {
3710
+ const stdlibComponents = [];
3711
+ const dependencies = [];
3712
+ if (!executables?.length) {
3713
+ return { stdlibComponents, dependencies };
3714
+ }
3715
+ const stdlibRefs = new Set();
3716
+ for (const component of executables) {
3717
+ if (!component?.properties) {
3718
+ continue;
3719
+ }
3720
+ const srcFile = component.properties.find(
3721
+ (property) => property.name === "internal:SrcFile",
3722
+ )?.value;
3723
+ if (!srcFile) {
3724
+ continue;
3725
+ }
3726
+ const goVersion = readGoBuildVersion(
3727
+ join(basePath, srcFile.replace(/^\/+/, "")),
3728
+ );
3729
+ if (!goVersion) {
3730
+ continue;
3731
+ }
3732
+ component.properties.push({
3733
+ name: "cdx:go:buildVersion",
3734
+ value: goVersion,
3735
+ });
3736
+ // A pre-1.18 toolchain has no version string to build a stdlib ref
3737
+ // from; the buildVersion property alone records the fact.
3738
+ if (goVersion === PRE_1_18_VERSION) {
3739
+ continue;
3740
+ }
3741
+ const stdlibRef = goStdlibRef(goVersion);
3742
+ if (!stdlibRefs.has(stdlibRef)) {
3743
+ stdlibRefs.add(stdlibRef);
3744
+ stdlibComponents.push({
3745
+ "bom-ref": stdlibRef,
3746
+ name: "stdlib",
3747
+ type: "library",
3748
+ version: stdlibRef.slice("pkg:golang/stdlib@".length),
3749
+ group: "",
3750
+ });
3751
+ dependencies.push({ ref: stdlibRef, dependsOn: [] });
3752
+ }
3753
+ if (component["bom-ref"]) {
3754
+ dependencies.push({
3755
+ ref: component["bom-ref"],
3756
+ dependsOn: [stdlibRef],
3757
+ });
3758
+ }
3759
+ }
3760
+ return { stdlibComponents, dependencies };
3761
+ }
3762
+
3763
+ /**
3764
+ * Read the PHP build version of each PHP-named executable and shared library,
3765
+ * add the cdx:php:* properties, and link the components to one platform
3766
+ * component per distinct version. Mutates the components in place, as the Go
3767
+ * collector does.
3768
+ *
3769
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3770
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
3771
+ * @param {string} basePath extracted rootfs the paths are relative to
3772
+ * @returns {{runtimeComponents: Object[], dependencies: Object[]}}
3773
+ */
3774
+ function collectPhpBuildInfo(executables, sharedLibs, basePath) {
3775
+ const inventory = createPhpRuntimeInventory();
3776
+ let probes = 0;
3777
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
3778
+ if (probes >= MAX_PHP_PROBES) {
3779
+ break;
3780
+ }
3781
+ if (!component?.properties) {
3782
+ continue;
3783
+ }
3784
+ const srcFile = component.properties.find(
3785
+ (property) => property.name === "internal:SrcFile",
3786
+ )?.value;
3787
+ if (!srcFile) {
3788
+ continue;
3789
+ }
3790
+ const sapi = classifyPhpSapi(srcFile);
3791
+ if (!sapi) {
3792
+ continue;
3793
+ }
3794
+ const absolutePath = join(basePath, srcFile.replace(/^\/+/, ""));
3795
+ let resolvedPath = srcFile;
3796
+ try {
3797
+ resolvedPath = relative(
3798
+ realpathSync(basePath),
3799
+ realpathSync(absolutePath),
3800
+ );
3801
+ } catch {
3802
+ // readPhpBuildVersion decides what an unresolvable path yields.
3803
+ }
3804
+ // Trivy does not list Nix, Guix or Homebrew packages, so a PHP in their
3805
+ // stores looks unpackaged here, but it is still theirs.
3806
+ if (packageStoreOf(srcFile) || packageStoreOf(resolvedPath)) {
3807
+ continue;
3808
+ }
3809
+ probes += 1;
3810
+ const build = readPhpBuildVersion(absolutePath, basePath);
3811
+ if (!build) {
3812
+ continue;
3813
+ }
3814
+ attachPhpBuild(inventory, component, build, sapi);
3815
+ }
3816
+ return {
3817
+ runtimeComponents: inventory.runtimeComponents,
3818
+ dependencies: inventory.dependencies,
3819
+ };
3820
+ }
3821
+
3476
3822
  async function fileComponents(basePath, fileList, fileType) {
3477
3823
  const hashResults = await mapWithConcurrency(fileList, (f) =>
3478
3824
  multiChecksumFile(["md5", "sha1"], join(basePath, f))