@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -0,0 +1,326 @@
1
+ // PyPI Trove classifier interpretation and package -> module-name resolution.
2
+ //
3
+ // PyPI responses carry two structured signals that were previously read only for
4
+ // their license lines and then discarded:
5
+ //
6
+ // * `Framework :: X` — the author's declaration that the distribution belongs to
7
+ // framework X. It is more reliable than any name-prefix heuristic: it catches
8
+ // `flask-talisman`, `dj-database-url` and `starlette-context` without anyone
9
+ // maintaining a list, and it never fires on a package that merely has a
10
+ // framework's name embedded in its own.
11
+ // * `Topic :: ...` — a curated taxonomy of what the distribution does, which maps
12
+ // cleanly onto the description-tag vocabulary in `data/component-tags.json`.
13
+ //
14
+ // Tags derived here flow into `component.tags` and, for the `framework` tag, into
15
+ // `determinePackageType()` in `lib/cli/bomAssembly.js`, which already promotes any
16
+ // package carrying a `framework` tag to `type: "framework"`.
17
+
18
+ import { PYPI_MODULE_PACKAGE_MAPPING } from "../core/state.js";
19
+
20
+ /**
21
+ * `Framework ::` values that do not mean "an application is built on this".
22
+ *
23
+ * Two groups, both of which would otherwise put a `framework` tag — and therefore
24
+ * `type: "framework"` — on a large share of an ordinary dependency tree, drowning
25
+ * the runtime frameworks the tag exists to surface:
26
+ *
27
+ * * Development, build, test and documentation tooling. A pytest plugin
28
+ * declaring `Framework :: Pytest` is making a true statement, but it belongs
29
+ * to the dev dependencies, not to what the application runs on.
30
+ * * Concurrency runtimes. `Framework :: AsyncIO` is declared by a very large
31
+ * number of libraries to advertise that they work under asyncio; observed on
32
+ * `fastapi`, and `Framework :: AnyIO` likewise on `starlette`. It describes an
33
+ * execution model, not a framework the application is written against.
34
+ *
35
+ * The declared name is still emitted as a tag in every case, and still recorded in
36
+ * `cdx:pypi:frameworks`; only the generic `framework` tag is withheld.
37
+ */
38
+ export const PYPI_NON_APPLICATION_FRAMEWORKS = new Set([
39
+ "anyio",
40
+ "asyncio",
41
+ "buildout",
42
+ "cookiecutter",
43
+ "flake8",
44
+ "hatch",
45
+ "hypothesis",
46
+ "mkdocs",
47
+ "pytest",
48
+ "robotframework",
49
+ "setuptools",
50
+ "sphinx",
51
+ "tox",
52
+ "trio",
53
+ "wheel",
54
+ ]);
55
+
56
+ /**
57
+ * `Topic ::` prefix -> tags, longest prefix wins.
58
+ *
59
+ * Every value on the right-hand side is an existing entry in
60
+ * `data/component-tags.json` so that a tag derived from a classifier and the same
61
+ * tag derived from a description are indistinguishable to consumers.
62
+ */
63
+ const TOPIC_TAGS = [
64
+ [
65
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
66
+ ["framework"],
67
+ ],
68
+ ["Topic :: Software Development :: Object Brokering", ["rpc"]],
69
+ ["Topic :: Software Development :: Testing", ["test"]],
70
+ ["Topic :: Internet :: WWW/HTTP :: WSGI", ["web", "http", "wsgi"]],
71
+ ["Topic :: Internet :: WWW/HTTP :: Session", ["web", "http", "auth"]],
72
+ ["Topic :: Internet :: WWW/HTTP :: Dynamic Content", ["web", "http"]],
73
+ ["Topic :: Internet :: WWW/HTTP", ["web", "http"]],
74
+ ["Topic :: Internet :: XML-RPC", ["rpc", "xml"]],
75
+ ["Topic :: Internet :: Proxy Servers", ["web", "traffic"]],
76
+ ["Topic :: Database :: Front-Ends", ["database"]],
77
+ ["Topic :: Database", ["database"]],
78
+ ["Topic :: Security :: Cryptography", ["crypto", "security"]],
79
+ ["Topic :: Security", ["security"]],
80
+ ["Topic :: System :: Logging", ["logging"]],
81
+ ["Topic :: System :: Distributed Computing", ["rpc"]],
82
+ ["Topic :: Communications :: Email", ["mail"]],
83
+ ["Topic :: Text Processing :: Markup :: HTML", ["html", "template"]],
84
+ ["Topic :: Text Processing :: Markup :: XML", ["xml"]],
85
+ ["Topic :: Scientific/Engineering :: Artificial Intelligence", ["ai", "ml"]],
86
+ [
87
+ "Topic :: Scientific/Engineering :: Image Recognition",
88
+ ["ml", "object-detect"],
89
+ ],
90
+ ["Topic :: Office/Business :: Financial", ["finance"]],
91
+ ["Topic :: Terminals", ["cli"]],
92
+ ["Topic :: Utilities", ["tools"]],
93
+ ];
94
+
95
+ /**
96
+ * Normalize a distribution name per PEP 503.
97
+ *
98
+ * @param {string} name Distribution name
99
+ * @returns {string} Normalized name, or an empty string for unusable input
100
+ */
101
+ export function normalizePypiName(name) {
102
+ if (!name || typeof name !== "string") {
103
+ return "";
104
+ }
105
+ let out = "";
106
+ let lastWasSeparator = false;
107
+ for (const ch of name.trim().toLowerCase()) {
108
+ if (ch === "-" || ch === "_" || ch === ".") {
109
+ if (!lastWasSeparator && out.length) {
110
+ out += "-";
111
+ }
112
+ lastWasSeparator = true;
113
+ continue;
114
+ }
115
+ out += ch;
116
+ lastWasSeparator = false;
117
+ }
118
+ return out.endsWith("-") ? out.slice(0, -1) : out;
119
+ }
120
+
121
+ /**
122
+ * `data/pypi-pkg-aliases.json` maps module name -> distribution name. Inverting it
123
+ * gives the mapping consumers actually need: distribution -> the module names it
124
+ * installs. Built once on first use.
125
+ *
126
+ * @type {Map<string, string[]> | undefined}
127
+ */
128
+ let invertedAliases;
129
+
130
+ function packageToModules() {
131
+ if (invertedAliases) {
132
+ return invertedAliases;
133
+ }
134
+ invertedAliases = new Map();
135
+ for (const [moduleName, pkgName] of Object.entries(
136
+ PYPI_MODULE_PACKAGE_MAPPING,
137
+ )) {
138
+ const key = normalizePypiName(pkgName);
139
+ if (!key || !moduleName) {
140
+ continue;
141
+ }
142
+ const existing = invertedAliases.get(key);
143
+ if (existing) {
144
+ if (!existing.includes(moduleName)) {
145
+ existing.push(moduleName);
146
+ }
147
+ } else {
148
+ invertedAliases.set(key, [moduleName]);
149
+ }
150
+ }
151
+ return invertedAliases;
152
+ }
153
+
154
+ /**
155
+ * Best-known import module names for a PyPI distribution.
156
+ *
157
+ * Two sources, in order: the curated alias map (which is where the cases that
158
+ * cannot be derived from the name live — `pyyaml` installs `yaml`, `absl-py`
159
+ * installs `absl`, `beautifulsoup4` installs `bs4`), and the name-derived form,
160
+ * which is correct for the large majority of distributions. Callers that need to
161
+ * distinguish the two should read the alias map themselves; for tagging purposes
162
+ * both are usable and a wrong extra candidate simply matches nothing.
163
+ *
164
+ * This is deliberately not a guess at *every* module a distribution ships — only
165
+ * at its top-level import names.
166
+ *
167
+ * @param {string} name Distribution name, e.g. "PyYAML"
168
+ * @returns {string[]} Candidate top-level module names, deduplicated
169
+ */
170
+ export function pypiModulesForPackage(name) {
171
+ const normalized = normalizePypiName(name);
172
+ if (!normalized) {
173
+ return [];
174
+ }
175
+ const modules = [...(packageToModules().get(normalized) || [])];
176
+ // The name-derived form. PEP 503 collapses `.` and `_` to `-`; imports use `_`.
177
+ const derived = normalized.replaceAll("-", "_");
178
+ if (derived && !modules.includes(derived)) {
179
+ modules.push(derived);
180
+ }
181
+ return modules;
182
+ }
183
+
184
+ /**
185
+ * Interpret a PyPI classifier list.
186
+ *
187
+ * @param {string[]} classifiers Raw `info.classifiers` from the PyPI JSON API
188
+ * @returns {{frameworks: string[], tags: string[]}} Declared frameworks (lowercased,
189
+ * e.g. `["django"]`) and the tag vocabulary they and the `Topic ::` lines imply
190
+ */
191
+ export function parsePypiClassifiers(classifiers) {
192
+ const frameworks = [];
193
+ const tags = new Set();
194
+ if (!Array.isArray(classifiers)) {
195
+ return { frameworks, tags: [] };
196
+ }
197
+ for (const classifier of classifiers) {
198
+ if (!classifier || typeof classifier !== "string") {
199
+ continue;
200
+ }
201
+ if (classifier.startsWith("Framework :: ")) {
202
+ // "Framework :: Django :: 5.0" -> "django". The version segment is dropped:
203
+ // it is the framework version the package supports, not a distinct signal.
204
+ const declared = classifier.split(" :: ")[1]?.trim();
205
+ if (!declared) {
206
+ continue;
207
+ }
208
+ const name = declared.toLowerCase().replaceAll(" ", "-");
209
+ if (!frameworks.includes(name)) {
210
+ frameworks.push(name);
211
+ }
212
+ tags.add(name);
213
+ if (!PYPI_NON_APPLICATION_FRAMEWORKS.has(name)) {
214
+ tags.add("framework");
215
+ }
216
+ continue;
217
+ }
218
+ if (!classifier.startsWith("Topic :: ")) {
219
+ continue;
220
+ }
221
+ for (const [prefix, topicTags] of TOPIC_TAGS) {
222
+ if (classifier === prefix || classifier.startsWith(`${prefix} ::`)) {
223
+ for (const tag of topicTags) {
224
+ tags.add(tag);
225
+ }
226
+ break;
227
+ }
228
+ }
229
+ }
230
+ return { frameworks, tags: Array.from(tags) };
231
+ }
232
+
233
+ function addProperty(pkg, name, value) {
234
+ if (!value) {
235
+ return;
236
+ }
237
+ if (!pkg.properties) {
238
+ pkg.properties = [];
239
+ }
240
+ const existing = pkg.properties.find((prop) => prop.name === name);
241
+ if (existing) {
242
+ existing.value = value;
243
+ return;
244
+ }
245
+ pkg.properties.push({ name, value });
246
+ }
247
+
248
+ /**
249
+ * Attach classifier-derived tags and properties to a package.
250
+ *
251
+ * Tags are merged into any the package already carries rather than replacing
252
+ * them — `lib/managers/binary.js` sets `tags: ["source"]` as an identity marker,
253
+ * and losing it would change how that package is reconciled later.
254
+ *
255
+ * @param {Object} pkg Package object, mutated in place
256
+ * @param {string[]} classifiers Raw `info.classifiers` from the PyPI JSON API
257
+ * @returns {Object} The same package object
258
+ */
259
+ export function applyPypiClassifierMetadata(pkg, classifiers) {
260
+ if (!pkg || !Array.isArray(classifiers) || !classifiers.length) {
261
+ return pkg;
262
+ }
263
+ const { frameworks, tags } = parsePypiClassifiers(classifiers);
264
+ if (tags.length) {
265
+ const merged = new Set(Array.isArray(pkg.tags) ? pkg.tags : []);
266
+ for (const tag of tags) {
267
+ merged.add(tag);
268
+ }
269
+ pkg.tags = Array.from(merged);
270
+ }
271
+ if (frameworks.length) {
272
+ addProperty(pkg, "cdx:pypi:frameworks", frameworks.join(","));
273
+ }
274
+ // The full list is retained so that consumers wanting the whole taxonomy are
275
+ // not limited to the tag vocabulary mapped above. Classifiers come from a
276
+ // closed PyPI-maintained set, so there is no free-text or secret exposure.
277
+ addProperty(
278
+ pkg,
279
+ "cdx:pypi:classifiers",
280
+ classifiers.filter((c) => typeof c === "string" && c.length).join("\n"),
281
+ );
282
+ return pkg;
283
+ }
284
+
285
+ /**
286
+ * Attach the distribution's top-level import module names as `internal:Namespaces`.
287
+ *
288
+ * This is the same property jar, gem, nuget and composer components already carry,
289
+ * so downstream consumers that map namespaces back to components need no
290
+ * pypi-specific path. Unlike the jar case the value is name-derived and
291
+ * alias-curated rather than read out of the artifact, which is why
292
+ * `cdx:pypi:modulesFrom` records the provenance.
293
+ *
294
+ * @param {Object} pkg Package object, mutated in place
295
+ * @returns {Object} The same package object
296
+ */
297
+ export function applyPypiModuleNames(pkg) {
298
+ if (!pkg?.name) {
299
+ return pkg;
300
+ }
301
+ // Extras (`requests[security]`) are not part of the distribution name.
302
+ const name = pkg.name.includes("[") ? pkg.name.split("[")[0] : pkg.name;
303
+ if (name.includes("https")) {
304
+ return pkg;
305
+ }
306
+ const normalized = normalizePypiName(name);
307
+ const modules = pypiModulesForPackage(name);
308
+ if (!modules.length) {
309
+ return pkg;
310
+ }
311
+ // Most alias-map entries map a distribution to the module its own name already
312
+ // implies, so the map having an entry says nothing. The provenance worth
313
+ // recording is whether it contributed a module the name could not have given -
314
+ // that is the difference between `flask` and `pyyaml` -> `yaml`.
315
+ const derived = normalized.replaceAll("-", "_");
316
+ const curatedAddedSomething = (packageToModules().get(normalized) || []).some(
317
+ (moduleName) => moduleName !== derived,
318
+ );
319
+ addProperty(pkg, "internal:Namespaces", modules.join("\n"));
320
+ addProperty(
321
+ pkg,
322
+ "cdx:pypi:modulesFrom",
323
+ curatedAddedSomething ? "alias-map,distribution-name" : "distribution-name",
324
+ );
325
+ return pkg;
326
+ }
@@ -331,6 +331,53 @@ export function get_python_command_from_env(env) {
331
331
  return fallbackCmd;
332
332
  }
