@cdxgen/cdxgen 13.6.0 → 13.7.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 (264) hide show
  1. package/README.md +15 -8
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +256 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +25 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +18 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/pqc-migration.json +239 -0
  15. package/data/remediations.json +52 -0
  16. package/data/rules/cbom-security.yaml +4 -3
  17. package/data/rules/package-integrity.yaml +28 -0
  18. package/lib/audit/index.js +25 -3
  19. package/lib/cli/bomAssembly.js +80 -1
  20. package/lib/cli/cliOptions.js +121 -17
  21. package/lib/cli/index.js +609 -898
  22. package/lib/cli/jsBom.js +3 -0
  23. package/lib/cli/jvmBom.js +365 -40
  24. package/lib/cli/managedBom.js +19 -3
  25. package/lib/cli/nativeBom.js +10 -0
  26. package/lib/core/env.js +26 -0
  27. package/lib/core/fs.js +126 -3
  28. package/lib/core/globPatterns.js +218 -0
  29. package/lib/core/processTree.js +99 -12
  30. package/lib/core/projectConfig.js +332 -25
  31. package/lib/core/toolRegistry.js +15 -1
  32. package/lib/ecosystems/denoutils.js +2 -2
  33. package/lib/ecosystems/golem.js +3 -8
  34. package/lib/ecosystems/gradleutils.js +1082 -6
  35. package/lib/ecosystems/jsEvidence.js +90 -38
  36. package/lib/ecosystems/kosi.js +3 -5
  37. package/lib/ecosystems/npmVersionResolution.js +376 -0
  38. package/lib/ecosystems/parsers-python.js +89 -28
  39. package/lib/ecosystems/parsers-rust.js +117 -15
  40. package/lib/ecosystems/remote/huggingface.js +22 -0
  41. package/lib/ecosystems/rusi.js +2 -4
  42. package/lib/ecosystems/scalasem.js +2 -1
  43. package/lib/evinser/evinser.js +196 -34
  44. package/lib/evinser/scalasem.js +5 -1
  45. package/lib/helpers/agenticToolutils.js +112 -1
  46. package/lib/helpers/bomSigner.js +43 -9
  47. package/lib/helpers/jssSigner.js +1276 -0
  48. package/lib/inventory/agentBlueprint.js +193 -0
  49. package/lib/inventory/agentToolNames.js +145 -0
  50. package/lib/inventory/aiCollector.js +153 -7
  51. package/lib/inventory/aiInventory.js +44 -8
  52. package/lib/inventory/aiProviders.js +324 -0
  53. package/lib/inventory/bomUtils.js +8 -1
  54. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  55. package/lib/inventory/citations.js +31 -2
  56. package/lib/inventory/communityAiConfigParser.js +978 -80
  57. package/lib/inventory/cyclonedx20.js +152 -0
  58. package/lib/inventory/cyclonedx20Common.js +286 -0
  59. package/lib/inventory/cyclonedx20Derived.js +159 -0
  60. package/lib/inventory/cyclonedx20Parties.js +552 -0
  61. package/lib/inventory/cyclonedx20Read.js +768 -0
  62. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  63. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  64. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  65. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  66. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  67. package/lib/inventory/cyclonedx20Render.js +766 -0
  68. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  69. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  70. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  71. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  72. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  73. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  74. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  75. package/lib/inventory/cyclonedx20Tables.js +594 -0
  76. package/lib/inventory/deps.js +106 -13
  77. package/lib/inventory/dosai.js +236 -1
  78. package/lib/inventory/dosaiParsers.js +3 -6
  79. package/lib/inventory/dotnetRuntime.js +164 -14
  80. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  81. package/lib/inventory/envcontext.js +132 -38
  82. package/lib/inventory/evidenceUtils.js +2 -1
  83. package/lib/inventory/formulationParsers.js +95 -13
  84. package/lib/inventory/hbom.js +45 -6
  85. package/lib/inventory/hbomLoader.js +166 -0
  86. package/lib/inventory/mcpConfigParser.js +5 -0
  87. package/lib/inventory/occurrenceKey.js +90 -0
  88. package/lib/inventory/pqcMigration.js +426 -0
  89. package/lib/inventory/rustFormulationParser.js +21 -10
  90. package/lib/inventory/source.js +65 -6
  91. package/lib/parsers/huggingfaceManifest.js +9 -1
  92. package/lib/parsers/huggingfaceUtils.js +18 -0
  93. package/lib/parsers/kiroPermissions.js +166 -0
  94. package/lib/server/server.js +36 -34
  95. package/lib/stages/postgen/postgen.js +41 -1
  96. package/lib/stages/postgen/specVersionCompat.js +76 -0
  97. package/lib/validator/bomValidator.js +8 -1
  98. package/lib/validator/complianceEngine.js +30 -3
  99. package/lib/validator/complianceRules.js +1416 -4
  100. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  101. package/lib/validator/index.js +48 -7
  102. package/lib/validator/reporters/json.js +6 -0
  103. package/package.json +9 -9
  104. package/types/bin/repl.d.ts.map +1 -1
  105. package/types/lib/audit/index.d.ts +6 -2
  106. package/types/lib/audit/index.d.ts.map +1 -1
  107. package/types/lib/cli/bomAssembly.d.ts +16 -0
  108. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  109. package/types/lib/cli/cliOptions.d.ts +6 -0
  110. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  111. package/types/lib/cli/index.d.ts.map +1 -1
  112. package/types/lib/cli/jsBom.d.ts +2 -1
  113. package/types/lib/cli/jsBom.d.ts.map +1 -1
  114. package/types/lib/cli/jvmBom.d.ts +15 -0
  115. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  116. package/types/lib/cli/managedBom.d.ts.map +1 -1
  117. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  118. package/types/lib/core/boundedFile.d.ts +30 -0
  119. package/types/lib/core/boundedFile.d.ts.map +1 -1
  120. package/types/lib/core/env.d.ts +10 -0
  121. package/types/lib/core/env.d.ts.map +1 -1
  122. package/types/lib/core/fs.d.ts +4 -0
  123. package/types/lib/core/fs.d.ts.map +1 -1
  124. package/types/lib/core/globPatterns.d.ts +70 -0
  125. package/types/lib/core/globPatterns.d.ts.map +1 -0
  126. package/types/lib/core/paths.d.ts +9 -0
  127. package/types/lib/core/paths.d.ts.map +1 -1
  128. package/types/lib/core/processTree.d.ts +16 -5
  129. package/types/lib/core/processTree.d.ts.map +1 -1
  130. package/types/lib/core/projectConfig.d.ts +47 -8
  131. package/types/lib/core/projectConfig.d.ts.map +1 -1
  132. package/types/lib/core/toolRegistry.d.ts.map +1 -1
  133. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  134. package/types/lib/ecosystems/gradleutils.d.ts +248 -8
  135. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  136. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  137. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  139. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  140. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  141. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  142. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  143. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  144. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  145. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  146. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  147. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  148. package/types/lib/evinser/evinser.d.ts +61 -9
  149. package/types/lib/evinser/evinser.d.ts.map +1 -1
  150. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  151. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  152. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  153. package/types/lib/helpers/bomSigner.d.ts +20 -3
  154. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  155. package/types/lib/helpers/jssSigner.d.ts +288 -0
  156. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  157. package/types/lib/helpers/vsixutils.d.ts +303 -19
  158. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  159. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  160. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  161. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  162. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  163. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  164. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  165. package/types/lib/inventory/aiProviders.d.ts +85 -0
  166. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  167. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  168. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  169. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  170. package/types/lib/inventory/citations.d.ts +5 -0
  171. package/types/lib/inventory/citations.d.ts.map +1 -1
  172. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  173. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  174. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  175. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  176. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  177. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  178. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  179. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  180. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  181. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  182. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  183. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  184. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  185. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  186. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  187. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  188. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  189. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  190. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  191. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  192. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  193. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  194. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  195. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  196. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  197. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  198. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  199. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  200. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  201. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  202. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  203. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  204. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  205. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  206. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  207. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  208. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  209. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  210. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  211. package/types/lib/inventory/deps.d.ts +35 -2
  212. package/types/lib/inventory/deps.d.ts.map +1 -1
  213. package/types/lib/inventory/dosai.d.ts +19 -0
  214. package/types/lib/inventory/dosai.d.ts.map +1 -1
  215. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  216. package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
  217. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
  218. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  219. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  220. package/types/lib/inventory/envcontext.d.ts +22 -6
  221. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  222. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  224. package/types/lib/inventory/hbom.d.ts +11 -0
  225. package/types/lib/inventory/hbom.d.ts.map +1 -1
  226. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  227. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  228. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  229. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  230. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  231. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  232. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  233. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  234. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  235. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  236. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  237. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  238. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  239. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  240. package/types/lib/inventory/source.d.ts +15 -0
  241. package/types/lib/inventory/source.d.ts.map +1 -1
  242. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  243. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  244. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  245. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  246. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  247. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  248. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  249. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  250. package/types/lib/server/server.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  252. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  253. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  254. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  255. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  256. package/types/lib/validator/bomValidator.d.ts +5 -1
  257. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  258. package/types/lib/validator/complianceEngine.d.ts +4 -4
  259. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  260. package/types/lib/validator/complianceRules.d.ts +14 -2
  261. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  262. package/types/lib/validator/index.d.ts +10 -0
  263. package/types/lib/validator/index.d.ts.map +1 -1
  264. package/types/lib/validator/reporters/json.d.ts.map +1 -1
