@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
@@ -13,9 +13,11 @@ import {
13
13
  getTmpDir,
14
14
  multiChecksumFile,
15
15
  safeExistsSync,
16
+ safeMkdirSync,
16
17
  safeMkdtempSync,
17
18
  safeRmSync,
18
19
  safeSpawnSync,
20
+ safeWriteSync,
19
21
  } from "../core/fs.js";
20
22
  import { isWin } from "../core/paths.js";
21
23
  import { xml2js } from "../parsers/xml.js";
@@ -34,6 +36,10 @@ import { findLicenseId, spdxLicenses } from "./spdx.js";
34
36
 
35
37
  const jarNSMapping_cache = new Map();
36
38
 
39
+ // The largest classes.jar on Google's Maven repository is a few tens of MiB,
40
+ // so this bound only stops an archive that claims an implausible size.
41
+ const MAX_AAR_INNER_JAR_BYTES = 256 * 1024 * 1024;
42
+
37
43
  /**
38
44
  * Whether a jar holds sources or javadoc rather than classes.
39
45
  *
@@ -193,9 +199,10 @@ export async function collectMvnDependencies(
193
199
  }
194
200
 
195
201
  /**
196
- * Method to collect class names from all jars in a directory
202
+ * Method to collect class names from all jars and Android libraries in a
203
+ * directory
197
204
  *
198
- * @param {string} jarPath Path containing jars
205
+ * @param {string} jarPath Path containing jars, or a single jar or aar file
199
206
  * @param {object} pomPathMap Map containing jar to pom names. Required to successfully parse gradle cache.
200
207
  *
201
208
  * @return object containing jar name and class list
@@ -213,20 +220,21 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
213
220
  )}`;
214
221
  }
215
222
  // Parse jar files to get class names
216
- const jarFiles = jarPath.endsWith(".jar")
223
+ const jarFiles = /\.(jar|aar)$/i.test(jarPath)
217
224
  ? [jarPath]
218
- : getAllFiles(jarPath, "**/*.jar");
225
+ : getAllFiles(jarPath, "**/*.{jar,aar}");
219
226
  if (jarFiles?.length) {
220
227
  for (const jf of jarFiles) {
221
228
  const jarName = basename(jf);
229
+ const isAar = jarName.toLowerCase().endsWith(".aar");
222
230
  // Sources and javadoc jars hold no runtime classes. Reading them would
223
231
  // attach their hashes to the main artifact's purl.
224
232
  if (isDocumentationJar(jarName)) {
225
233
  continue;
226
234
  }
227
- let pomname =
228
- pomPathMap[jarName.replace(".jar", ".pom")] ||
229
- jf.replace(".jar", ".pom");
235
+ // Only the extension is replaced, never a match earlier in the path.
236
+ const toPom = (file) => `${file.slice(0, -".jar".length)}.pom`;
237
+ let pomname = pomPathMap[toPom(jarName)] || toPom(jf);
230
238
  let pomData;
231
239
  let purl;
232
240
  let manifestLicenses;
@@ -235,7 +243,12 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
235
243
  let searchDir = dirname(jf);
236
244
  // in case of gradle, there would be hash directory that is different for jar vs pom
237
245
  // so we need to start search from a level up
238
- if (searchDir.includes(join(".gradle", "caches"))) {
246
+ if (
247
+ searchDir.includes(join(".gradle", "caches")) ||
248
+ // A copied cache, or one reached through GRADLE_CACHE_DIR, keeps
249
+ // the files-2.1 directory without the .gradle parent.
250
+ searchDir.split(/[\\/]/).includes("files-2.1")
251
+ ) {
239
252
  searchDir = join(searchDir, "..");
240
253
  }
241
254
  const pomSearch = getAllFiles(searchDir, "**/*.pom");
@@ -262,7 +275,9 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
262
275
  }
263
276
  } else {
264
277
  const coordinates = inferMavenCoordinatesFromPath(jf);
265
- if (coordinates?.extension === "jar") {
278
+ // An aar gets the same type=jar purl the Gradle dependency tree gives
279
+ // it, so the two entries merge into one component.
280
+ if (coordinates && ["jar", "aar"].includes(coordinates.extension)) {
266
281
  purl = jarPurl(
267
282
  coordinates.group,
268
283
  coordinates.name,
@@ -274,8 +289,9 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
274
289
  // descriptor, so its metadata and manifest licence are read here,
275
290
  // before any remote lookup. Only a jar whose path names its
276
291
  // coordinates is read: they choose its descriptor, and an entry
277
- // without a purl never becomes a component.
278
- if (purl) {
292
+ // without a purl never becomes a component. An aar keeps no
293
+ // descriptor of its own; its POM sits beside it in the cache.
294
+ if (purl && !isAar) {
279
295
  const embedded = await readEmbeddedJarMetadata(jf, coordinates);
280
296
  if (embedded?.pomXml) {
281
297
  try {
@@ -300,7 +316,9 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
300
316
  console.log(`Parsing ${jf}`);
301
317
  }
302
318
  const [nsList, hashValues] = await Promise.all([
303
- getJarClasses(jf),
319
+ // An aar is hashed as published, over the aar file itself; its
320
+ // classes come from the jars it holds.
321
+ isAar ? getAarClasses(jf) : getJarClasses(jf),
304
322
  multiChecksumFile(["md5", "sha1", "sha256", "sha512"], jf).catch(
305
323
  () => undefined,
306
324
  ),
@@ -336,7 +354,7 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
336
354
  }
337
355
  } else {
338
356
  console.log(
339
- `${jarPath} did not contain any jars. Try building the project to improve the BOM precision.`,
357
+ `${jarPath} did not contain any jars or aar files. Try building the project to improve the BOM precision.`,
340
358
  );
341
359
  }
342
360
  return jarNSMapping;
@@ -1136,6 +1154,81 @@ export async function getJarClasses(jarFile) {
1136
1154
  return retList;
1137
1155
  }
1138
1156
 
1157
+ /**
1158
+ * Method to get the classes of an Android library. An `.aar` keeps its code
1159
+ * in `classes.jar` at its root and any number of further jars under `libs/`,
1160
+ * each of which is read in turn.
1161
+ *
1162
+ * Each inner jar is unpacked to a temporary file, so in dry run mode, which
1163
+ * blocks those writes, an aar yields no classes. An inner jar that declares
1164
+ * more than `maxInnerJarBytes` uncompressed is skipped without being
1165
+ * decompressed, because the whole entry is held in memory while it is copied.
1166
+ *
1167
+ * @param {string} aarFile Aar file to read
1168
+ * @param {Object} [opts] Options
1169
+ * @param {number} [opts.maxInnerJarBytes] Uncompressed size bound for each
1170
+ * inner jar. Defaults to 256 MiB.
1171
+ *
1172
+ * @returns {Promise<string[]>} Sorted class names of every jar inside
1173
+ */
1174
+ export async function getAarClasses(
1175
+ aarFile,
1176
+ { maxInnerJarBytes = MAX_AAR_INNER_JAR_BYTES } = {},
1177
+ ) {
1178
+ const classes = new Set();
1179
+ let zip;
1180
+ let tempDir;
1181
+ try {
1182
+ zip = new StreamZip.async({ file: aarFile });
1183
+ const innerJars = Object.values(await zip.entries())
1184
+ .filter(
1185
+ (entry) =>
1186
+ !entry.isDirectory &&
1187
+ (entry.name === "classes.jar" ||
1188
+ (entry.name.startsWith("libs/") && entry.name.endsWith(".jar"))),
1189
+ )
1190
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
1191
+ for (const entry of innerJars) {
1192
+ if (entry.size > maxInnerJarBytes) {
1193
+ if (DEBUG_MODE) {
1194
+ console.log(
1195
+ `Skipping ${entry.name} in ${aarFile}: ${entry.size} bytes exceeds the ${maxInnerJarBytes} byte limit.`,
1196
+ );
1197
+ }
1198
+ continue;
1199
+ }
1200
+ tempDir ||= safeMkdtempSync(join(getTmpDir(), "aar-classes-"));
1201
+ // Entry names use forward slashes on every platform. node-stream-zip
1202
+ // rejects names with "..", a backslash or an absolute prefix, so the
1203
+ // file stays inside tempDir.
1204
+ const innerJar = join(tempDir, ...entry.name.split("/"));
1205
+ safeMkdirSync(dirname(innerJar), { recursive: true });
1206
+ safeWriteSync(innerJar, await zip.entryData(entry.name));
1207
+ // The write is skipped in dry run mode or without write permission.
1208
+ if (!safeExistsSync(innerJar)) {
1209
+ continue;
1210
+ }
1211
+ for (const cls of await getJarClasses(innerJar)) {
1212
+ classes.add(cls);
1213
+ }
1214
+ }
1215
+ } catch (err) {
1216
+ if (DEBUG_MODE) {
1217
+ console.log(`Unable to read the jars inside ${aarFile}: ${err.message}`);
1218
+ }
1219
+ } finally {
1220
+ if (tempDir) {
1221
+ safeRmSync(tempDir, { recursive: true, force: true });
1222
+ }
1223
+ try {
1224
+ await zip?.close();
1225
+ } catch (_err) {
1226
+ // An archive that failed to open reports its close as well.
1227
+ }
1228
+ }
1229
+ return [...classes].sort();
1230
+ }
1231
+
1139
1232
  /**
1140
1233
  * Recursively flatten a Python package's transitive dependencies into the
1141
1234
  * dependencies map and flat package list.
@@ -2,12 +2,21 @@ import { Buffer } from "node:buffer";
2
2
  import {
3
3
  closeSync,
4
4
  openSync,
5
+ readdirSync,
5
6
  readFileSync,
6
7
  readSync,
7
8
  realpathSync,
8
9
  statSync,
9
10
  } from "node:fs";
10
- import { basename, delimiter, isAbsolute, join, resolve } from "node:path";
11
+ import {
12
+ basename,
13
+ delimiter,
14
+ extname,
15
+ isAbsolute,
16
+ join,
17
+ relative,
18
+ resolve,
19
+ } from "node:path";
11
20
  import process from "node:process";
12
21
 
13
22
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
@@ -24,12 +33,22 @@ import {
24
33
  } from "../core/fs.js";
25
34
  import { formatDuration, reapProcessTree } from "../core/processTree.js";
26
35
  import { encodeUriReference } from "../core/propertySanitizer.js";
36
+ import {
37
+ normalizeHuggingFaceReference,
38
+ toHuggingFaceSupplier,
39
+ } from "../parsers/huggingfaceUtils.js";
27
40
  import {
28
41
  indexJsonObject,
29
42
  MAX_JSON_TEXT_BYTES,
30
43
  readJsonMember,
31
44
  readJsonRange,
32
45
  } from "../parsers/largeJson.js";
46
+ import {
47
+ modelCreatorEntity,
48
+ modelCreatorFromOrganization,
49
+ resolveModelCreator,
50
+ } from "./aiProviders.js";
51
+ import { isPathExcludedByGlobPatterns } from "./atomUtils.js";
33
52
  import {
34
53
  addDosaiSetValue,
35
54
  buildDosaiPurlAliasMap,
@@ -712,6 +731,198 @@ export function dosaiSectionsCdxgenReads(omittedSections) {
712
731
  );
713
732
  }
714
733
 
734
+ // The files dosai reads from a source tree whose changes make an existing
735
+ // methods slice stale: the C#, VB, and F# sources it analyses, the project
736
+ // and MSBuild files that shape the analysis, and the Razor and WebForms
737
+ // templates whose call sites it maps from build output. packages.config is
738
+ // read by cdxgen's own dotnet analysis and holds the same package list a
739
+ // csproj does, so a change to it can change the slice too. Solution files are
740
+ // left out: dosai opens them only to budget its restore runs, and a changed
741
+ // solution does not change the occurrences a slice holds.
742
+ const DOSAI_SLICE_SOURCE_EXTENSIONS = new Set([
743
+ ".cs",
744
+ ".cshtml",
745
+ ".csproj",
746
+ ".fs",
747
+ ".fsi",
748
+ ".fsproj",
749
+ ".fsx",
750
+ ".props",
751
+ ".razor",
752
+ ".targets",
753
+ ".vb",
754
+ ".vbproj",
755
+ ]);
756
+ const DOSAI_SLICE_SOURCE_FILES = new Set(["packages.config"]);
757
+
758
+ // Directories that hold nothing dosai analyses. Its own tree walk skips the
759
+ // build-output directories (obj for every file, bin for sources) and the
760
+ // package and version-control directories, so a build cannot make a slice
761
+ // stale and a dependency tree cannot slow the check down.
762
+ const DOSAI_SLICE_SOURCE_SKIP_DIRS = new Set([
763
+ ".git",
764
+ "bin",
765
+ "node_modules",
766
+ "obj",
767
+ ]);
768
+
769
+ // The tree walk is bounded so a pathological tree (a symlink farm, or entries
770
+ // that appear faster than the walk reads them) cannot keep a scan from
771
+ // finishing. A walk that reaches a bound leaves freshness unknown, and the
772
+ // callers regenerate the slice, which is the run a fresh scan pays anyway.
773
+ const DOSAI_FRESHNESS_MAX_DIRS = 50_000;
774
+ const DOSAI_FRESHNESS_MAX_FILES = 200_000;
775
+
776
+ /**
777
+ * The newest mtime among the source files dosai analyses under a directory.
778
+ *
779
+ * The walk is deterministic and bounded: directory entries are read and then
780
+ * visited depth-first in sorted name order, symlinked directories are not
781
+ * followed, and counts of visited directories and analysed files cap the
782
+ * work. Excluded paths are pruned rather than filtered, so nothing beneath an
783
+ * excluded directory is read.
784
+ *
785
+ * @param {string} projectDir Absolute directory dosai analyses
786
+ * @param {string[]} excludePatterns Exclude globs relative to projectDir
787
+ * @returns {{mtimeMs: number|undefined, complete: boolean}} The newest mtime seen (undefined when no analysed file was found), and whether the walk finished, which is false for an unreadable directory or file, or a tree past the bounds
788
+ */
789
+ function newestDosaiSourceMtimeSync(projectDir, excludePatterns) {
790
+ const newest = { mtimeMs: undefined, complete: true };
791
+ const stack = [[projectDir, ""]];
792
+ let dirsVisited = 0;
793
+ let filesVisited = 0;
794
+ while (newest.complete && stack.length) {
795
+ if (dirsVisited++ >= DOSAI_FRESHNESS_MAX_DIRS) {
796
+ newest.complete = false;
797
+ break;
798
+ }
799
+ const [dir, relDir] = stack.pop();
800
+ let entries;
801
+ try {
802
+ entries = readdirSync(dir, { withFileTypes: true });
803
+ } catch (_err) {
804
+ newest.complete = false;
805
+ break;
806
+ }
807
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
808
+ for (const entry of entries) {
809
+ const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
810
+ if (entry.isDirectory()) {
811
+ if (
812
+ !DOSAI_SLICE_SOURCE_SKIP_DIRS.has(entry.name.toLowerCase()) &&
813
+ !(
814
+ excludePatterns.length &&
815
+ isPathExcludedByGlobPatterns(relPath, excludePatterns)
816
+ )
817
+ ) {
818
+ stack.push([join(dir, entry.name), relPath]);
819
+ }
820
+ continue;
821
+ }
822
+ if (
823
+ !DOSAI_SLICE_SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase()) &&
824
+ !DOSAI_SLICE_SOURCE_FILES.has(entry.name.toLowerCase())
825
+ ) {
826
+ continue;
827
+ }
828
+ if (filesVisited++ >= DOSAI_FRESHNESS_MAX_FILES) {
829
+ newest.complete = false;
830
+ break;
831
+ }
832
+ try {
833
+ // statSync follows a symlink to the source it points at, which is
834
+ // the file dosai reads.
835
+ const mtimeMs = statSync(join(dir, entry.name)).mtimeMs;
836
+ if (newest.mtimeMs === undefined || mtimeMs > newest.mtimeMs) {
837
+ newest.mtimeMs = mtimeMs;
838
+ }
839
+ } catch (_err) {
840
+ newest.complete = false;
841
+ break;
842
+ }
843
+ }
844
+ }
845
+ return newest;
846
+ }
847
+
848
+ /**
849
+ * The cdxgen exclude patterns that apply under a directory, as globs relative
850
+ * to it.
851
+ *
852
+ * The patterns are matched the way the atom evidence filter matches
853
+ * `options.exclude`: a relative pattern matches at any depth, and an absolute
854
+ * pattern is anchored to the scanned directory, which for the walk means the
855
+ * patterns that sit beneath it are rebased to relative ones. Patterns that
856
+ * fall outside the directory are dropped.
857
+ *
858
+ * @param {string[]} excludes cdxgen exclude globs
859
+ * @param {string} projectDir Absolute directory dosai analyses
860
+ * @returns {string[]} Exclude globs relative to projectDir
861
+ */
862
+ function dosaiSourceExcludePatterns(excludes, projectDir) {
863
+ const rebased = [];
864
+ if (!Array.isArray(excludes)) {
865
+ return rebased;
866
+ }
867
+ for (const pattern of excludes) {
868
+ if (typeof pattern !== "string" || !pattern.trim()) {
869
+ continue;
870
+ }
871
+ // resolve() leaves a relative pattern where it was and turns an absolute
872
+ // one into a path the relative() call drops the scanned directory from.
873
+ const underRoot = resolve(projectDir, pattern);
874
+ const rel = relative(projectDir, underRoot);
875
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel)) {
876
+ rebased.push(rel.replaceAll("\\", "/"));
877
+ }
878
+ }
879
+ return rebased;
880
+ }
881
+
882
+ /**
883
+ * Whether an existing dosai methods slice is newer than the sources it
884
+ * describes.
885
+ *
886
+ * A slice written by an earlier scan holds the occurrences of the sources as
887
+ * they were then, so a slice that predates an edit must be regenerated rather
888
+ * than reused: reusing one keeps occurrences of call sites that no longer
889
+ * exist (issue 4675). The slice is fresh only when its mtime is not older
890
+ * than the newest mtime among the files dosai analyses under the directory.
891
+ * A missing slice, a slice whose mtime cannot be read, a source that cannot
892
+ * be stat-ed, and a tree that exceeds the walk bounds all count as stale: the
893
+ * callers regenerate the slice, which is the run a fresh scan pays anyway.
894
+ *
895
+ * @param {string} slicesFile Candidate dosai methods slice
896
+ * @param {string} projectDir Directory dosai analyses
897
+ * @param {Object} [options] Scan options; options.exclude prunes the walk
898
+ * @returns {boolean} True when the slice is newer than every source it describes
899
+ */
900
+ export function dosaiSlicesFileIsFresh(slicesFile, projectDir, options = {}) {
901
+ if (!slicesFile || !projectDir || !safeExistsSync(slicesFile)) {
902
+ return false;
903
+ }
904
+ let sliceMtimeMs;
905
+ try {
906
+ sliceMtimeMs = statSync(slicesFile).mtimeMs;
907
+ } catch (_err) {
908
+ return false;
909
+ }
910
+ const excludePatterns = dosaiSourceExcludePatterns(
911
+ options.exclude,
912
+ resolve(projectDir),
913
+ );
914
+ const newest = newestDosaiSourceMtimeSync(
915
+ resolve(projectDir),
916
+ excludePatterns,
917
+ );
918
+ if (!newest.complete) {
919
+ return false;
920
+ }
921
+ // A directory with no file dosai analyses holds nothing that can go stale,
922
+ // so the slice counts as fresh.
923
+ return newest.mtimeMs === undefined || sliceMtimeMs >= newest.mtimeMs;
924
+ }
925
+
715
926
  // The file each report returned by readDosaiReport came from, and whether it
716
927
  // was trimmed to what cdxgen reads. persistDosaiSemanticsReport copies the
717
928
  // file rather than re-serialising a trimmed or oversized report.
@@ -1978,6 +2189,23 @@ export function collectDosaiAiComponents(methodsSlice, components = []) {
1978
2189
  format,
1979
2190
  }));
1980
2191
  }
2192
+ const hubReference = String(ai.Purl || "").startsWith("pkg:huggingface/")
2193
+ ? normalizeHuggingFaceReference(ai.Purl)
2194
+ : undefined;
2195
+ const hubNamespace = hubReference?.repoId?.split("/")[0];
2196
+ // A Hub model is credited to the namespace only when it is the
2197
+ // creator's own organisation, as the source collectors do. Otherwise
2198
+ // dosai reports the provider the code calls, which names the client
2199
+ // rather than the model, so the creator is resolved from the model
2200
+ // name.
2201
+ const manufacturer = modelCreatorEntity(
2202
+ hubNamespace
2203
+ ? modelCreatorFromOrganization(hubNamespace)
2204
+ : resolveModelCreator(ai.Name, {
2205
+ fileProviders: ai.Provider ? [ai.Provider] : [],
2206
+ provider: ai.Provider,
2207
+ }),
2208
+ );
1981
2209
  const component = {
1982
2210
  "bom-ref": ai.Id,
1983
2211
  type: "machine-learning-model",
@@ -1985,6 +2213,13 @@ export function collectDosaiAiComponents(methodsSlice, components = []) {
1985
2213
  version: ai.Version,
1986
2214
  purl: ai.Purl,
1987
2215
  modelCard,
2216
+ // The creator is the manufacturer; a Hugging Face namespace that
2217
+ // published the model is the supplier. The provider stays a
2218
+ // property either way.
2219
+ manufacturer,
2220
+ supplier: hubNamespace
2221
+ ? toHuggingFaceSupplier(hubNamespace)
2222
+ : undefined,
1988
2223
  properties: [
1989
2224
  { name: "cdx:ai:provider", value: ai.Provider || "unknown" },
1990
2225
  { name: "cdx:ai:deployment", value: ai.Deployment || "unknown" },
@@ -12,6 +12,7 @@ import {
12
12
  import { Purl } from "@cdxgen/cdx-purl";
13
13
 
14
14
  import { safeExistsSync } from "../core/fs.js";
15
+ import { occurrenceLocationKey } from "./occurrenceKey.js";
15
16
 
16
17
  /**
17
18
  * Build a lowercase type/namespace/name lookup key for a purl.
@@ -68,10 +69,7 @@ export function dosaiLocation(item) {
68
69
  return undefined;
69
70
  }
70
71
  const lineNumber = location?.LineNumber || item?.LineNumber;
71
- if (lineNumber && lineNumber > 0) {
72
- return `${fileName}#${lineNumber}`;
73
- }
74
- return fileName;
72
+ return occurrenceLocationKey(fileName, lineNumber);
75
73
  }
76
74
 
77
75
  function dosaiSourceFileName(item) {
@@ -110,8 +108,7 @@ export function dosaiCallSiteLocation(methodCall, srcPath) {
110
108
  if (!file || lowerFile.endsWith(".dll") || lowerFile.endsWith(".exe")) {
111
109
  return undefined;
112
110
  }
113
- const lineNumber = methodCall?.LineNumber;
114
- return lineNumber > 0 ? `${file}#${lineNumber}` : file;
111
+ return occurrenceLocationKey(file, methodCall?.LineNumber);
115
112
  }
116
113
 
117
114
  function dosaiCallSiteFile(methodCall, srcPath) {