333
333
 
334
+ /**
335
+ * Is this import satisfied by a module that lives in the project itself?
336
+ *
337
+ * `atom parsedeps` reports every non-stdlib import it sees, including imports of the
338
+ * project's own packages. Attributing those to a distribution invents dependencies:
339
+ * `data/pypi-pkg-aliases.json` maps the module `app` to `zope2`, `test` to `pytabix`,
340
+ * `models` to `asposestorage` and `src` to `auto-mix-prep`, so any project with a
341
+ * local package of that name acquires an obscure component it never depended on -
342
+ * along with every namespace and tag that component carries downstream. Filtering by
343
+ * name would need a denylist of every plausible first-party name; asking the
344
+ * filesystem whether the module is ours answers it exactly.
345
+ *
346
+ * Checks the project root and a `src/` layout for a module file, a package directory,
347
+ * and a PEP 420 namespace package directory.
348
+ *
349
+ * @param {string} src Path to the project being analysed
350
+ * @param {string} name Imported top-level module name
351
+ * @returns {boolean} `true` if the project supplies this module itself
352
+ */
353
+ export function isFirstPartyModule(src, name) {
354
+ if (!src || !name || name.includes("/") || name.includes("\\")) {
355
+ return false;
356
+ }
357
+ for (const root of [src, join(src, "src")]) {
358
+ if (
359
+ safeExistsSync(join(root, `${name}.py`)) ||
360
+ safeExistsSync(join(root, name, "__init__.py")) ||
361
+ safeExistsSync(join(root, name, "__main__.py"))
362
+ ) {
363
+ return true;
364
+ }
365
+ // PEP 420 namespace package: a directory of the right name holding python
366
+ // sources but no __init__.py.
367
+ const candidate = join(root, name);
368
+ if (safeExistsSync(candidate)) {
369
+ try {
370
+ if (readdirSync(candidate).some((f) => f.endsWith(".py"))) {
371
+ return true;
372
+ }
373
+ } catch {
374
+ // not a readable directory - treat as not first-party
375
+ }
376
+ }
377
+ }
378
+ return false;
379
+ }
380
+
334
381
  /**
335
382
  * Method to find python modules by parsing the imports and then checking with PyPI to obtain the latest version
336
383
  *
@@ -362,7 +409,8 @@ export async function getPyModules(src, epkgList, options) {
362
409
  (x) =>
363
410
  !pyDefaultModules.has(x.name.toLowerCase()) &&
364
411
  !x.name.startsWith("_") &&
365
- !x.name.startsWith("."),
412
+ !x.name.startsWith(".") &&
413
+ !isFirstPartyModule(src, x.name),
366
414
  );
367
415
  let pkgList = modList.map((p) => {
368
416
  const apkg = {
@@ -19,6 +19,7 @@ import {
19
19
  prefetchEnabled,
20
20
  prefetchedResponse,
21
21
  prefetchJson,
22
+ recordPolicyDegradationFromError,
22
23
  } from "../inventory/fetchBatch.js";
23
24
 
24
25
  // FIXME. This has to get removed, once we improve the module detection one-liner.
@@ -780,6 +781,7 @@ export async function getRubyGemsMetadata(pkgList) {
780
781
  continue;
781
782
  }
782
783
  } catch (err) {
784
+ recordPolicyDegradationFromError(err);
783
785
  if (DEBUG_MODE) {
784
786
  console.error(name, err);
785
787
  }
@@ -832,6 +834,7 @@ async function enrichGemFromVersionEndpoint(
832
834
  }
833
835
  applyGemMetadata(p, body);
834
836
  } catch (err) {
837
+ recordPolicyDegradationFromError(err);
835
838
  if (DEBUG_MODE) {
836
839
  console.error(p, err);
837
840
  }
@@ -1241,6 +1244,16 @@ export async function parseGemspecData(gemspecData, gemspecFile) {
1241
1244
  pkg.externalReferences.push({ type: refType, url: value, comment: key });
1242
1245
  }
1243
1246
  }
1247
+ // `s.homepage = "..."` is the project website. Keep it as an external
1248
+ // reference; the transient `homepage` key is a plain string that never
1249
+ // reaches the BOM because component conversion expects the `{ url }` shape.
1250
+ if (pkg.homepage) {
1251
+ pkg.externalReferences = pkg.externalReferences || [];
1252
+ if (!pkg.externalReferences.some((ref) => ref.url === pkg.homepage)) {
1253
+ pkg.externalReferences.push({ type: "website", url: pkg.homepage });
1254
+ }
1255
+ delete pkg.homepage;
1256
+ }
1244
1257
  if (pkg.name) {
1245
1258
  const purlString = toGemPurl(pkg.name, pkg.version, gemPlatform);
1246
1259
  pkg.purl = purlString;
@@ -9,11 +9,16 @@ import {
9
9
  safeRmSync,
10
10
  safeSpawnSync,
11
11
  } from "../core/fs.js";
12
+ import { dirNameStr } from "../core/paths.js";
12
13
  import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
13
14
  import { resolvePluginBinary } from "../inventory/plugins.js";
14
15
 
15
16
  const RUST_LANGUAGES = new Set(["rust", "rs", "rust-lang"]);
16
17
 
18
+ const RUSI_CRYPTO_OIDS = JSON.parse(
19
+ readFileSync(join(dirNameStr, "data", "crypto-oid.json"), "utf-8"),
20
+ );
21
+
17
22
  /**
18
23
  * Resolves the rusi binary.
19
24
  *
@@ -82,6 +87,75 @@ function createPurlAliasMap(components = []) {
82
87
  return purlAliasMap;
83
88
  }
84
89
 
90
+ /**
91
+ * Path roots that never name a dependency crate.
92
+ */
93
+ const RUST_NON_CRATE_PATH_ROOTS = new Set([
94
+ "crate",
95
+ "self",
96
+ "super",
97
+ "std",
98
+ "core",
99
+ "alloc",
100
+ "proc_macro",
101
+ "test",
102
+ ]);
103
+
104
+ /**
105
+ * Index the cargo components by the identifier a Rust path spells them with.
106
+ *
107
+ * A crate named `cfg-if` is written `cfg_if` in source, and a lock file can
108
+ * carry several versions of the same crate, so a name maps to every matching
109
+ * purl.
110
+ *
111
+ * @param {Object[]} components Components from the BOM
112
+ * @returns {Map<string, string[]>} Crate identifier to purls
113
+ */
114
+ function createCrateNameIndex(components = []) {
115
+ const crateNameIndex = new Map();
116
+ for (const component of components) {
117
+ if (!component?.purl?.startsWith("pkg:cargo/") || !component.name) {
118
+ continue;
119
+ }
120
+ const crateIdentifier = component.name.toLowerCase().replaceAll("-", "_");
121
+ const purls = crateNameIndex.get(crateIdentifier) || [];
122
+ if (!purls.includes(component.purl)) {
123
+ purls.push(component.purl);
124
+ }
125
+ crateNameIndex.set(crateIdentifier, purls);
126
+ }
127
+ return crateNameIndex;
128
+ }
129
+
130
+ /**
131
+ * Resolve the dependency crates a Rust path refers to.
132
+ *
133
+ * rusi reports an import against the package that declares it, so the imported
134
+ * crate has to be recovered from the path itself. This is what turns an
135
+ * `use hex::encode;` into usage evidence for the `hex` component.
136
+ *
137
+ * @param {string} importPath Rust path, such as `hex::encode`
138
+ * @param {Map<string, string[]>} crateNameIndex Index from {@link createCrateNameIndex}
139
+ * @param {string} [ownerPurl] purl of the package that declares the path
140
+ * @returns {string[]} Matching component purls
141
+ */
142
+ function resolveImportedCratePurls(importPath, crateNameIndex, ownerPurl) {
143
+ if (!importPath || !crateNameIndex?.size) {
144
+ return [];
145
+ }
146
+ const pathRoot = String(importPath)
147
+ .replace(/^::/, "")
148
+ .split("::")[0]
149
+ .trim()
150
+ .toLowerCase();
151
+ if (!pathRoot || RUST_NON_CRATE_PATH_ROOTS.has(pathRoot)) {
152
+ return [];
153
+ }
154
+ return (crateNameIndex.get(pathRoot) || []).filter(
155
+ (purl) => purl !== ownerPurl,
156
+ );
157
+ }
158
+
85
159
  function resolveComponentPurl(purl, purlAliasMap) {
86
160
  if (!purl) {
87
161
  return undefined;
@@ -182,19 +256,37 @@ function mergeCryptoComponent(componentsByRef, component, item) {
182
256
  return existing;
183
257
  }
184
258
 
259
+ // The schema requires an oid for algorithm assets, so an algorithm the OID
260
+ // table cannot name is skipped rather than emitted invalid. rusi reports
261
+ // display-cased names ("SHA-256") while the table is lowercase ("sha-256").
262
+ function resolveCryptoAlgorithmMetadata(name) {
263
+ if (!name) {
264
+ return undefined;
265
+ }
266
+ const lowered = String(name).toLowerCase();
267
+ return (
268
+ RUSI_CRYPTO_OIDS[name] ||
269
+ RUSI_CRYPTO_OIDS[lowered] ||
270
+ RUSI_CRYPTO_OIDS[lowered.replace(/[\s_]+/g, "-")]
271
+ );
272
+ }
273
+
185
274
  function cryptoAlgorithmComponent(asset) {
275
+ const algorithmName = asset.algorithm || asset.kind || "unknown";
276
+ const algorithmMetadata = resolveCryptoAlgorithmMetadata(algorithmName);
277
+ if (!algorithmMetadata?.oid) {
278
+ return undefined;
279
+ }
186
280
  const component = {
187
281
  type: "cryptographic-asset",
188
- name: asset.algorithm || asset.kind || "unknown",
189
- "bom-ref": cryptoBomRef(
190
- "algorithm",
191
- asset.algorithm || asset.kind,
192
- asset.provider || "unknown",
193
- ),
282
+ name: algorithmName,
283
+ "bom-ref": cryptoBomRef("algorithm", algorithmName, algorithmMetadata.oid),
194
284
  description:
285
+ algorithmMetadata.description ||
195
286
  "Cryptographic algorithm/component detected by rusi source analysis",
196
287
  cryptoProperties: {
197
288
  assetType: "algorithm",
289
+ oid: algorithmMetadata.oid,
198
290
  algorithmProperties: { primitive: asset.kind || "unknown" },
199
291
  },
200
292
  properties: [],
@@ -361,6 +453,7 @@ function addImportEvidence(
361
453
  purlAliasMap,
362
454
  purlLocationMap,
363
455
  componentPropertiesMap,
456
+ crateNameIndex,
364
457
  ) {
365
458
  for (const importUsage of rusiReport.imports || []) {
366
459
  const purl = resolveComponentPurl(importUsage.purl, purlAliasMap);
@@ -380,6 +473,31 @@ function addImportEvidence(
380
473
  "cdx:rusi:importAlias",
381
474
  importUsage.alias,
382
475
  );
476
+ // The import is also evidence about the crate being imported, which is the
477
+ // component a consumer filters on.
478
+ for (const cratePurl of resolveImportedCratePurls(
479
+ importUsage.path,
480
+ crateNameIndex,
481
+ purl,
482
+ )) {
483
+ addSetValue(
484
+ purlLocationMap,
485
+ cratePurl,
486
+ positionLocation(importUsage.position),
487
+ );
488
+ addPropertyValue(
489
+ componentPropertiesMap,
490
+ cratePurl,
491
+ "cdx:rusi:importedBy",
492
+ purl,
493
+ );
494
+ addPropertyValue(
495
+ componentPropertiesMap,
496
+ cratePurl,
497
+ "cdx:rusi:importPath",
498
+ importUsage.path,
499
+ );
500
+ }
383
501
  }
384
502
  }
385
503
 
@@ -761,6 +879,7 @@ export function analyzeRusiProject(src, options = {}) {
761
879
  */
762
880
  export function collectRusiEvidence(rusiReport = {}, components = []) {
763
881
  const purlAliasMap = createPurlAliasMap(components);
882
+ const crateNameIndex = createCrateNameIndex(components);
764
883
  const purlLocationMap = {};
765
884
  const dataFlowFrames = {};
766
885
  const componentPropertiesMap = {};
@@ -774,6 +893,7 @@ export function collectRusiEvidence(rusiReport = {}, components = []) {
774
893
  purlAliasMap,
775
894
  purlLocationMap,
776
895
  componentPropertiesMap,
896
+ crateNameIndex,
777
897
  );
778
898
  addUsageEvidence(
779
899
  rusiReport,