package/lib/cli/index.js CHANGED
@@ -53,7 +53,11 @@ import {
53
53
  import { collectObomIdeInventory } from "../helpers/vsixutils.js";
54
54
  import { optionIncludesAiInventoryProjectType } from "../inventory/aiInventory.js";
55
55
  import { readApkInstalledDb } from "../inventory/apkInstalledDb.js";
56
- import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
56
+ import {
57
+ isCycloneDx20SpecVersion,
58
+ isCycloneDxComponentTypeEnabled,
59
+ } from "../inventory/bomUtils.js";
60
+ import { DEPENDENCY_TRACK_CDX20_ERROR } from "../inventory/cyclonedx20.js";
57
61
  import {
58
62
  mergeDependencies,
59
63
  mergeServices,
@@ -852,12 +856,50 @@ export function summarizePurlTypes(components) {
852
856
  * data object (components, dependencies, bomJson, parentComponent, etc.)
853
857
  */
854
858
  export async function createMultiXBom(pathList, options) {
859
+ /**
860
+ * What createMultiXBom reads from the result of a create*Bom builder.
861
+ *
862
+ * @typedef {Object} BuilderBomData
863
+ * @property {{components?: Object[], dependencies?: Object[], services?: Object[]}} [bomJson] The generated BOM
864
+ * @property {Object} [parentComponent] The project the builder detected
865
+ * @property {Object[]} [dependencies] Dependencies returned beside bomJson (crypto scan)
866
+ * @property {Object[]} [formulationList] Formulation components for postProcess
867
+ * @property {Object[]} [metadataProperties] Properties for metadata (php platform data)
868
+ */
869
+
870
+ /**
871
+ * One ecosystem that createMultiXBom scans every path for.
872
+ *
873
+ * @typedef {Object} PathScanStep
874
+ * @property {(path: string, options: Object) => boolean} applies Whether the step runs for this path
875
+ * @property {(path: string, options: Object) => any} [prepare] Computes a value once for the hooks below
876
+ * @property {(path: string, options: Object, context: any) => void} [announce] Thought logs written before the scan
877
+ * @property {string|((context: any) => string)} activityType Project type recorded in the activity context
878
+ * @property {(path: string) => void} [beforeBuild] Runs after the activity context is set
879
+ * @property {(path: string, options: Object) => any} build The create*Bom builder, whose result is read as BuilderBomData
880
+ * @property {(bomData: BuilderBomData | undefined) => boolean} [isUseful] Whether the result is merged; by default, when it has components
881
+ * @property {(count: number, path: string, context: any) => string} [foundThought] Thought log for the components found
882
+ * @property {(count: number, path: string, context: any) => string} debugMessage Debug log for the components found
883
+ * @property {"concat"|"topLevel"|"mergeWithParent"} [dependencyMerge] How dependencies are merged; mergeDependencies by default
884
+ * @property {boolean} [mergeServices] Merge bomJson.services into options.services
885
+ * @property {boolean} [keepParent] Record the detected project as a sub-component; true by default
886
+ * @property {"plain"|"gradle"} [hoistSubComponents] Hoist the project's own sub-components
887
+ * @property {boolean} [collectMetadataProperties] Collect metadataProperties whether or not components were found
888
+ * @property {boolean} [collectFormulation] Collect the formulation list whether or not components were found
889
+ */
890
+
891
+ /** @type {Object[]} */
855
892
  let components = [];
893
+ /** @type {Object[]} */
856
894
  let dependencies = [];
895
+ /** @type {BuilderBomData | undefined} */
857
896
  let bomData;
897
+ /** @type {Object[]} */
858
898
  let formulationList = [];
899
+ /** @type {Object[]} */
859
900
  let metadataProperties = [];
860
901
  let parentComponent = determineParentComponent(options) || {};
902
+ /** @type {Object[]} */
861
903
  let parentSubComponents = [];
862
904
  let osFileOwners;
863
905
  const setProjectTypeActivityContext = (projectType, sourcePath) => {
@@ -960,943 +1002,600 @@ export async function createMultiXBom(pathList, options) {
960
1002
  dependencies.splice(0, 0, {
961
1003
  ref: parentComponent["bom-ref"],
962
1004
  dependsOn: Array.from(parentDependsOn).sort(),
963
- });
964
- }
965
- }
966
- if (hasAnyProjectType(["os"], options, false) && options.bomData) {
967
- bomData = options.bomData;
968
- if (bomData?.bomJson?.components) {
969
- if (DEBUG_MODE) {
970
- console.log(`Found ${bomData.bomJson.components.length} OS components`);
971
- }
972
- if (bomData.bomJson.components.length) {
973
- thoughtLog(
974
- `I found ${bomData.bomJson.components.length} OS packages 😎.`,
975
- );
976
- }
977
- components = components.concat(bomData.bomJson.components);
978
- }
979
- }
980
- const sharedOptions = options;
981
- // The exploded image layers are written once by the extractor and only read
982
- // from here on, so repeated walks of them can share one view of the tree.
983
- setDirWalkCacheRoot(options.allLayersExplodedDir);
984
- const scanOnePath = async (path) => {
985
- try {
986
- return await scanOnePathInner(path);
987
- } catch (err) {
988
- if (!isDeferredFailOnError(err)) {
989
- throw err;
990
- }
991
- // The failing path contributes nothing, and the remaining paths still
992
- // do: fail-on-error scopes the loss to the leg that failed.
993
- return {
994
- components: [],
995
- dependencies: [],
996
- options: { ...sharedOptions },
997
- parentSubComponents: [],
998
- metadataProperties: [],
999
- };
1000
- }
1001
- };
1002
- const scanOnePathInner = async (path) => {
1003
- // Each path accumulates into its own lists and its own view of options, so
1004
- // that legs which record a parent component or a tool cannot overwrite
1005
- // another path's. The lists are merged below in pathList order.
1006
- const options = { ...sharedOptions };
1007
- let bomData;
1008
- let components = [];
1009
- let dependencies = [];
1010
- let parentSubComponents = [];
1011
- let metadataProperties = [];
1012
- // Each builder detects the project of its own ecosystem. A builder that sets
1013
- // options.parentComponent to force its project into its own BOM must not
1014
- // make that project the next builder's too, or the next ecosystem's project
1015
- // is lost: every builder starts from the parent the caller gave.
1016
- const callerParent = options.parentComponent;
1017
- const withOwnParent = async (build) => {
1018
- options.parentComponent = callerParent;
1019
- try {
1020
- return await build();
1021
- } finally {
1022
- options.parentComponent = callerParent;
1023
- }
1024
- };
1025
- setActivityContext({
1026
- projectType: Array.isArray(options.projectType)
1027
- ? options.projectType.join(",")
1028
- : options.projectType,
1029
- sourcePath: path,
1030
- });
1031
- recordActivity({
1032
- kind: "read",
1033
- reason:
1034
- "Scanning project path for supported languages and package managers.",
1035
- status: "completed",
1036
- target: path,
1037
- });
1038
- if (DEBUG_MODE) {
1039
- console.log("Scanning", path);
1040
- }
1041
- if (pathList.length > 2) {
1042
- thoughtLog(`Let's thoroughly check the path ${path}.`);
1043
- }
1044
- // Node.js and AI inventory
1045
- if (
1046
- hasAnyProjectType(["oci", "js"], options) ||
1047
- hasAnyProjectType(["mcp", "ai-skill", "ai"], options) ||
1048
- getDirectAiInventoryType(path, options)
1049
- ) {
1050
- const exactAiInventoryType = getDirectAiInventoryType(path, options);
1051
- if (!hasAnyProjectType(["oci"], options, false)) {
1052
- if (exactAiInventoryType === "mcp") {
1053
- thoughtLog(
1054
- "**MCP**: Looking for MCP services, MCP configs, and related AI control-plane artifacts.",
1055
- );
1056
- } else if (exactAiInventoryType === "ai") {
1057
- thoughtLog(
1058
- "**AI-BOM**: Looking for AI models, direct model metadata, pedigree, quantization, and operator-facing AI context.",
1059
- );
1060
- } else if (exactAiInventoryType === "ai-skill") {
1061
- thoughtLog(
1062
- "**AI-SKILL**: Looking for AI instruction, skill, and agent-definition files that can influence build or release flows.",
1063
- );
1064
- } else {
1065
- thoughtLog(
1066
- "**JS**: Now looking for JavaScript projects (npm, yarn, pnpm) and files.",
1067
- );
1068
- }
1069
- }
1070
- setProjectTypeActivityContext(exactAiInventoryType || "js", path);
1071
- bomData = await withOwnParent(() => createNodejsBom(path, options));
1072
- if (bomData?.bomJson?.components?.length) {
1073
- if (exactAiInventoryType) {
1074
- thoughtLog(
1075
- `I found ${bomData.bomJson.components.length} ${exactAiInventoryType} component(s). Let's keep looking.`,
1076
- );
1077
- } else {
1078
- thoughtLog(
1079
- `I found ${bomData.bomJson.components.length} npm packages. Let's keep looking.`,
1080
- );
1081
- }
1082
- if (DEBUG_MODE) {
1083
- console.log(
1084
- `Found ${bomData.bomJson.components.length} ${exactAiInventoryType || "npm"} components at ${path}`,
1085
- );
1086
- }
1087
- components = components.concat(bomData.bomJson.components);
1088
- dependencies = mergeDependencies(
1089
- dependencies,
1090
- bomData.bomJson.dependencies,
1091
- );
1092
- if (bomData?.bomJson?.services?.length) {
1093
- options.services = mergeServices(
1094
- options.services || [],
1095
- bomData.bomJson.services,
1096
- );
1097
- }
1098
- if (
1099
- bomData.parentComponent &&
1100
- Object.keys(bomData.parentComponent).length
1101
- ) {
1102
- parentSubComponents.push(bomData.parentComponent);
1103
- }
1104
- // Retain metadata.component.components
1105
- if (bomData.parentComponent.components?.length) {
1106
- parentSubComponents = parentSubComponents.concat(
1107
- bomData.parentComponent.components,
1108
- );
1109
- delete bomData.parentComponent.components;
1110
- }
1111
- }
1112
- }
1113
- // Java
1114
- if (hasAnyProjectType(["oci", "java"], options)) {
1115
- if (!hasAnyProjectType(["oci"], options, false)) {
1116
- thoughtLog(
1117
- "**JAVA**: Looking for Java projects (e.g., Maven, Gradle, SBT). I hope all configurations—from Java version to individual build settings—are correctly aligned.",
1118
- );
1119
- }
1120
- setProjectTypeActivityContext("java", path);
1121
- bomData = await withOwnParent(() => createJavaBom(path, options));
1122
- if (bomData?.bomJson?.components?.length) {
1123
- thoughtLog(
1124
- `I found ${bomData.bomJson.components.length} java packages.`,
1125
- );
1126
- if (DEBUG_MODE) {
1127
- console.log(
1128
- `Found ${bomData.bomJson.components.length} java packages at ${path}`,
1129
- );
1130
- }
1131
- components = components.concat(bomData.bomJson.components);
1132
- dependencies = mergeDependencies(
1133
- dependencies,
1134
- bomData.bomJson.dependencies,
1135
- );
1136
- if (
1137
- bomData.parentComponent &&
1138
- Object.keys(bomData.parentComponent).length
1139
- ) {
1140
- parentSubComponents.push(bomData.parentComponent);
1141
- }
1142
- // Retain metadata.component.components, but add duplicates to the list of current components
1143
- // and removing them from metadata.component.components -- the components are merged later
1144
- if (bomData.parentComponent.components?.length) {
1145
- let bomSubComponents = bomData.parentComponent.components;
1146
- if (
1147
- !["false", "0"].includes(
1148
- readEnvironmentVariable("GRADLE_RESOLVE_FROM_NODE"),
1149
- )
1150
- ) {
1151
- thoughtLog(
1152
- "Wait, the user wants me to resolve gradle projects from npm.",
1153
- );
1154
- const allRefs = components.map((c) => c["bom-ref"]);
1155
- const duplicateComponents = bomSubComponents.filter((c) =>
1156
- allRefs.includes(c["bom-ref"]),
1157
- );
1158
- components = components.concat(duplicateComponents);
1159
- const duplicateComponentRefs = duplicateComponents.map(
1160
- (c) => c["bom-ref"],
1161
- );
1162
- bomSubComponents = bomSubComponents.filter(
1163
- (c) => !duplicateComponentRefs.includes(c["bom-ref"]),
1164
- );
1165
- }
1166
- parentSubComponents = parentSubComponents.concat(bomSubComponents);
1167
- delete bomData.parentComponent.components;
1168
- }
1169
- }
1170
- }
1171
- if (hasAnyProjectType(["oci", "py"], options)) {
1172
- if (!hasAnyProjectType(["oci"], options, false)) {
1173
- thoughtLog(
1174
- "**PYTHON**: Looking for Python projects with package managers such as pip, poetry, uv, etc. Wish me good luck!",
1175
- );
1176
- }
1177
- if (
1178
- readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true" &&
1179
- pathList.length <= 2
1180
- ) {
1181
- thoughtLog(
1182
- "I'm running in a non-container environment. Let's hope the correct build tools are available ✌️.",
1183
- );
1184
- }
1185
- setProjectTypeActivityContext("py", path);
1186
- bomData = await withOwnParent(() => createPythonBom(path, options));
1187
- if (bomData?.bomJson?.components?.length) {
1188
- thoughtLog(
1189
- `I found ${bomData.bomJson.components.length} python packages.`,
1190
- );
1191
- if (DEBUG_MODE) {
1192
- console.log(
1193
- `Found ${bomData.bomJson.components.length} python packages at ${path}`,
1194
- );
1195
- }
1196
- components = components.concat(bomData.bomJson.components);
1197
- dependencies = mergeDependencies(
1198
- dependencies,
1199
- bomData.bomJson.dependencies,
1200
- );
1201
- if (
1202
- bomData.parentComponent &&
1203
- Object.keys(bomData.parentComponent).length
1204
- ) {
1205
- parentSubComponents.push(bomData.parentComponent);
1206
- }
1207
- // Retain metadata.component.components. uv workspace members are
1208
- // nested under the python project, so hoisting them keeps them
1209
- // siblings of the other sub-components instead of riding one level
1210
- // deeper (or being dropped by the same-name collapse) when another
1211
- // ecosystem's project becomes the parent.
1212
- if (bomData.parentComponent?.components?.length) {
1213
- parentSubComponents = parentSubComponents.concat(
1214
- bomData.parentComponent.components,
1215
- );
1216
- delete bomData.parentComponent.components;
1217
- }
1218
- }
1219
- if (bomData?.formulationList?.length) {
1220
- formulationList = formulationList.concat(bomData.formulationList);
1221
- }
1222
- }
1223
- if (hasAnyProjectType(["oci", "go"], options)) {
1224
- if (!hasAnyProjectType(["oci"], options, false)) {
1225
- thoughtLog(
1226
- "**GO**: Looking for go projects. I need to be cautious about purl namespaces and potential failures with the 'go list' command.",
1227
- );
1228
- }
1229
- setProjectTypeActivityContext("go", path);
1230
- bomData = await withOwnParent(() => createGoBom(path, options));
1231
- if (bomData?.bomJson?.components?.length) {
1232
- thoughtLog(`I found ${bomData.bomJson.components.length} go packages.`);
1233
- if (DEBUG_MODE) {
1234
- console.log(
1235
- `Found ${bomData.bomJson.components.length} go packages at ${path}`,
1236
- );
1237
- }
1238
- components = components.concat(bomData.bomJson.components);
1239
- dependencies = mergeDependencies(
1240
- dependencies,
1241
- bomData.bomJson.dependencies,
1242
- );
1243
- if (
1244
- bomData.parentComponent &&
1245
- Object.keys(bomData.parentComponent).length
1246
- ) {
1247
- parentSubComponents.push(bomData.parentComponent);
1248
- }
1249
- }
1250
- }
1251
- if (hasAnyProjectType(["oci", "rust"], options)) {
1252
- if (!hasAnyProjectType(["oci"], options, false)) {
1253
- thoughtLog(
1254
- "**RUST**: Let's search for Cargo/Rust projects. Should I warn the user that we don't support Cargo 'features' and native dependencies, which may lead to both false positives and false negatives? 🤔?",
1255
- );
1256
- }
1257
- setProjectTypeActivityContext("rust", path);
1258
- bomData = await withOwnParent(() => createRustBom(path, options));
1259
- if (bomData?.bomJson?.components?.length) {
1260
- thoughtLog(
1261
- `I found ${bomData.bomJson.components.length} rust packages.`,
1262
- );
1263
- if (DEBUG_MODE) {
1264
- console.log(
1265
- `Found ${bomData.bomJson.components.length} rust packages at ${path}`,
1266
- );
1267
- }
1268
- components = components.concat(bomData.bomJson.components);
1269
- dependencies = mergeDependencies(
1270
- dependencies,
1271
- bomData.bomJson.dependencies,
1272
- );
1273
- if (
1274
- bomData.parentComponent &&
1275
- Object.keys(bomData.parentComponent).length
1276
- ) {
1277
- parentSubComponents.push(bomData.parentComponent);
1278
- }
1279
- // Retain metadata.component.components
1280
- if (bomData.parentComponent?.components?.length) {
1281
- parentSubComponents = parentSubComponents.concat(
1282
- bomData.parentComponent.components,
1283
- );
1284
- delete bomData.parentComponent.components;
1285
- }
1286
- }
1287
- }
1288
- if (hasAnyProjectType(["oci", "php"], options)) {
1289
- if (!hasAnyProjectType(["oci"], options, false)) {
1290
- thoughtLog(
1291
- "**PHP**: About to search for Composer-based projects. I hope lock files are available; otherwise, the 'composer install' command might fail for various reasons.",
1292
- );
1293
- }
1294
- setProjectTypeActivityContext("php", path);
1295
- bomData = await withOwnParent(() => createPHPBom(path, options));
1296
- // A lock with no packages can still hold the project's platform data.
1297
- if (bomData?.metadataProperties?.length) {
1298
- metadataProperties = metadataProperties.concat(
1299
- bomData.metadataProperties,
1300
- );
1301
- }
1302
- if (bomData?.bomJson?.components?.length) {
1303
- thoughtLog(
1304
- `I found ${bomData.bomJson.components.length} php packages.`,
1305
- );
1306
- if (DEBUG_MODE) {
1307
- console.log(
1308
- `Found ${bomData.bomJson.components.length} php packages at ${path}`,
1309
- );
1310
- }
1311
- components = components.concat(bomData.bomJson.components);
1312
- dependencies = mergeDependencies(
1313
- dependencies,
1314
- bomData.bomJson.dependencies,
1315
- );
1316
- if (
1317
- bomData.parentComponent &&
1318
- Object.keys(bomData.parentComponent).length
1319
- ) {
1320
- parentSubComponents.push(bomData.parentComponent);
1321
- }
1322
- // Retain metadata.component.components
1323
- if (bomData.parentComponent?.components?.length) {
1324
- parentSubComponents = parentSubComponents.concat(
1325
- bomData.parentComponent.components,
1326
- );
1327
- delete bomData.parentComponent.components;
1328
- }
1329
- }
1330
- }
1331
- if (hasAnyProjectType(["oci", "ruby"], options)) {
1332
- if (!hasAnyProjectType(["oci"], options, false)) {
1333
- thoughtLog(
1334
- "**RUBY**: Are there any Ruby projects in this path? There's only one way to know.",
1335
- );
1336
- }
1337
- setProjectTypeActivityContext("ruby", path);
1338
- bomData = await withOwnParent(() => createRubyBom(path, options));
1339
- if (bomData?.bomJson?.components?.length) {
1340
- thoughtLog(
1341
- `We got ${bomData.bomJson.components.length} ruby packages.`,
1342
- );
1343
- if (DEBUG_MODE) {
1344
- console.log(
1345
- `Found ${bomData.bomJson.components.length} ruby packages at ${path}`,
1346
- );
1347
- }
1348
- components = components.concat(bomData.bomJson.components);
1349
- dependencies = mergeDependencies(
1350
- dependencies,
1351
- bomData.bomJson.dependencies,
1352
- bomData.parentComponent,
1353
- );
1354
- if (
1355
- bomData.parentComponent &&
1356
- Object.keys(bomData.parentComponent).length
1357
- ) {
1358
- parentSubComponents.push(bomData.parentComponent);
1359
- }
1360
- // Retain metadata.component.components
1361
- if (bomData.parentComponent?.components?.length) {
1362
- parentSubComponents = parentSubComponents.concat(
1363
- bomData.parentComponent.components,
1364
- );
1365
- delete bomData.parentComponent.components;
1366
- }
1367
- }
1368
- }
1369
- if (hasAnyProjectType(["oci", "csharp"], options)) {
1370
- if (!hasAnyProjectType(["oci"], options, false)) {
1371
- thoughtLog("**CSHARP**: What about csharp and fsharp projects?");
1372
- }
1373
- setProjectTypeActivityContext("csharp", path);
1374
- bomData = await withOwnParent(() => createCsharpBom(path, options));
1375
- // Every project the scan read has a dependency entry of its own, so a
1376
- // project that uses no packages is kept along with its parent.
1377
- if (
1378
- bomData?.bomJson?.components?.length ||
1379
- bomData?.bomJson?.dependencies?.length
1380
- ) {
1381
- thoughtLog(
1382
- `There are ${bomData.bomJson.components.length} csharp packages.`,
1383
- );
1384
- if (DEBUG_MODE) {
1385
- console.log(
1386
- `Found ${bomData.bomJson.components.length} csharp packages at ${path}`,
1387
- );
1388
- }
1389
- components = components.concat(bomData.bomJson.components);
1390
- dependencies = mergeDependencies(
1391
- dependencies,
1392
- bomData.bomJson.dependencies,
1393
- );
1394
- if (
1395
- bomData.parentComponent &&
1396
- Object.keys(bomData.parentComponent).length
1397
- ) {
1398
- parentSubComponents.push(bomData.parentComponent);
1399
- }
1400
- // Retain metadata.component.components
1401
- if (bomData.parentComponent?.components?.length) {
1402
- parentSubComponents = parentSubComponents.concat(
1403
- bomData.parentComponent.components,
1404
- );
1405
- delete bomData.parentComponent.components;
1406
- }
1407
- }
1408
- }
1409
- if (hasAnyProjectType(["oci", "dart"], options)) {
1410
- if (!hasAnyProjectType(["oci"], options, false)) {
1411
- thoughtLog(
1412
- "**DART**: Looking for Dart projects. These are rare ones. Should I inform the user that they can pass the types argument via the command-line to speed things up?",
1413
- );
1414
- }
1415
- setProjectTypeActivityContext("dart", path);
1416
- bomData = await withOwnParent(() => createDartBom(path, options));
1417
- if (bomData?.bomJson?.components?.length) {
1418
- thoughtLog(
1419
- `I found ${bomData.bomJson.components.length} pub packages.`,
1420
- );
1421
- if (DEBUG_MODE) {
1422
- console.log(
1423
- `Found ${bomData.bomJson.components.length} pub packages at ${path}`,
1424
- );
1425
- }
1426
- components = components.concat(bomData.bomJson.components);
1427
- dependencies = mergeDependencies(
1428
- dependencies,
1429
- bomData.bomJson.dependencies,
1430
- );
1431
- if (
1432
- bomData.parentComponent &&
1433
- Object.keys(bomData.parentComponent).length
1434
- ) {
1435
- parentSubComponents.push(bomData.parentComponent);
1436
- }
1437
- }
1438
- }
1439
- if (hasAnyProjectType(["oci", "haskell"], options)) {
1440
- if (!hasAnyProjectType(["oci"], options, false)) {
1441
- thoughtLog(
1442
- "**HASKELL**: Looking for Haskell projects. They're rarely encountered.",
1443
- );
1444
- }
1445
- setProjectTypeActivityContext("haskell", path);
1446
- bomData = await withOwnParent(() => createHaskellBom(path, options));
1447
- if (bomData?.bomJson?.components?.length) {
1448
- thoughtLog(
1449
- `I found ${bomData.bomJson.components.length} hackage packages.`,
1450
- );
1451
- if (DEBUG_MODE) {
1452
- console.log(
1453
- `Found ${bomData.bomJson.components.length} hackage packages at ${path}`,
1454
- );
1455
- }
1456
- components = components.concat(bomData.bomJson.components);
1457
- dependencies = mergeDependencies(
1458
- dependencies,
1459
- bomData.bomJson.dependencies,
1460
- );
1461
- if (
1462
- bomData.parentComponent &&
1463
- Object.keys(bomData.parentComponent).length
1464
- ) {
1465
- parentSubComponents.push(bomData.parentComponent);
1466
- }
1467
- }
1468
- }
1469
- if (hasAnyProjectType(["oci", "elixir"], options)) {
1470
- if (!hasAnyProjectType(["oci"], options, false)) {
1471
- thoughtLog(
1472
- "**ELIXIR**: Looking for Elixir projects—they're quite rare as well.",
1473
- );
1474
- }
1475
- setProjectTypeActivityContext("elixir", path);
1476
- bomData = await withOwnParent(() => createElixirBom(path, options));
1477
- if (bomData?.bomJson?.components?.length) {
1478
- thoughtLog(
1479
- `I found ${bomData.bomJson.components.length} mix packages.`,
1480
- );
1481
- if (DEBUG_MODE) {
1482
- console.log(
1483
- `Found ${bomData.bomJson.components.length} mix packages at ${path}`,
1484
- );
1485
- }
1486
- components = components.concat(bomData.bomJson.components);
1487
- dependencies = mergeDependencies(
1488
- dependencies,
1489
- bomData.bomJson.dependencies,
1490
- );
1491
- if (
1492
- bomData.parentComponent &&
1493
- Object.keys(bomData.parentComponent).length
1494
- ) {
1495
- parentSubComponents.push(bomData.parentComponent);
1496
- }
1497
- }
1498
- }
1499
- if (hasAnyProjectType(["oci", "c"], options)) {
1500
- if (!hasAnyProjectType(["oci"], options, false)) {
1501
- thoughtLog(
1502
- "**C/C++**: Looking for C/C++ projects. Should I warn the user that the generated SBOM might have low accuracy and contain errors?",
1503
- );
1504
- }
1505
- setProjectTypeActivityContext("c", path);
1506
- bomData = await withOwnParent(() => createCppBom(path, options));
1507
- if (bomData?.bomJson?.components?.length) {
1508
- thoughtLog(
1509
- `I found ${bomData.bomJson.components.length} cpp packages.`,
1510
- );
1511
- if (DEBUG_MODE) {
1512
- console.log(
1513
- `Found ${bomData.bomJson.components.length} cpp packages at ${path}`,
1514
- );
1515
- }
1516
- components = components.concat(bomData.bomJson.components);
1517
- dependencies = mergeDependencies(
1518
- dependencies,
1519
- bomData.bomJson.dependencies,
1520
- );
1521
- if (
1522
- bomData.parentComponent &&
1523
- Object.keys(bomData.parentComponent).length
1524
- ) {
1525
- parentSubComponents.push(bomData.parentComponent);
1526
- }
1527
- }
1528
- // presets and compilers describe the build, whatever it depends on
1529
- if (bomData?.formulationList?.length) {
1530
- formulationList = formulationList.concat(bomData.formulationList);
1531
- }
1532
- }
1533
- if (hasAnyProjectType(["oci", "clojure"], options)) {
1534
- if (!hasAnyProjectType(["oci"], options, false)) {
1535
- thoughtLog(
1536
- "**CLOJURE**: Looking for Clojure projects. Should I warn the user that the purl namespace 'clojars' isn't widely supported by tools like Dependency-Track?",
1537
- );
1538
- }
1539
- setProjectTypeActivityContext("clojure", path);
1540
- bomData = await withOwnParent(() => createClojureBom(path, options));
1541
- if (bomData?.bomJson?.components?.length) {
1542
- thoughtLog(
1543
- `I found ${bomData.bomJson.components.length} clojure packages.`,
1544
- );
1545
- if (DEBUG_MODE) {
1546
- console.log(
1547
- `Found ${bomData.bomJson.components.length} clojure packages at ${path}`,
1548
- );
1549
- }
1550
- components = components.concat(bomData.bomJson.components);
1551
- dependencies = mergeDependencies(
1552
- dependencies,
1553
- bomData.bomJson.dependencies,
1554
- );
1555
- if (
1556
- bomData.parentComponent &&
1557
- Object.keys(bomData.parentComponent).length
1558
- ) {
1559
- parentSubComponents.push(bomData.parentComponent);
1560
- }
1561
- }
1562
- }
1563
- if (hasAnyProjectType(["oci", "github"], options)) {
1564
- if (!hasAnyProjectType(["oci"], options, false)) {
1565
- thoughtLog(
1566
- "**GITHUB**: Looking for any github packages and workflows.",
1567
- );
1568
- }
1569
- setProjectTypeActivityContext("github", path);
1570
- bomData = await withOwnParent(() => createGitHubBom(path, options));
1571
- if (bomData?.bomJson?.components?.length) {
1572
- thoughtLog(
1573
- `I found ${bomData.bomJson.components.length} github action packages as well. Should I convert these to formulation instead 🤔`,
1574
- );
1575
- if (DEBUG_MODE) {
1576
- console.log(
1577
- `Found ${bomData.bomJson.components.length} GitHub action packages at ${path}`,
1578
- );
1579
- }
1580
- components = components.concat(bomData.bomJson.components);
1581
- dependencies = mergeDependencies(
1582
- dependencies,
1583
- bomData.bomJson.dependencies,
1584
- );
1585
- if (
1586
- bomData.parentComponent &&
1587
- Object.keys(bomData.parentComponent).length
1588
- ) {
1589
- parentSubComponents.push(bomData.parentComponent);
1590
- }
1591
- }
1592
- }
1593
- if (hasAnyProjectType(["oci", "cloudbuild"], options)) {
1594
- if (!hasAnyProjectType(["oci"], options, false)) {
1595
- thoughtLog(
1596
- "**CLOUDBUILD**: Let's check for CloudBuild configuration files that include package dependencies.",
1597
- );
1598
- }
1599
- setProjectTypeActivityContext("cloudbuild", path);
1600
- bomData = await withOwnParent(() => createCloudBuildBom(path, options));
1601
- if (bomData?.bomJson?.components?.length) {
1602
- thoughtLog(
1603
- `I found ${bomData.bomJson.components.length} cloudbuild packages.`,
1604
- );
1605
- if (DEBUG_MODE) {
1606
- console.log(
1607
- `Found ${bomData.bomJson.components.length} CloudBuild configuration at ${path}`,
1608
- );
1609
- }
1610
- components = components.concat(bomData.bomJson.components);
1611
- dependencies = mergeDependencies(
1612
- dependencies,
1613
- bomData.bomJson.dependencies,
1614
- );
1615
- if (
1616
- bomData.parentComponent &&
1617
- Object.keys(bomData.parentComponent).length
1618
- ) {
1619
- parentSubComponents.push(bomData.parentComponent);
1620
- }
1621
- }
1622
- }
1623
- if (hasAnyProjectType(["oci", "swift"], options)) {
1624
- if (!hasAnyProjectType(["oci"], options, false)) {
1625
- thoughtLog(
1626
- "**SWIFT**: Now checking for Swift projects. We don't support CocoaPods, Objective-C, or pure Xcode projects, so the SBOM will be incomplete.",
1627
- );
1628
- }
1629
- setProjectTypeActivityContext("swift", path);
1630
- bomData = await withOwnParent(() => createSwiftBom(path, options));
1631
- if (bomData?.bomJson?.components?.length) {
1632
- thoughtLog(
1633
- `I found ${bomData.bomJson.components.length} swift packages here.`,
1634
- );
1635
- if (DEBUG_MODE) {
1636
- console.log(
1637
- `Found ${bomData.bomJson.components.length} Swift packages at ${path}`,
1638
- );
1639
- }
1640
- components = components.concat(bomData.bomJson.components);
1641
- dependencies = mergeDependencies(
1642
- dependencies,
1643
- bomData.bomJson.dependencies,
1644
- );
1645
- if (
1646
- bomData.parentComponent &&
1647
- Object.keys(bomData.parentComponent).length
1648
- ) {
1649
- parentSubComponents.push(bomData.parentComponent);
1650
- }
1651
- }
1005
+ });
1652
1006
  }
1653
- if (hasAnyProjectType(["oci", "jar", "war", "ear"], options)) {
1654
- if (!hasAnyProjectType(["oci"], options, false)) {
1655
- thoughtLog(
1656
- "**JAR**: Let's check for any bundled jar/war/ear files to improve the SBOM accuracy.",
1657
- );
1007
+ }
1008
+ if (hasAnyProjectType(["os"], options, false) && options.bomData) {
1009
+ bomData = options.bomData;
1010
+ if (bomData?.bomJson?.components) {
1011
+ if (DEBUG_MODE) {
1012
+ console.log(`Found ${bomData.bomJson.components.length} OS components`);
1658
1013
  }
1659
- setProjectTypeActivityContext("jar", path);
1660
- recordDegradation("jvm.jar.heuristic", {
1661
- ecosystem: "java",
1662
- tool: "jar",
1663
- impact: "versions",
1664
- path,
1665
- detail:
1666
- "Components are being inferred by scanning jar/war/ear files from disk.",
1667
- });
1668
- bomData = await withOwnParent(() => createJarBom(path, options));
1669
- if (bomData?.bomJson?.components?.length) {
1014
+ if (bomData.bomJson.components.length) {
1670
1015
  thoughtLog(
1671
- `I found ${bomData.bomJson.components.length} jar packages as well.`,
1672
- );
1673
- if (DEBUG_MODE) {
1674
- console.log(
1675
- `Found ${bomData.bomJson.components.length} jar packages at ${path}`,
1676
- );
1677
- }
1678
- components = components.concat(bomData.bomJson.components);
1679
- dependencies = mergeDependencies(
1680
- dependencies,
1681
- bomData.bomJson.dependencies,
1016
+ `I found ${bomData.bomJson.components.length} OS packages 😎.`,
1682
1017
  );
1683
- if (
1684
- bomData.parentComponent &&
1685
- Object.keys(bomData.parentComponent).length
1686
- ) {
1687
- parentSubComponents.push(bomData.parentComponent);
1688
- }
1689
1018
  }
1019
+ components = components.concat(bomData.bomJson.components);
1690
1020
  }
1691
- if (hasAnyProjectType(["oci", "cocoa"], options)) {
1692
- setProjectTypeActivityContext("cocoa", path);
1693
- bomData = await withOwnParent(() => createCocoaBom(path, options));
1694
- if (bomData?.bomJson?.components?.length) {
1695
- if (DEBUG_MODE) {
1696
- console.log(
1697
- `Found ${bomData.bomJson.components.length} Cocoa packages at ${path}`,
1698
- );
1699
- }
1700
- components = components.concat(bomData.bomJson.components);
1701
- dependencies = dependencies.concat(bomData.bomJson.dependencies);
1702
- if (
1703
- bomData.parentComponent &&
1704
- Object.keys(bomData.parentComponent).length
1705
- ) {
1706
- parentSubComponents.push(bomData.parentComponent);
1707
- }
1708
- }
1021
+ }
1022
+ const sharedOptions = options;
1023
+ // The exploded image layers are written once by the extractor and only read
1024
+ // from here on, so repeated walks of them can share one view of the tree.
1025
+ setDirWalkCacheRoot(options.allLayersExplodedDir);
1026
+ // The ecosystems every path is scanned for, in the order their builders run.
1027
+ // Each step reads the per-path copy of options that scanOnePathInner passes
1028
+ // in. One list merged by one loop also keeps type checking of this function
1029
+ // linear: a separate block per ecosystem, each reassigning an untyped
1030
+ // evolving array, makes TypeScript's control-flow analysis exponential in
1031
+ // the number of blocks (issue #4705).
1032
+ /**
1033
+ * @param {string} path Scanned path
1034
+ * @returns {boolean} Whether the path is a Chrome extension directory or its manifest.json
1035
+ */
1036
+ const isChromeExtensionPath = (path) => {
1037
+ if (!safeExistsSync(path)) {
1038
+ return false;
1709
1039
  }
1710
- if (hasAnyProjectType(["oci", "nix"], options)) {
1711
- setProjectTypeActivityContext("nix", path);
1712
- bomData = await withOwnParent(() => createNixBom(path, options));
1713
- if (bomData?.bomJson?.components?.length) {
1714
- if (DEBUG_MODE) {
1715
- console.log(
1716
- `Found ${bomData.bomJson.components.length} Nix flake packages at ${path}`,
1040
+ if (basename(path) === "manifest.json") {
1041
+ return true;
1042
+ }
1043
+ try {
1044
+ return (
1045
+ statSync(path).isDirectory() &&
1046
+ safeExistsSync(join(path, "manifest.json"))
1047
+ );
1048
+ } catch (_err) {
1049
+ return false;
1050
+ }
1051
+ };
1052
+ /**
1053
+ * @param {string} thought Thought log written unless the scan is of a container image
1054
+ * @returns {(path: string, scanOptions: Object) => void} The announce hook
1055
+ */
1056
+ const ociQuiet = (thought) => (_path, scanOptions) => {
1057
+ if (!hasAnyProjectType(["oci"], scanOptions, false)) {
1058
+ thoughtLog(thought);
1059
+ }
1060
+ };
1061
+ /** @type {PathScanStep[]} */
1062
+ const pathScanSteps = [
1063
+ {
1064
+ // Node.js and AI inventory
1065
+ applies: (path, scanOptions) =>
1066
+ hasAnyProjectType(["oci", "js"], scanOptions) ||
1067
+ hasAnyProjectType(["mcp", "ai-skill", "ai"], scanOptions) ||
1068
+ !!getDirectAiInventoryType(path, scanOptions),
1069
+ prepare: (path, scanOptions) =>
1070
+ getDirectAiInventoryType(path, scanOptions),
1071
+ announce: (_path, scanOptions, exactAiInventoryType) => {
1072
+ if (hasAnyProjectType(["oci"], scanOptions, false)) {
1073
+ return;
1074
+ }
1075
+ if (exactAiInventoryType === "mcp") {
1076
+ thoughtLog(
1077
+ "**MCP**: Looking for MCP services, MCP configs, and related AI control-plane artifacts.",
1078
+ );
1079
+ } else if (exactAiInventoryType === "ai") {
1080
+ thoughtLog(
1081
+ "**AI-BOM**: Looking for AI models, direct model metadata, pedigree, quantization, and operator-facing AI context.",
1082
+ );
1083
+ } else if (exactAiInventoryType === "ai-skill") {
1084
+ thoughtLog(
1085
+ "**AI-SKILL**: Looking for AI instruction, skill, and agent-definition files that can influence build or release flows.",
1086
+ );
1087
+ } else {
1088
+ thoughtLog(
1089
+ "**JS**: Now looking for JavaScript projects (npm, yarn, pnpm) and files.",
1717
1090
  );
1718
1091
  }
1719
- components = components.concat(bomData.bomJson.components);
1720
- dependencies = mergeDependencies(
1721
- dependencies,
1722
- bomData.bomJson.dependencies,
1723
- );
1092
+ },
1093
+ activityType: (exactAiInventoryType) => exactAiInventoryType || "js",
1094
+ build: createNodejsBom,
1095
+ foundThought: (count, _path, exactAiInventoryType) =>
1096
+ exactAiInventoryType
1097
+ ? `I found ${count} ${exactAiInventoryType} component(s). Let's keep looking.`
1098
+ : `I found ${count} npm packages. Let's keep looking.`,
1099
+ debugMessage: (count, path, exactAiInventoryType) =>
1100
+ `Found ${count} ${exactAiInventoryType || "npm"} components at ${path}`,
1101
+ mergeServices: true,
1102
+ hoistSubComponents: "plain",
1103
+ },
1104
+ {
1105
+ applies: (_path, scanOptions) =>
1106
+ hasAnyProjectType(["oci", "java"], scanOptions),
1107
+ announce: ociQuiet(
1108
+ "**JAVA**: Looking for Java projects (e.g., Maven, Gradle, SBT). I hope all configurations—from Java version to individual build settings—are correctly aligned.",
1109
+ ),
1110
+ activityType: "java",
1111
+ build: createJavaBom,
1112
+ foundThought: (count) => `I found ${count} java packages.`,
1113
+ debugMessage: (count, path) => `Found ${count} java packages at ${path}`,
1114
+ hoistSubComponents: "gradle",
1115
+ },
1116
+ {
1117
+ applies: (_path, scanOptions) =>
1118
+ hasAnyProjectType(["oci", "py"], scanOptions),
1119
+ announce: (path, scanOptions) => {
1120
+ ociQuiet(
1121
+ "**PYTHON**: Looking for Python projects with package managers such as pip, poetry, uv, etc. Wish me good luck!",
1122
+ )(path, scanOptions);
1724
1123
  if (
1725
- bomData.parentComponent &&
1726
- Object.keys(bomData.parentComponent).length
1124
+ readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true" &&
1125
+ pathList.length <= 2
1727
1126
  ) {
1728
- parentSubComponents.push(bomData.parentComponent);
1729
- }
1730
- }
1731
- }
1732
- if (hasAnyProjectType(["caxa"], options, false)) {
1733
- setProjectTypeActivityContext("caxa", path);
1734
- bomData = await withOwnParent(() => createCaxaBom(path, options));
1735
- if (bomData?.bomJson?.components?.length) {
1736
- if (DEBUG_MODE) {
1737
- console.log(
1738
- `Found ${bomData.bomJson.components.length} caxa packages at ${path}`,
1127
+ thoughtLog(
1128
+ "I'm running in a non-container environment. Let's hope the correct build tools are available ✌️.",
1739
1129
  );
1740
1130
  }
1741
- components = components.concat(bomData.bomJson.components);
1742
- dependencies = mergeDependencies(
1743
- dependencies,
1744
- bomData.bomJson.dependencies,
1745
- );
1746
- if (
1747
- bomData.parentComponent &&
1748
- Object.keys(bomData.parentComponent).length
1749
- ) {
1750
- parentSubComponents.push(bomData.parentComponent);
1131
+ },
1132
+ activityType: "py",
1133
+ build: createPythonBom,
1134
+ foundThought: (count) => `I found ${count} python packages.`,
1135
+ debugMessage: (count, path) =>
1136
+ `Found ${count} python packages at ${path}`,
1137
+ // uv workspace members are nested under the python project, so hoisting
1138
+ // them keeps them siblings of the other sub-components instead of riding
1139
+ // one level deeper (or being dropped by the same-name collapse) when
1140
+ // another ecosystem's project becomes the parent.
1141
+ hoistSubComponents: "plain",
1142
+ collectFormulation: true,
1143
+ },
1144
+ {
1145
+ applies: (_path, scanOptions) =>
1146
+ hasAnyProjectType(["oci", "go"], scanOptions),
1147
+ announce: ociQuiet(
1148
+ "**GO**: Looking for go projects. I need to be cautious about purl namespaces and potential failures with the 'go list' command.",
1149
+ ),
1150
+ activityType: "go",
1151
+ build: createGoBom,
1152
+ foundThought: (count) => `I found ${count} go packages.`,
1153
+ debugMessage: (count, path) => `Found ${count} go packages at ${path}`,
1154
+ },
1155
+ {
1156
+ applies: (_path, scanOptions) =>
1157
+ hasAnyProjectType(["oci", "rust"], scanOptions),
1158
+ announce: ociQuiet(
1159
+ "**RUST**: Let's search for Cargo/Rust projects. Should I warn the user that we don't support Cargo 'features' and native dependencies, which may lead to both false positives and false negatives? 🤔?",
1160
+ ),
1161
+ activityType: "rust",
1162
+ build: createRustBom,
1163
+ foundThought: (count) => `I found ${count} rust packages.`,
1164
+ debugMessage: (count, path) => `Found ${count} rust packages at ${path}`,
1165
+ hoistSubComponents: "plain",
1166
+ },
1167
+ {
1168
+ applies: (_path, scanOptions) =>
1169
+ hasAnyProjectType(["oci", "php"], scanOptions),
1170
+ announce: ociQuiet(
1171
+ "**PHP**: About to search for Composer-based projects. I hope lock files are available; otherwise, the 'composer install' command might fail for various reasons.",
1172
+ ),
1173
+ activityType: "php",
1174
+ build: createPHPBom,
1175
+ // A lock with no packages can still hold the project's platform data.
1176
+ collectMetadataProperties: true,
1177
+ foundThought: (count) => `I found ${count} php packages.`,
1178
+ debugMessage: (count, path) => `Found ${count} php packages at ${path}`,
1179
+ hoistSubComponents: "plain",
1180
+ },
1181
+ {
1182
+ applies: (_path, scanOptions) =>
1183
+ hasAnyProjectType(["oci", "ruby"], scanOptions),
1184
+ announce: ociQuiet(
1185
+ "**RUBY**: Are there any Ruby projects in this path? There's only one way to know.",
1186
+ ),
1187
+ activityType: "ruby",
1188
+ build: createRubyBom,
1189
+ foundThought: (count) => `We got ${count} ruby packages.`,
1190
+ debugMessage: (count, path) => `Found ${count} ruby packages at ${path}`,
1191
+ dependencyMerge: "mergeWithParent",
1192
+ hoistSubComponents: "plain",
1193
+ },
1194
+ {
1195
+ applies: (_path, scanOptions) =>
1196
+ hasAnyProjectType(["oci", "csharp"], scanOptions),
1197
+ announce: ociQuiet("**CSHARP**: What about csharp and fsharp projects?"),
1198
+ activityType: "csharp",
1199
+ build: createCsharpBom,
1200
+ // Every project the scan read has a dependency entry of its own, so a
1201
+ // project that uses no packages is kept along with its parent.
1202
+ isUseful: (bomData) =>
1203
+ !!(
1204
+ bomData?.bomJson?.components?.length ||
1205
+ bomData?.bomJson?.dependencies?.length
1206
+ ),
1207
+ foundThought: (count) => `There are ${count} csharp packages.`,
1208
+ debugMessage: (count, path) =>
1209
+ `Found ${count} csharp packages at ${path}`,
1210
+ hoistSubComponents: "plain",
1211
+ },
1212
+ {
1213
+ applies: (_path, scanOptions) =>
1214
+ hasAnyProjectType(["oci", "dart"], scanOptions),
1215
+ announce: ociQuiet(
1216
+ "**DART**: Looking for Dart projects. These are rare ones. Should I inform the user that they can pass the types argument via the command-line to speed things up?",
1217
+ ),
1218
+ activityType: "dart",
1219
+ build: createDartBom,
1220
+ foundThought: (count) => `I found ${count} pub packages.`,
1221
+ debugMessage: (count, path) => `Found ${count} pub packages at ${path}`,
1222
+ },
1223
+ {
1224
+ applies: (_path, scanOptions) =>
1225
+ hasAnyProjectType(["oci", "haskell"], scanOptions),
1226
+ announce: ociQuiet(
1227
+ "**HASKELL**: Looking for Haskell projects. They're rarely encountered.",
1228
+ ),
1229
+ activityType: "haskell",
1230
+ build: createHaskellBom,
1231
+ foundThought: (count) => `I found ${count} hackage packages.`,
1232
+ debugMessage: (count, path) =>
1233
+ `Found ${count} hackage packages at ${path}`,
1234
+ },
1235
+ {
1236
+ applies: (_path, scanOptions) =>
1237
+ hasAnyProjectType(["oci", "elixir"], scanOptions),
1238
+ announce: ociQuiet(
1239
+ "**ELIXIR**: Looking for Elixir projects—they're quite rare as well.",
1240
+ ),
1241
+ activityType: "elixir",
1242
+ build: createElixirBom,
1243
+ foundThought: (count) => `I found ${count} mix packages.`,
1244
+ debugMessage: (count, path) => `Found ${count} mix packages at ${path}`,
1245
+ },
1246
+ {
1247
+ applies: (_path, scanOptions) =>
1248
+ hasAnyProjectType(["oci", "c"], scanOptions),
1249
+ announce: ociQuiet(
1250
+ "**C/C++**: Looking for C/C++ projects. Should I warn the user that the generated SBOM might have low accuracy and contain errors?",
1251
+ ),
1252
+ activityType: "c",
1253
+ build: createCppBom,
1254
+ foundThought: (count) => `I found ${count} cpp packages.`,
1255
+ debugMessage: (count, path) => `Found ${count} cpp packages at ${path}`,
1256
+ // presets and compilers describe the build, whatever it depends on
1257
+ collectFormulation: true,
1258
+ },
1259
+ {
1260
+ applies: (_path, scanOptions) =>
1261
+ hasAnyProjectType(["oci", "clojure"], scanOptions),
1262
+ announce: ociQuiet(
1263
+ "**CLOJURE**: Looking for Clojure projects. Should I warn the user that the purl namespace 'clojars' isn't widely supported by tools like Dependency-Track?",
1264
+ ),
1265
+ activityType: "clojure",
1266
+ build: createClojureBom,
1267
+ foundThought: (count) => `I found ${count} clojure packages.`,
1268
+ debugMessage: (count, path) =>
1269
+ `Found ${count} clojure packages at ${path}`,
1270
+ },
1271
+ {
1272
+ applies: (_path, scanOptions) =>
1273
+ hasAnyProjectType(["oci", "github"], scanOptions),
1274
+ announce: ociQuiet(
1275
+ "**GITHUB**: Looking for any github packages and workflows.",
1276
+ ),
1277
+ activityType: "github",
1278
+ build: createGitHubBom,
1279
+ foundThought: (count) =>
1280
+ `I found ${count} github action packages as well. Should I convert these to formulation instead 🤔`,
1281
+ debugMessage: (count, path) =>
1282
+ `Found ${count} GitHub action packages at ${path}`,
1283
+ },
1284
+ {
1285
+ applies: (_path, scanOptions) =>
1286
+ hasAnyProjectType(["oci", "cloudbuild"], scanOptions),
1287
+ announce: ociQuiet(
1288
+ "**CLOUDBUILD**: Let's check for CloudBuild configuration files that include package dependencies.",
1289
+ ),
1290
+ activityType: "cloudbuild",
1291
+ build: createCloudBuildBom,
1292
+ foundThought: (count) => `I found ${count} cloudbuild packages.`,
1293
+ debugMessage: (count, path) =>
1294
+ `Found ${count} CloudBuild configuration at ${path}`,
1295
+ },
1296
+ {
1297
+ applies: (_path, scanOptions) =>
1298
+ hasAnyProjectType(["oci", "swift"], scanOptions),
1299
+ announce: ociQuiet(
1300
+ "**SWIFT**: Now checking for Swift projects. We don't support CocoaPods, Objective-C, or pure Xcode projects, so the SBOM will be incomplete.",
1301
+ ),
1302
+ activityType: "swift",
1303
+ build: createSwiftBom,
1304
+ foundThought: (count) => `I found ${count} swift packages here.`,
1305
+ debugMessage: (count, path) => `Found ${count} Swift packages at ${path}`,
1306
+ },
1307
+ {
1308
+ applies: (_path, scanOptions) =>
1309
+ hasAnyProjectType(["oci", "jar", "war", "ear"], scanOptions),
1310
+ announce: ociQuiet(
1311
+ "**JAR**: Let's check for any bundled jar/war/ear files to improve the SBOM accuracy.",
1312
+ ),
1313
+ activityType: "jar",
1314
+ beforeBuild: (path) => {
1315
+ recordDegradation("jvm.jar.heuristic", {
1316
+ ecosystem: "java",
1317
+ tool: "jar",
1318
+ impact: "versions",
1319
+ path,
1320
+ detail:
1321
+ "Components are being inferred by scanning jar/war/ear files from disk.",
1322
+ });
1323
+ },
1324
+ build: createJarBom,
1325
+ foundThought: (count) => `I found ${count} jar packages as well.`,
1326
+ debugMessage: (count, path) => `Found ${count} jar packages at ${path}`,
1327
+ },
1328
+ {
1329
+ applies: (_path, scanOptions) =>
1330
+ hasAnyProjectType(["oci", "cocoa"], scanOptions),
1331
+ activityType: "cocoa",
1332
+ build: createCocoaBom,
1333
+ debugMessage: (count, path) => `Found ${count} Cocoa packages at ${path}`,
1334
+ dependencyMerge: "concat",
1335
+ },
1336
+ {
1337
+ applies: (_path, scanOptions) =>
1338
+ hasAnyProjectType(["oci", "nix"], scanOptions),
1339
+ activityType: "nix",
1340
+ build: createNixBom,
1341
+ debugMessage: (count, path) =>
1342
+ `Found ${count} Nix flake packages at ${path}`,
1343
+ },
1344
+ {
1345
+ applies: (_path, scanOptions) =>
1346
+ hasAnyProjectType(["caxa"], scanOptions, false),
1347
+ activityType: "caxa",
1348
+ build: createCaxaBom,
1349
+ debugMessage: (count, path) => `Found ${count} caxa packages at ${path}`,
1350
+ },
1351
+ {
1352
+ applies: (_path, scanOptions) =>
1353
+ hasAnyProjectType(["asar"], scanOptions, false),
1354
+ activityType: "asar",
1355
+ build: createAsarBom,
1356
+ debugMessage: (count, path) =>
1357
+ `Found ${count} ASAR component(s) at ${path}`,
1358
+ },
1359
+ {
1360
+ applies: (_path, scanOptions) =>
1361
+ hasAnyProjectType(["vscode-extension"], scanOptions, false),
1362
+ activityType: "vscode-extension",
1363
+ build: createVscodeExtensionBom,
1364
+ debugMessage: (count, path) =>
1365
+ `Found ${count} VS Code extension(s) at ${path}`,
1366
+ },
1367
+ {
1368
+ // A path that is a Chrome extension is always scanned. Any other path
1369
+ // scans the extensions installed on this host, which happens once per
1370
+ // run however many paths there are.
1371
+ applies: (path, scanOptions) =>
1372
+ hasAnyProjectType(["chrome-extension"], scanOptions, false) &&
1373
+ (isChromeExtensionPath(path) ||
1374
+ !sharedOptions.__didScanChromeExtensions),
1375
+ beforeBuild: (path) => {
1376
+ if (!isChromeExtensionPath(path)) {
1377
+ sharedOptions.__didScanChromeExtensions = true;
1751
1378
  }
1379
+ },
1380
+ activityType: "chrome-extension",
1381
+ build: createChromeExtensionBom,
1382
+ debugMessage: (count) =>
1383
+ `Found ${count} Chrome extension(s) on this host`,
1384
+ },
1385
+ {
1386
+ applies: (_path, scanOptions) =>
1387
+ hasAnyProjectType(["terraform"], scanOptions),
1388
+ announce: () => {
1389
+ thoughtLog(
1390
+ "**TERRAFORM**: Looking for Terraform and OpenTofu modules and providers. The registry metadata fetch is opt-in, so I will rely on the lock file, the module manifest and the configuration files.",
1391
+ );
1392
+ },
1393
+ activityType: "terraform",
1394
+ build: createTerraformBom,
1395
+ foundThought: (count) =>
1396
+ `I found ${count} terraform components. Should I also resolve their licenses from the registry when FETCH_LICENSE is enabled?`,
1397
+ debugMessage: (count, path) =>
1398
+ `Found ${count} terraform components at ${path}`,
1399
+ },
1400
+ {
1401
+ // Collect any crypto keys
1402
+ applies: (_path, scanOptions) =>
1403
+ scanOptions.specVersion >= 1.6 &&
1404
+ !!scanOptions.includeCrypto &&
1405
+ isCycloneDxComponentTypeEnabled("cryptographic-asset", scanOptions),
1406
+ announce: ociQuiet(
1407
+ "**CBOM**: Wait, the user wants me to look for cryptographic assets. Let's check thoroughly.",
1408
+ ),
1409
+ activityType: "cbom",
1410
+ build: createCryptoCertsBom,
1411
+ foundThought: (count) => `I found ${count} crypto assets.`,
1412
+ debugMessage: (count, path) => `Found ${count} crypto assets at ${path}`,
1413
+ dependencyMerge: "topLevel",
1414
+ keepParent: false,
1415
+ },
1416
+ ];
1417
+ const scanOnePath = async (path) => {
1418
+ try {
1419
+ return await scanOnePathInner(path);
1420
+ } catch (err) {
1421
+ if (!isDeferredFailOnError(err)) {
1422
+ throw err;
1752
1423
  }
1424
+ // The failing path contributes nothing, and the remaining paths still
1425
+ // do: fail-on-error scopes the loss to the leg that failed.
1426
+ return {
1427
+ components: [],
1428
+ dependencies: [],
1429
+ options: { ...sharedOptions },
1430
+ parentSubComponents: [],
1431
+ metadataProperties: [],
1432
+ formulationList: [],
1433
+ };
1753
1434
  }
1754
- if (hasAnyProjectType(["asar"], options, false)) {
1755
- setProjectTypeActivityContext("asar", path);
1756
- bomData = await withOwnParent(() => createAsarBom(path, options));
1757
- if (bomData?.bomJson?.components?.length) {
1758
- if (DEBUG_MODE) {
1759
- console.log(
1760
- `Found ${bomData.bomJson.components.length} ASAR component(s) at ${path}`,
1761
- );
1762
- }
1763
- components = components.concat(bomData.bomJson.components);
1764
- dependencies = mergeDependencies(
1765
- dependencies,
1766
- bomData.bomJson.dependencies,
1767
- );
1768
- if (
1769
- bomData.parentComponent &&
1770
- Object.keys(bomData.parentComponent).length
1771
- ) {
1772
- parentSubComponents.push(bomData.parentComponent);
1773
- }
1435
+ };
1436
+ const scanOnePathInner = async (path) => {
1437
+ // Each path accumulates into its own lists and its own view of options, so
1438
+ // that legs which record a parent component or a tool cannot overwrite
1439
+ // another path's. The lists are merged below in pathList order.
1440
+ const options = { ...sharedOptions };
1441
+ /** @type {BuilderBomData | undefined} */
1442
+ let bomData;
1443
+ /** @type {Object[]} */
1444
+ let components = [];
1445
+ /** @type {Object[]} */
1446
+ let dependencies = [];
1447
+ /** @type {Object[]} */
1448
+ let parentSubComponents = [];
1449
+ /** @type {Object[]} */
1450
+ let metadataProperties = [];
1451
+ /** @type {Object[]} */
1452
+ let pathFormulationList = [];
1453
+ // Each builder detects the project of its own ecosystem. A builder that sets
1454
+ // options.parentComponent to force its project into its own BOM must not
1455
+ // make that project the next builder's too, or the next ecosystem's project
1456
+ // is lost: every builder starts from the parent the caller gave.
1457
+ const callerParent = options.parentComponent;
1458
+ const withOwnParent = async (build) => {
1459
+ options.parentComponent = callerParent;
1460
+ try {
1461
+ return await build();
1462
+ } finally {
1463
+ options.parentComponent = callerParent;
1774
1464
  }
1465
+ };
1466
+ setActivityContext({
1467
+ projectType: Array.isArray(options.projectType)
1468
+ ? options.projectType.join(",")
1469
+ : options.projectType,
1470
+ sourcePath: path,
1471
+ });
1472
+ recordActivity({
1473
+ kind: "read",
1474
+ reason:
1475
+ "Scanning project path for supported languages and package managers.",
1476
+ status: "completed",
1477
+ target: path,
1478
+ });
1479
+ if (DEBUG_MODE) {
1480
+ console.log("Scanning", path);
1481
+ }
1482
+ if (pathList.length > 2) {
1483
+ thoughtLog(`Let's thoroughly check the path ${path}.`);
1775
1484
  }
1776
- if (hasAnyProjectType(["vscode-extension"], options, false)) {
1777
- setProjectTypeActivityContext("vscode-extension", path);
1778
- bomData = await withOwnParent(() =>
1779
- createVscodeExtensionBom(path, options),
1485
+ for (const step of pathScanSteps) {
1486
+ if (!step.applies(path, options)) {
1487
+ continue;
1488
+ }
1489
+ const context = step.prepare?.(path, options);
1490
+ step.announce?.(path, options, context);
1491
+ setProjectTypeActivityContext(
1492
+ typeof step.activityType === "function"
1493
+ ? step.activityType(context)
1494
+ : step.activityType,
1495
+ path,
1780
1496
  );
1781
- if (bomData?.bomJson?.components?.length) {
1782
- if (DEBUG_MODE) {
1783
- console.log(
1784
- `Found ${bomData.bomJson.components.length} VS Code extension(s) at ${path}`,
1785
- );
1786
- }
1787
- components = components.concat(bomData.bomJson.components);
1788
- dependencies = mergeDependencies(
1789
- dependencies,
1790
- bomData.bomJson.dependencies,
1497
+ step.beforeBuild?.(path);
1498
+ const { build } = step;
1499
+ bomData = /** @type {BuilderBomData | undefined} */ (
1500
+ await withOwnParent(() => build(path, options))
1501
+ );
1502
+ if (
1503
+ step.collectMetadataProperties &&
1504
+ bomData?.metadataProperties?.length
1505
+ ) {
1506
+ metadataProperties = metadataProperties.concat(
1507
+ bomData.metadataProperties,
1791
1508
  );
1792
- if (
1793
- bomData.parentComponent &&
1794
- Object.keys(bomData.parentComponent).length
1795
- ) {
1796
- parentSubComponents.push(bomData.parentComponent);
1797
- }
1798
1509
  }
1799
- }
1800
- if (hasAnyProjectType(["chrome-extension"], options, false)) {
1801
- let isExplicitChromeExtensionPath = false;
1802
- if (safeExistsSync(path)) {
1803
- if (basename(path) === "manifest.json") {
1804
- isExplicitChromeExtensionPath = true;
1805
- } else {
1806
- try {
1807
- isExplicitChromeExtensionPath =
1808
- statSync(path).isDirectory() &&
1809
- safeExistsSync(join(path, "manifest.json"));
1810
- } catch (_err) {
1811
- isExplicitChromeExtensionPath = false;
1812
- }
1510
+ const isUseful = step.isUseful
1511
+ ? step.isUseful(bomData)
1512
+ : !!bomData?.bomJson?.components?.length;
1513
+ if (isUseful) {
1514
+ // a csharp result can be kept for its dependencies alone
1515
+ const count = bomData.bomJson.components?.length || 0;
1516
+ const thought = step.foundThought?.(count, path, context);
1517
+ if (thought) {
1518
+ thoughtLog(thought);
1813
1519
  }
1814
- }
1815
- if (
1816
- isExplicitChromeExtensionPath ||
1817
- !sharedOptions.__didScanChromeExtensions
1818
- ) {
1819
- if (!isExplicitChromeExtensionPath) {
1820
- sharedOptions.__didScanChromeExtensions = true;
1520
+ if (DEBUG_MODE) {
1521
+ console.log(step.debugMessage(count, path, context));
1821
1522
  }
1822
- setProjectTypeActivityContext("chrome-extension", path);
1823
- bomData = await withOwnParent(() =>
1824
- createChromeExtensionBom(path, options),
1825
- );
1826
- if (bomData?.bomJson?.components?.length) {
1827
- if (DEBUG_MODE) {
1828
- console.log(
1829
- `Found ${bomData.bomJson.components.length} Chrome extension(s) on this host`,
1523
+ components = components.concat(bomData.bomJson.components || []);
1524
+ if (step.dependencyMerge === "concat") {
1525
+ dependencies = dependencies.concat(
1526
+ bomData.bomJson.dependencies || [],
1527
+ );
1528
+ } else if (step.dependencyMerge === "topLevel") {
1529
+ // the crypto scan returns its dependencies beside bomJson
1530
+ if (bomData.dependencies?.length) {
1531
+ dependencies = mergeDependencies(
1532
+ dependencies,
1533
+ bomData.dependencies,
1830
1534
  );
1831
1535
  }
1832
- components = components.concat(bomData.bomJson.components);
1536
+ } else if (step.dependencyMerge === "mergeWithParent") {
1537
+ dependencies = mergeDependencies(
1538
+ dependencies,
1539
+ bomData.bomJson.dependencies,
1540
+ bomData.parentComponent,
1541
+ );
1542
+ } else {
1833
1543
  dependencies = mergeDependencies(
1834
1544
  dependencies,
1835
1545
  bomData.bomJson.dependencies,
1836
1546
  );
1837
- if (
1838
- bomData.parentComponent &&
1839
- Object.keys(bomData.parentComponent).length
1840
- ) {
1841
- parentSubComponents.push(bomData.parentComponent);
1842
- }
1843
1547
  }
1844
- }
1845
- }
1846
- if (hasAnyProjectType(["terraform"], options)) {
1847
- thoughtLog(
1848
- "**TERRAFORM**: Looking for Terraform and OpenTofu modules and providers. The registry metadata fetch is opt-in, so I will rely on the lock file, the module manifest and the configuration files.",
1849
- );
1850
- setProjectTypeActivityContext("terraform", path);
1851
- bomData = await withOwnParent(() => createTerraformBom(path, options));
1852
- if (bomData?.bomJson?.components?.length) {
1853
- thoughtLog(
1854
- `I found ${bomData.bomJson.components.length} terraform components. Should I also resolve their licenses from the registry when FETCH_LICENSE is enabled?`,
1855
- );
1856
- if (DEBUG_MODE) {
1857
- console.log(
1858
- `Found ${bomData.bomJson.components.length} terraform components at ${path}`,
1548
+ if (step.mergeServices && bomData?.bomJson?.services?.length) {
1549
+ options.services = mergeServices(
1550
+ options.services || [],
1551
+ bomData.bomJson.services,
1859
1552
  );
1860
1553
  }
1861
- components = components.concat(bomData.bomJson.components);
1862
- dependencies = mergeDependencies(
1863
- dependencies,
1864
- bomData.bomJson.dependencies,
1865
- );
1866
1554
  if (
1555
+ step.keepParent !== false &&
1867
1556
  bomData.parentComponent &&
1868
1557
  Object.keys(bomData.parentComponent).length
1869
1558
  ) {
1870
1559
  parentSubComponents.push(bomData.parentComponent);
1871
1560
  }
1561
+ // Retain metadata.component.components
1562
+ if (
1563
+ step.hoistSubComponents &&
1564
+ bomData.parentComponent?.components?.length
1565
+ ) {
1566
+ let bomSubComponents = bomData.parentComponent.components;
1567
+ // For java, sub-components that are already components are added
1568
+ // to the list of current components and removed from
1569
+ // metadata.component.components; the components are merged later.
1570
+ if (
1571
+ step.hoistSubComponents === "gradle" &&
1572
+ !["false", "0"].includes(
1573
+ readEnvironmentVariable("GRADLE_RESOLVE_FROM_NODE"),
1574
+ )
1575
+ ) {
1576
+ thoughtLog(
1577
+ "Wait, the user wants me to resolve gradle projects from npm.",
1578
+ );
1579
+ const allRefs = components.map((c) => c["bom-ref"]);
1580
+ const duplicateComponents = bomSubComponents.filter((c) =>
1581
+ allRefs.includes(c["bom-ref"]),
1582
+ );
1583
+ components = components.concat(duplicateComponents);
1584
+ const duplicateComponentRefs = duplicateComponents.map(
1585
+ (c) => c["bom-ref"],
1586
+ );
1587
+ bomSubComponents = bomSubComponents.filter(
1588
+ (c) => !duplicateComponentRefs.includes(c["bom-ref"]),
1589
+ );
1590
+ }
1591
+ parentSubComponents = parentSubComponents.concat(bomSubComponents);
1592
+ delete bomData.parentComponent.components;
1593
+ }
1872
1594
  }
1873
- }
1874
- // Collect any crypto keys
1875
- if (
1876
- options.specVersion >= 1.6 &&
1877
- options.includeCrypto &&
1878
- isCycloneDxComponentTypeEnabled("cryptographic-asset", options)
1879
- ) {
1880
- if (!hasAnyProjectType(["oci"], options, false)) {
1881
- thoughtLog(
1882
- "**CBOM**: Wait, the user wants me to look for cryptographic assets. Let's check thoroughly.",
1883
- );
1884
- }
1885
- setProjectTypeActivityContext("cbom", path);
1886
- bomData = await withOwnParent(() => createCryptoCertsBom(path, options));
1887
- if (bomData?.bomJson?.components?.length) {
1888
- thoughtLog(
1889
- `I found ${bomData.bomJson.components.length} crypto assets.`,
1595
+ if (step.collectFormulation && bomData?.formulationList?.length) {
1596
+ pathFormulationList = pathFormulationList.concat(
1597
+ bomData.formulationList,
1890
1598
  );
1891
- if (DEBUG_MODE) {
1892
- console.log(
1893
- `Found ${bomData.bomJson.components.length} crypto assets at ${path}`,
1894
- );
1895
- }
1896
- components = components.concat(bomData.bomJson.components);
1897
- if (bomData.dependencies?.length) {
1898
- dependencies = mergeDependencies(dependencies, bomData.dependencies);
1899
- }
1900
1599
  }
1901
1600
  }
1902
1601
  return {
@@ -1905,6 +1604,7 @@ export async function createMultiXBom(pathList, options) {
1905
1604
  options,
1906
1605
  parentSubComponents,
1907
1606
  metadataProperties,
1607
+ formulationList: pathFormulationList,
1908
1608
  };
1909
1609
  };
1910
1610
  const pathResults = [];
@@ -1971,6 +1671,7 @@ export async function createMultiXBom(pathList, options) {
1971
1671
  metadataProperties = metadataProperties.concat(
1972
1672
  pathResult.metadataProperties,
1973
1673
  );
1674
+ formulationList = formulationList.concat(pathResult.formulationList);
1974
1675
  if (pathResult.options.services?.length) {
1975
1676
  sharedOptions.services = mergeServices(
1976
1677
  sharedOptions.services || [],
@@ -3158,6 +2859,16 @@ async function createBomForPath(path, options) {
3158
2859
  * @throws {Error} if the request fails
3159
2860
  */
3160
2861
  export async function submitBom(args, bomContents) {
2862
+ // Dependency-Track ingests CycloneDX 1.x only. The CLI and the server refuse
2863
+ // a 2.0 upload before generating; this covers library callers.
2864
+ if (
2865
+ isCycloneDx20SpecVersion(bomContents?.specVersion) ||
2866
+ bomContents?.specFormat !== undefined
2867
+ ) {
2868
+ console.log(DEPENDENCY_TRACK_CDX20_ERROR);
2869
+ args.failOnError && process.exit(1);
2870
+ return undefined;
2871
+ }
3161
2872
  const dependencyTrackApiUrl = getDependencyTrackBomApiUrl(args.serverUrl);
3162
2873
  const serverUrl = dependencyTrackApiUrl?.toString();
3163
2874
  if (!dependencyTrackApiUrl || !serverUrl) {