@cdxgen/cdxgen 13.4.0 → 13.6.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -1,10 +1,13 @@
1
1
  import { platform } from "node:os";
2
- import { extname } from "node:path";
2
+ import { basename, dirname, extname } from "node:path";
3
3
 
4
4
  import { build } from "@cdxgen/cdx-purl";
5
5
 
6
6
  import { safeExistsSync } from "../core/fs.js";
7
7
  import { thoughtLog } from "../core/logger.js";
8
+ import { sanitizeBomUrl } from "../core/propertySanitizer.js";
9
+ import { vscodeExtensionPlatformFromPath } from "../parsers/vscodeExtensionId.js";
10
+ import { isAgentSkillSpecValid } from "./agentSkillSpec.js";
8
11
  import {
9
12
  addDosaiReachabilityProperties,
10
13
  readDosaiMethodsReport,
@@ -20,16 +23,26 @@ import {
20
23
  } from "./dosaiParsers.js";
21
24
  import { createGtfoBinsPropertiesFromRow } from "./gtfobins.js";
22
25
  import { createLolbasProperties } from "./lolbas.js";
26
+ import { isOsPackagePurlType } from "./osPackagePurl.js";
23
27
  import {
24
28
  createOsQueryFallbackBomRef,
29
+ createOsQueryPackageIdentity,
25
30
  createOsQueryPurl,
26
31
  deriveOsQueryDescription,
27
32
  deriveOsQueryName,
28
33
  deriveOsQueryPublisher,
29
34
  deriveOsQueryVersion,
35
+ OS_PACKAGE_LICENSE_COLUMNS,
36
+ osPackagePropertyName,
30
37
  sanitizeOsQueryIdentity,
31
38
  shouldCreateOsQueryPurl,
32
39
  } from "./osqueryTransform.js";
40
+ import {
41
+ adjustLicenseInformation,
42
+ findLicenseId,
43
+ isSpdxLicenseExpression,
44
+ spdxLicenses,
45
+ } from "./spdx.js";
33
46
 
34
47
  /**
35
48
  * Build a CycloneDX occurrence evidence object from a location string.
@@ -540,20 +553,55 @@ export function convertOSQueryResults(
540
553
  enhance = false,
541
554
  osPackageListers = undefined,
542
555
  ) {
556
+ if (queryCategory === "ai_agent_skills") {
557
+ return convertAgentSkillRows(results);
558
+ }
543
559
  const pkgList = [];
560
+ // deb, rpm, apk, pacman, Homebrew, MacPorts, snap and flatpak rows describe a
561
+ // package, not a file or an app: the package manager names them, and nothing
562
+ // in such a row is a path inside the package.
563
+ const isOsPackage = isOsPackagePurlType(queryObj.purlType);
564
+ const isLocatedPackage =
565
+ LOCATED_PACKAGE_PURL_TYPES.has(queryObj.purlType) ||
566
+ LOCATED_PACKAGE_CATEGORIES.has(queryCategory);
544
567
  if (results?.length) {
545
568
  for (const res of results) {
546
- const version = deriveOsQueryVersion(res);
569
+ let version = deriveOsQueryVersion(res);
547
570
  let name = deriveOsQueryName(res, results.length === 1, queryObj.name);
548
571
  if (queryObj.purlType === "chrome-extension") {
549
- name = (res.identifier || res.extension_id || name || "").toLowerCase();
572
+ // osquery computes identifier from the manifest key, which an
573
+ // unpacked extension may lack; the profile still knows its ID.
574
+ name = (
575
+ res.identifier ||
576
+ res.referenced_identifier ||
577
+ res.extension_id ||
578
+ name ||
579
+ ""
580
+ ).toLowerCase();
581
+ }
582
+ let npmScope;
583
+ if (queryObj.purlType === "npm") {
584
+ const scoped = /^@([^/]+)\/(.+)$/.exec(`${name || ""}`);
585
+ if (scoped) {
586
+ [, npmScope, name] = scoped;
587
+ }
550
588
  }
551
589
  let group = "";
552
- const subpath = res.path || res.admindir || res.source;
590
+ const subpath =
591
+ isOsPackage || isLocatedPackage
592
+ ? undefined
593
+ : res.path || res.admindir || res.source;
553
594
  const publisher = deriveOsQueryPublisher(res);
554
- // For vscode-extension purl type, the publisher is used as the namespace
555
- if (queryObj.purlType === "vscode-extension" && publisher) {
556
- group = publisher.toLowerCase();
595
+ if (queryObj.purlType === "vscode-extension") {
596
+ // osquery names a VS Code extension by its ID, <publisher>.<name>,
597
+ // which the vscode-extension type splits into namespace and name. Its
598
+ // publisher column is the publisher's display name.
599
+ const extensionId = /^([^.]+)\.(.+)$/u.exec(`${name || ""}`);
600
+ if (extensionId) {
601
+ [, group, name] = extensionId;
602
+ } else if (publisher) {
603
+ group = publisher.toLowerCase();
604
+ }
557
605
  }
558
606
  let scope;
559
607
  const compScope = res.priority;
@@ -568,25 +616,60 @@ export function convertOSQueryResults(
568
616
  qualifiers = {
569
617
  tag_id: res.identifying_number.replace("{", "").replace("}", ""),
570
618
  };
619
+ } else if (queryCategory === "firefox_addons" && res.identifier) {
620
+ // The profile an add-on was found in is not part of its purl, so the
621
+ // add-on ID is what tells apart two add-ons that share a name.
622
+ qualifiers = { tag_id: firefoxAddonTagId(res.identifier) };
623
+ } else if (queryObj.purlType === "vscode-extension") {
624
+ // A platform-specific build is a different artefact from the one for
625
+ // another platform, and the extension scan names it the same way.
626
+ const targetPlatform = vscodeExtensionPlatformFromPath(res.path);
627
+ if (targetPlatform) {
628
+ qualifiers = { platform: targetPlatform };
629
+ }
571
630
  }
572
631
  if (name) {
573
632
  name = sanitizeOsQueryIdentity(name);
574
- group = sanitizeOsQueryIdentity(group);
633
+ // sanitizeOsQueryIdentity drops a leading @, which an npm scope keeps.
634
+ group = npmScope
635
+ ? `@${sanitizeOsQueryIdentity(npmScope)}`
636
+ : sanitizeOsQueryIdentity(group);
575
637
  const isCryptoAsset = queryObj.componentType === "cryptographic-asset";
576
- const purl = shouldCreateOsQueryPurl(queryObj.componentType)
577
- ? createOsQueryPurl(
578
- queryObj.purlType,
579
- group,
580
- name,
581
- version,
582
- qualifiers,
583
- subpath,
584
- )
585
- : undefined;
638
+ let purl;
639
+ if (isOsPackage) {
640
+ // The version of such a row is part of its identity: an rpm's is its
641
+ // version and release, behind its epoch.
642
+ const identity = createOsQueryPackageIdentity(
643
+ queryObj.purlType,
644
+ res,
645
+ name,
646
+ );
647
+ version = identity.version;
648
+ purl = shouldCreateOsQueryPurl(queryObj.componentType)
649
+ ? identity.purl
650
+ : undefined;
651
+ } else if (shouldCreateOsQueryPurl(queryObj.componentType)) {
652
+ purl = createOsQueryPurl(
653
+ queryObj.purlType,
654
+ group,
655
+ name,
656
+ version,
657
+ qualifiers,
658
+ subpath,
659
+ );
660
+ }
586
661
  const props = [{ name: "cdx:osquery:category", value: queryCategory }];
587
662
  props.push(...createLolbasProperties(queryCategory, res));
588
663
  if (platform() === "linux") {
589
- props.push(...createGtfoBinsPropertiesFromRow(queryCategory, res));
664
+ // The GTFOBins matcher reads a description for command names, and
665
+ // the free text of a package's description matches by chance: zlib's
666
+ // "the deflate compression method found in gzip" is no use of gzip.
667
+ props.push(
668
+ ...createGtfoBinsPropertiesFromRow(
669
+ queryCategory,
670
+ isOsPackage ? { ...res, description: undefined } : res,
671
+ ),
672
+ );
590
673
  }
591
674
  let providesList;
592
675
  if (enhance) {
@@ -620,10 +703,12 @@ export function convertOSQueryResults(
620
703
  version: version || "",
621
704
  description,
622
705
  publisher,
706
+ // Without a purl, the bom-ref is built from the row, and the path of
707
+ // a located package is not part of what it is.
623
708
  "bom-ref": createOsQueryBomRef(
624
709
  queryCategory,
625
710
  queryObj.componentType,
626
- res,
711
+ isLocatedPackage ? { ...res, path: undefined } : res,
627
712
  name,
628
713
  version,
629
714
  purl,
@@ -644,25 +729,364 @@ export function convertOSQueryResults(
644
729
  if (cryptoProperties) {
645
730
  apkg.cryptoProperties = cryptoProperties;
646
731
  }
732
+ const licenseColumns = isOsPackage
733
+ ? OS_PACKAGE_LICENSE_COLUMNS[queryObj.purlType]
734
+ : undefined;
735
+ if (licenseColumns) {
736
+ const licenses = osPackageLicenses(
737
+ res[licenseColumns.license],
738
+ licenseColumns.separator,
739
+ );
740
+ if (licenses.length) {
741
+ apkg.licenses = licenses;
742
+ }
743
+ const website = licenseColumns.website
744
+ ? osPackageWebsite(res[licenseColumns.website])
745
+ : undefined;
746
+ if (website) {
747
+ apkg.externalReferences = [{ type: "website", url: website }];
748
+ }
749
+ }
647
750
  for (const k of Object.keys(res).filter((p) => {
648
751
  if (["version", "description", "publisher"].includes(p)) {
649
752
  return false;
650
753
  }
651
754
  return !(queryObj.purlType !== "chrome-extension" && p === "name");
652
755
  })) {
653
- if (res[k] && res[k] !== "null") {
756
+ const propertyName = isOsPackage
757
+ ? osPackagePropertyName(queryObj.purlType, k)
758
+ : k;
759
+ // (none) is what rpm's database says for a tag a record lacks, such
760
+ // as the source rpm of a key record.
761
+ if (
762
+ propertyName &&
763
+ res[k] &&
764
+ res[k] !== "null" &&
765
+ !(isOsPackage && res[k] === "(none)")
766
+ ) {
654
767
  props.push({
655
- name: k,
768
+ name: propertyName,
656
769
  value: res[k],
657
770
  });
658
771
  }
659
772
  }
773
+ if (isLocatedPackage) {
774
+ // osquery writes a JSON null as "null", which is the path Firefox
775
+ // gives its default theme.
776
+ const occurrence = createOccurrenceEvidence(
777
+ [res.path, res.directory].find(
778
+ (value) => value && value !== "null",
779
+ ),
780
+ );
781
+ if (occurrence) {
782
+ apkg.evidence = { occurrences: [occurrence] };
783
+ }
784
+ }
660
785
  apkg.properties = props;
661
786
  pkgList.push(apkg);
662
787
  }
663
788
  }
664
789
  }
665
- return pkgList;
790
+ return isLocatedPackage ? mergeLocatedPackages(pkgList) : pkgList;
791
+ }
792
+
793
+ // npm and pypi rows name a package found at a path on the host, Chromium and
794
+ // VS Code extension rows an extension found in a browser profile or an editor's
795
+ // extensions folder, and a firefox_addons row an add-on found in a Firefox
796
+ // profile. Where it was found is not part of what it is, so the path is an
797
+ // occurrence rather than a subpath: a home directory stays out of the purl, and
798
+ // one package installed in two places is one component with two occurrences.
799
+ // Chocolatey rows carry the absolute path of the package's .nuspec, which a
800
+ // purl subpath cannot hold, so it is kept as an occurrence like these.
801
+ const LOCATED_PACKAGE_PURL_TYPES = new Set([
802
+ "npm",
803
+ "pypi",
804
+ "chrome-extension",
805
+ "vscode-extension",
806
+ "chocolatey",
807
+ ]);
808
+ const LOCATED_PACKAGE_CATEGORIES = new Set(["firefox_addons"]);
809
+
810
+ const FIREFOX_ADDON_GUID =
811
+ /^\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}$/iu;
812
+
813
+ // A Firefox add-on ID is an email-like string or a GUID in braces. A swid
814
+ // tag_id that is a GUID must be in lower case, and is written without braces.
815
+ function firefoxAddonTagId(identifier) {
816
+ const id = `${identifier}`.trim();
817
+ const guid = FIREFOX_ADDON_GUID.exec(id);
818
+ return guid ? guid[1].toLowerCase() : id;
819
+ }
820
+
821
+ function mergeLocatedPackages(components) {
822
+ const byRef = new Map();
823
+ for (const component of components) {
824
+ const existing = byRef.get(component["bom-ref"]);
825
+ if (!existing) {
826
+ byRef.set(component["bom-ref"], component);
827
+ continue;
828
+ }
829
+ const occurrences = existing.evidence?.occurrences || [];
830
+ for (const occurrence of component.evidence?.occurrences || []) {
831
+ if (
832
+ !occurrences.some((known) => known.location === occurrence.location)
833
+ ) {
834
+ occurrences.push(occurrence);
835
+ }
836
+ }
837
+ if (occurrences.length) {
838
+ existing.evidence = { ...existing.evidence, occurrences };
839
+ }
840
+ for (const property of component.properties) {
841
+ if (
842
+ !existing.properties.some(
843
+ (known) =>
844
+ known.name === property.name && known.value === property.value,
845
+ )
846
+ ) {
847
+ existing.properties.push(property);
848
+ }
849
+ }
850
+ }
851
+ for (const component of byRef.values()) {
852
+ component.evidence?.occurrences?.sort((a, b) =>
853
+ a.location.localeCompare(b.location),
854
+ );
855
+ }
856
+ return [...byRef.values()];
857
+ }
858
+
859
+ // The licence an OS package declares. An apk `L:` field is a list of SPDX ids
860
+ // separated by spaces on older packages and an SPDX expression on newer ones;
861
+ // pacman keeps one licence per entry, each an SPDX id or expression, which
862
+ // osquery joins with commas; a snap or a flatpak declares one expression. Only
863
+ // an explicit operator or a parenthesis makes a text an expression: the SPDX
864
+ // parser's own check takes the `or` inside `LGPL-2.0-or-later` for an operator,
865
+ // and would turn a plain list into one expression. A token is an id only when
866
+ // the SPDX list has what it maps to; any other is kept by name rather than
867
+ // dropped.
868
+ const MAX_OS_PACKAGE_LICENSES = 64;
869
+ const SPDX_OPERATORS = new Set(["AND", "OR", "WITH"]);
870
+
871
+ function isExplicitSpdxExpression(text) {
872
+ // No SPDX expression holds a comma, but the expression parser lets one
873
+ // through, which would take a whole pacman licence list for one expression.
874
+ return (
875
+ !text.includes(",") &&
876
+ (text.split(/\s+/u).some((token) => SPDX_OPERATORS.has(token)) ||
877
+ text.includes("(")) &&
878
+ isSpdxLicenseExpression(text)
879
+ );
880
+ }
881
+
882
+ function osPackageLicenses(text, separator) {
883
+ const license = `${text ?? ""}`.split(/\s+/u).filter(Boolean).join(" ");
884
+ if (!license) {
885
+ return [];
886
+ }
887
+ if (isExplicitSpdxExpression(license)) {
888
+ return [{ expression: license }];
889
+ }
890
+ const entries = [];
891
+ const items = separator ? license.split(separator) : [license];
892
+ for (const item of items.slice(0, MAX_OS_PACKAGE_LICENSES)) {
893
+ const entry = item.trim();
894
+ if (!entry || SPDX_OPERATORS.has(entry)) {
895
+ continue;
896
+ }
897
+ if (isExplicitSpdxExpression(entry)) {
898
+ entries.push({ expression: entry });
899
+ continue;
900
+ }
901
+ const id = findLicenseId(entry);
902
+ entries.push(
903
+ spdxLicenses.includes(id)
904
+ ? { license: { id } }
905
+ : { license: { name: entry } },
906
+ );
907
+ }
908
+ // A list that holds an expression must become one expression, since a
909
+ // component's licenses are either licences or a single expression. Every
910
+ // entry applies, so they are joined with AND.
911
+ if (entries.length > 1 && entries.some((entry) => entry.expression)) {
912
+ const parts = entries.map((entry) =>
913
+ entry.expression ? `(${entry.expression})` : entry.license.id,
914
+ );
915
+ const expression = parts.join(" AND ");
916
+ if (parts.every(Boolean) && isSpdxLicenseExpression(expression)) {
917
+ return [{ expression }];
918
+ }
919
+ }
920
+ return adjustLicenseInformation(entries);
921
+ }
922
+
923
+ // The home page of a package. A URL a package carries is untrusted text: only
924
+ // an http(s) URL is kept, without credentials, query or fragment.
925
+ function osPackageWebsite(value) {
926
+ const cleaned = sanitizeBomUrl(`${value ?? ""}`.trim());
927
+ try {
928
+ const { protocol } = new URL(cleaned);
929
+ return protocol === "http:" || protocol === "https:" ? cleaned : undefined;
930
+ } catch {
931
+ return undefined;
932
+ }
933
+ }
934
+
935
+ /**
936
+ * Convert the packages of an apk installed database into components.
937
+ *
938
+ * The packages are named as the container scan names an Alpine, Wolfi or
939
+ * Chainguard package, and grouped with the other OS package categories under
940
+ * `cdx:osquery:category` = `apk_packages`. Beyond what an osquery package row
941
+ * gives, an apk record knows its licence and home page, which become
942
+ * `licenses` and a `website` external reference.
943
+ *
944
+ * @param {Object[]} packages Packages from readApkInstalledDb()
945
+ * @returns {Object[]} Components
946
+ */
947
+ export function convertApkPackages(packages) {
948
+ const components = [];
949
+ for (const pkg of packages || []) {
950
+ const row = { name: pkg.name, version: pkg.version };
951
+ for (const column of ["arch", "origin", "maintainer"]) {
952
+ if (pkg[column]) {
953
+ row[column] = pkg[column];
954
+ }
955
+ }
956
+ const [component] = convertOSQueryResults(
957
+ "apk_packages",
958
+ { purlType: "apk" },
959
+ [row],
960
+ );
961
+ if (!component) {
962
+ continue;
963
+ }
964
+ // The description is set here and not in the row. The GTFOBins matcher reads
965
+ // a row's description for command names, which a deb or rpm row has no
966
+ // column for, and the free text of an apk description matches by chance:
967
+ // "Alpine base dir structure" is no use of the alpine mail client.
968
+ if (pkg.description) {
969
+ component.description = pkg.description;
970
+ }
971
+ const licenses = osPackageLicenses(pkg.license, /\s+/u);
972
+ if (licenses.length) {
973
+ component.licenses = licenses;
974
+ }
975
+ const website = osPackageWebsite(pkg.url);
976
+ if (website) {
977
+ component.externalReferences = [{ type: "website", url: website }];
978
+ }
979
+ components.push(component);
980
+ }
981
+ return components;
982
+ }
983
+
984
+ // The frontmatter and osquery columns of a skill row that become properties,
985
+ // named as the ai-skill project scan names them where it has a name.
986
+ const AGENT_SKILL_PROPERTY_NAMES = [
987
+ ["name", "cdx:skill:name"],
988
+ ["description", "cdx:skill:description"],
989
+ ["license", "cdx:skill:license"],
990
+ ["compatibility", "cdx:skill:compatibility"],
991
+ ["agent", "cdx:skill:agent"],
992
+ ["scope", "cdx:skill:scope"],
993
+ ["allowed_tools", "cdx:skill:allowedTools"],
994
+ ["resource_count", "cdx:skill:resourceCount"],
995
+ ["script_count", "cdx:skill:scriptCount"],
996
+ ["username", "username"],
997
+ ];
998
+ const SHA256_HEX = /^[0-9a-f]{64}$/u;
999
+
1000
+ // A skill's licence is free text ("Complete terms in LICENSE.txt"), so it
1001
+ // becomes `licenses` only when it is an SPDX id or expression.
1002
+ function agentSkillLicenses(text) {
1003
+ const license = `${text ?? ""}`.trim();
1004
+ if (!license) {
1005
+ return [];
1006
+ }
1007
+ if (isExplicitSpdxExpression(license)) {
1008
+ return [{ expression: license }];
1009
+ }
1010
+ const id = findLicenseId(license);
1011
+ return spdxLicenses.includes(id) ? [{ license: { id } }] : [];
1012
+ }
1013
+
1014
+ /**
1015
+ * Convert the rows of the `ai_agent_skills` query into components.
1016
+ *
1017
+ * A skill installed on the host is what the `ai-skill` project scan finds in a
1018
+ * repository, so it is a `file` component with the same `cdx:file:kind` and
1019
+ * `cdx:skill:*` properties, and no purl: a skill has no package identity. Its
1020
+ * bom-ref is the SHA-256 of its SKILL.md rather than the path, which is under a
1021
+ * home directory, and where it was found is an occurrence. The same skill
1022
+ * found under several roots, such as a plugin marketplace checkout and the
1023
+ * plugin cache, is one component with one occurrence for each.
1024
+ *
1025
+ * @param {Object[]} results Rows of `ai_agent_skills` joined with `hash`
1026
+ * @returns {Object[]} Components
1027
+ */
1028
+ export function convertAgentSkillRows(results) {
1029
+ const components = [];
1030
+ for (const row of results || []) {
1031
+ const skillFile = `${row.path ?? ""}`;
1032
+ const directory = `${row.directory ?? ""}` || dirname(skillFile);
1033
+ // A SKILL.md whose frontmatter osquery cannot read has no name; the
1034
+ // project scan names such a skill after its directory too.
1035
+ const name = sanitizeOsQueryIdentity(row.name || basename(directory));
1036
+ if (!skillFile || !name) {
1037
+ continue;
1038
+ }
1039
+ const version = row.version || "";
1040
+ const sha256 = SHA256_HEX.test(`${row.sha256 ?? ""}`)
1041
+ ? row.sha256
1042
+ : undefined;
1043
+ const properties = [
1044
+ { name: "cdx:osquery:category", value: "ai_agent_skills" },
1045
+ { name: "cdx:file:kind", value: "skill-file" },
1046
+ { name: "cdx:agent:inventorySource", value: "osquery" },
1047
+ ];
1048
+ for (const [column, propertyName] of AGENT_SKILL_PROPERTY_NAMES) {
1049
+ const value = `${row[column] ?? ""}`.trim();
1050
+ if (value && value !== "null") {
1051
+ properties.push({ name: propertyName, value });
1052
+ }
1053
+ }
1054
+ if (Number(row.script_count) > 0) {
1055
+ properties.push({ name: "cdx:skill:hasScripts", value: "true" });
1056
+ }
1057
+ properties.push({
1058
+ name: "cdx:skill:specValid",
1059
+ value: isAgentSkillSpecValid(row) ? "true" : "false",
1060
+ });
1061
+ const component = {
1062
+ name,
1063
+ version,
1064
+ description: row.description || "",
1065
+ type: "file",
1066
+ "bom-ref": createOsQueryFallbackBomRef(
1067
+ "ai_agent_skills",
1068
+ "file",
1069
+ name,
1070
+ version,
1071
+ sha256 ? "sha256" : "scope",
1072
+ sha256 || `${row.agent || ""}/${row.scope || ""}`,
1073
+ ),
1074
+ properties,
1075
+ };
1076
+ if (sha256) {
1077
+ component.hashes = [{ alg: "SHA-256", content: sha256 }];
1078
+ }
1079
+ const licenses = agentSkillLicenses(row.license);
1080
+ if (licenses.length) {
1081
+ component.licenses = licenses;
1082
+ }
1083
+ const occurrence = createOccurrenceEvidence(skillFile);
1084
+ if (occurrence) {
1085
+ component.evidence = { occurrences: [occurrence] };
1086
+ }
1087
+ components.push(component);
1088
+ }
1089
+ return mergeLocatedPackages(components);
666
1090
  }
667
1091
  function createOsQueryCryptoProperties(queryCategory, res, version) {
668
1092
  switch (queryCategory) {
@@ -221,7 +221,7 @@ export function openHostCircuit(
221
221
  );
222
222
  } else if (breaker === "maven-central") {
223
223
  console.warn(
224
- `${host} ${what}, so cdxgen will not contact it for the next ${minutes} minute(s) and Maven metadata comes from local caches only. Set MAVEN_CENTRAL_URL to a mirror or repository manager, for example https://maven-central.storage-download.googleapis.com/maven2/, to keep license enrichment working.`,
224
+ `${host} ${what}, so cdxgen will not contact it for the next ${minutes} minute(s) and Maven metadata comes from local caches only. Set MAVEN_CENTRAL_MIRROR=true to use Google's Maven Central mirror, or name a repository manager in MAVEN_CENTRAL_URL, to keep license enrichment working.`,
225
225
  );
226
226
  } else {
227
227
  console.warn(
@@ -4,7 +4,9 @@ import { basename } from "node:path";
4
4
  import process from "node:process";
5
5
 
6
6
  import { readEnvironmentVariable } from "../core/activity.js";
7
+ import { PROJECT_TYPE_ALIASES } from "../core/env.js";
7
8
  import { getAllFiles } from "../core/fs.js";
9
+ import { thoughtLog } from "../core/logger.js";
8
10
  import { collectOSCryptoLibs } from "./cbomutils.js";
9
11
  import { azurePipelinesParser } from "./ciParsers/azurePipelines.js";
10
12
  import { circleCiParser } from "./ciParsers/circleCi.js";
@@ -187,6 +189,16 @@ const ENV_BLOCKLIST = [
187
189
  "webhook",
188
190
  ];
189
191
 
192
+ // The project types a scan asked for, as given on the command line.
193
+ function requestedProjectTypes(options) {
194
+ return [].concat(options?.projectType || []).flatMap((type) =>
195
+ `${type}`
196
+ .split(",")
197
+ .map((part) => part.trim().toLowerCase())
198
+ .filter(Boolean),
199
+ );
200
+ }
201
+
190
202
  /**
191
203
  * Build the formulation section for a CycloneDX BOM.
192
204
  *
@@ -285,14 +297,31 @@ export function addFormulationSection(filePath, options, context = {}) {
285
297
  }
286
298
  }
287
299
 
288
- // Build environment details (Java, .NET, Python, Node, GCC, Rust, Go, Ruby)
289
- const infoComponents = collectEnvInfo(projectPath);
300
+ // The toolchains and OS crypto libraries are probed on the machine cdxgen
301
+ // runs on. A container image or root filesystem was built elsewhere, so the
302
+ // scanning host's tools say nothing about it.
303
+ // Only an explicit container type counts: hasAnyProjectType treats
304
+ // `universal` as every type, and a universal source scan runs on the host.
305
+ const describesScanningHost = !requestedProjectTypes(options).some((type) =>
306
+ PROJECT_TYPE_ALIASES.oci.includes(type),
307
+ );
308
+ if (!describesScanningHost) {
309
+ thoughtLog(
310
+ "Skipping the toolchain and OS crypto library probes for formulation, since they would describe the scanning host and not the image.",
311
+ );
312
+ }
313
+
314
+ // Build environment details (Java, .NET, Python, Node, GCC, Rust, Go, Ruby,
315
+ // RubyGems, Bundler, PHP, Composer)
316
+ const infoComponents = describesScanningHost
317
+ ? collectEnvInfo(projectPath, options)
318
+ : [];
290
319
  if (infoComponents?.length) {
291
320
  components = components.concat(infoComponents);
292
321
  }
293
322
 
294
323
  // OS crypto libraries (cbom mode)
295
- if (options.includeCrypto) {
324
+ if (options.includeCrypto && describesScanningHost) {
296
325
  const cryptoLibs = collectOSCryptoLibs(options, context?.executeOsQuery);
297
326
  if (cryptoLibs?.length) {
298
327
  components = components.concat(cryptoLibs);