@cdxgen/cdxgen 13.1.0 → 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 (186) hide show
  1. package/README.md +8 -7
  2. package/bin/cdxgen.js +16 -3
  3. package/bin/evinse.js +2 -0
  4. package/data/crypto-oid.json +88 -12
  5. package/data/remediations.json +23 -0
  6. package/data/rules/ai-bom.yaml +78 -0
  7. package/data/rules/cargo-rust.yaml +132 -0
  8. package/data/rules/mcp-servers.yaml +122 -0
  9. package/lib/audit/targets.js +6 -1
  10. package/lib/cli/bomAssembly.js +12 -0
  11. package/lib/cli/index.js +192 -1
  12. package/lib/cli/jsBom.js +59 -2
  13. package/lib/cli/jvmBom.js +22 -0
  14. package/lib/cli/managedBom.js +152 -0
  15. package/lib/cli/nativeBom.js +798 -1
  16. package/lib/core/activity.js +120 -2
  17. package/lib/core/env.js +39 -1
  18. package/lib/core/userAgent.js +151 -0
  19. package/lib/ecosystems/cargoCache.js +364 -0
  20. package/lib/ecosystems/caxa.js +13 -5
  21. package/lib/ecosystems/cppEvidence.js +40 -2
  22. package/lib/ecosystems/ecosystems.js +241 -91
  23. package/lib/ecosystems/golem.js +11 -1
  24. package/lib/ecosystems/gradleutils.js +143 -2
  25. package/lib/ecosystems/kosi.js +1256 -0
  26. package/lib/ecosystems/parsers-crystal.js +179 -0
  27. package/lib/ecosystems/parsers-dotnet.js +69 -39
  28. package/lib/ecosystems/parsers-elm.js +816 -0
  29. package/lib/ecosystems/parsers-gleam.js +1 -1
  30. package/lib/ecosystems/parsers-julia.js +268 -0
  31. package/lib/ecosystems/parsers-lua.js +218 -0
  32. package/lib/ecosystems/parsers-misc.js +172 -0
  33. package/lib/ecosystems/parsers-nim.js +221 -0
  34. package/lib/ecosystems/parsers-opam.js +180 -0
  35. package/lib/ecosystems/parsers-perl.js +277 -0
  36. package/lib/ecosystems/parsers-python.js +1 -1
  37. package/lib/ecosystems/parsers-r.js +148 -0
  38. package/lib/ecosystems/parsers-rebar.js +411 -0
  39. package/lib/ecosystems/parsers-rust.js +932 -17
  40. package/lib/ecosystems/parsers-spack.js +204 -0
  41. package/lib/ecosystems/parsers-stack.js +264 -0
  42. package/lib/ecosystems/parsers-terraform.js +194 -0
  43. package/lib/ecosystems/rusi.js +126 -6
  44. package/lib/evinser/evinser.js +111 -1
  45. package/lib/helpers/agenticToolutils.js +925 -0
  46. package/lib/inventory/agentFormulationParser.js +57 -4
  47. package/lib/inventory/aiCollector.js +504 -10
  48. package/lib/inventory/aiInventory.js +12 -0
  49. package/lib/inventory/analyzer.js +488 -30
  50. package/lib/inventory/astWorker.js +6 -0
  51. package/lib/inventory/auditCategories.js +8 -0
  52. package/lib/inventory/cbomutils.js +30 -0
  53. package/lib/inventory/cdxrs.js +7 -2
  54. package/lib/inventory/communityAiConfigParser.js +215 -2
  55. package/lib/inventory/dosai.js +77 -0
  56. package/lib/inventory/envcontext.js +9 -6
  57. package/lib/inventory/evidenceUtils.js +13 -0
  58. package/lib/inventory/fetchBatch.js +74 -2
  59. package/lib/inventory/fetchRate.js +74 -11
  60. package/lib/inventory/forgeEnricher.js +3 -2
  61. package/lib/inventory/jvmToolEnv.js +6 -5
  62. package/lib/inventory/mcpConfigParser.js +166 -2
  63. package/lib/inventory/mcpDiscovery.js +11 -0
  64. package/lib/inventory/mcpServerManifest.js +389 -0
  65. package/lib/inventory/mcpbBundle.js +419 -0
  66. package/lib/inventory/plugins.js +3 -0
  67. package/lib/inventory/protobom.js +54 -88
  68. package/lib/inventory/protobomLoader.js +4 -0
  69. package/lib/inventory/rustFormulationParser.js +1 -1
  70. package/lib/inventory/source.js +10 -4
  71. package/lib/inventory/stylePackageRefs.js +41 -0
  72. package/lib/inventory/svelteUtils.js +524 -0
  73. package/lib/parsers/huggingfaceUtils.js +15 -7
  74. package/lib/parsers/onnx.js +294 -0
  75. package/lib/parsers/safetensors.js +114 -0
  76. package/lib/stages/postgen/introspection/markers.js +29 -6
  77. package/lib/stages/postgen/postgen.js +169 -1
  78. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  79. package/package.json +25 -25
  80. package/types/lib/audit/targets.d.ts.map +1 -1
  81. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  82. package/types/lib/cli/index.d.ts +2 -2
  83. package/types/lib/cli/index.d.ts.map +1 -1
  84. package/types/lib/cli/jsBom.d.ts +23 -0
  85. package/types/lib/cli/jsBom.d.ts.map +1 -1
  86. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  87. package/types/lib/cli/managedBom.d.ts +33 -0
  88. package/types/lib/cli/managedBom.d.ts.map +1 -1
  89. package/types/lib/cli/nativeBom.d.ts +88 -8
  90. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  91. package/types/lib/core/activity.d.ts.map +1 -1
  92. package/types/lib/core/env.d.ts +21 -0
  93. package/types/lib/core/env.d.ts.map +1 -1
  94. package/types/lib/core/userAgent.d.ts +56 -0
  95. package/types/lib/core/userAgent.d.ts.map +1 -0
  96. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  97. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  98. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  99. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  100. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  101. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  102. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  103. package/types/lib/ecosystems/gradleutils.d.ts +20 -0
  104. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  105. package/types/lib/ecosystems/kosi.d.ts +73 -0
  106. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  107. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  108. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  109. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  110. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  111. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  112. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  113. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  114. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  115. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  116. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  117. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  118. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  120. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  121. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  122. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  123. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  124. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  125. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  126. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  127. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  128. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  129. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  130. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  131. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  132. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  133. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  134. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  135. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  136. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  137. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  138. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  139. package/types/lib/evinser/evinser.d.ts +29 -0
  140. package/types/lib/evinser/evinser.d.ts.map +1 -1
  141. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  142. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  143. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  144. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  145. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  146. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  147. package/types/lib/inventory/auditCategories.d.ts +4 -0
  148. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  149. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  150. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  151. package/types/lib/inventory/dosai.d.ts +23 -0
  152. package/types/lib/inventory/dosai.d.ts.map +1 -1
  153. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  154. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  155. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  156. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  157. package/types/lib/inventory/fetchRate.d.ts +9 -6
  158. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  159. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  160. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  161. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  162. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  163. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  164. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  165. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  166. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  167. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  168. package/types/lib/inventory/plugins.d.ts.map +1 -1
  169. package/types/lib/inventory/protobom.d.ts +18 -13
  170. package/types/lib/inventory/protobom.d.ts.map +1 -1
  171. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  172. package/types/lib/inventory/source.d.ts.map +1 -1
  173. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  174. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  175. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  176. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  177. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  178. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  179. package/types/lib/parsers/onnx.d.ts +28 -0
  180. package/types/lib/parsers/onnx.d.ts.map +1 -0
  181. package/types/lib/parsers/safetensors.d.ts +22 -0
  182. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  183. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  184. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  185. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  186. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -3,7 +3,7 @@ import process from "node:process";
3
3
 
4
4
  import { createHttpClient, getDefaultHttpTimeoutMs } from "./httpClient.js";
5
5
  import { traceLog } from "./logger.js";
6
- import { CDXGEN_VERSION } from "./state.js";
6
+ import { getUserAgent } from "./userAgent.js";
7
7
 
8
8
  /** True when CDXGEN_DEBUG_MODE or SCAN_DEBUG_MODE requests debug/verbose output. */
9
9
  export const DEBUG_MODE =
@@ -85,6 +85,30 @@ const LOCKFILE_ACTIVITY_HINTS = new Map([
85
85
  label: "Bundler lockfile",
86
86
  },
87
87
  ],
88
+ [
89
+ "cpanfile.snapshot",
90
+ {
91
+ classification: "lockfile",
92
+ ecosystem: "cpan",
93
+ label: "Carton snapshot",
94
+ },
95
+ ],
96
+ [
97
+ "manifest.toml",
98
+ {
99
+ classification: "lockfile",
100
+ ecosystem: "gleam",
101
+ label: "Gleam or Julia lockfile",
102
+ },
103
+ ],
104
+ [
105
+ "nimble.lock",
106
+ {
107
+ classification: "lockfile",
108
+ ecosystem: "nim",
109
+ label: "Nimble lockfile",
110
+ },
111
+ ],
88
112
  [
89
113
  "package-lock.json",
90
114
  { classification: "lockfile", ecosystem: "npm", label: "npm lockfile" },
@@ -125,6 +149,62 @@ const LOCKFILE_ACTIVITY_HINTS = new Map([
125
149
  label: "PEP 751 lockfile",
126
150
  },
127
151
  ],
152
+ [
153
+ "renv.lock",
154
+ {
155
+ classification: "lockfile",
156
+ ecosystem: "cran",
157
+ label: "renv lockfile",
158
+ },
159
+ ],
160
+ [
161
+ "rebar.lock",
162
+ {
163
+ classification: "lockfile",
164
+ ecosystem: "hex",
165
+ label: "rebar3 lockfile",
166
+ },
167
+ ],
168
+ [
169
+ "shard.lock",
170
+ {
171
+ classification: "lockfile",
172
+ ecosystem: "crystal",
173
+ label: "Crystal shards lockfile",
174
+ },
175
+ ],
176
+ [
177
+ "spack.lock",
178
+ {
179
+ classification: "lockfile",
180
+ ecosystem: "spack",
181
+ label: "Spack lockfile",
182
+ },
183
+ ],
184
+ [
185
+ "stack.lock",
186
+ {
187
+ classification: "lockfile",
188
+ ecosystem: "hackage",
189
+ label: "Stack lockfile",
190
+ },
191
+ ],
192
+ [
193
+ "stack.yaml.lock",
194
+ {
195
+ classification: "lockfile",
196
+ ecosystem: "hackage",
197
+ label: "Stack lockfile",
198
+ },
199
+ ],
200
+ [
201
+ ".terraform.lock.hcl",
202
+ {
203
+ classification: "lockfile",
204
+ ecosystem: "terraform",
205
+ label: "Terraform provider lockfile",
206
+ },
207
+ ],
128
208
  [
129
209
  "uv.lock",
130
210
  { classification: "lockfile", ecosystem: "python", label: "uv lockfile" },
@@ -167,6 +247,18 @@ const MANIFEST_ACTIVITY_HINTS = new Map([
167
247
  "pom.xml",
168
248
  { classification: "manifest", ecosystem: "maven", label: "Maven manifest" },
169
249
  ],
250
+ [
251
+ "cpanfile",
252
+ { classification: "manifest", ecosystem: "cpan", label: "CPAN manifest" },
253
+ ],
254
+ [
255
+ "project.toml",
256
+ {
257
+ classification: "manifest",
258
+ ecosystem: "julia",
259
+ label: "Julia project manifest",
260
+ },
261
+ ],
170
262
  [
171
263
  "pyproject.toml",
172
264
  {
@@ -942,10 +1034,35 @@ function inferNetworkIntent(requestUrl) {
942
1034
  return "metadata-fetch";
943
1035
  }
944
1036
 
1037
+ /**
1038
+ * Set the request's user-agent to the value resolved for its destination host,
1039
+ * so that a `CDXGEN_USER_AGENT_<REGISTRY>` override reaches the registry it
1040
+ * names. A user-agent a caller set explicitly is left alone; only the client's
1041
+ * own default, which carries no host context, is replaced.
1042
+ *
1043
+ * @param {Object} options Request options being prepared by the HTTP client.
1044
+ * @returns {void}
1045
+ */
1046
+ function applyUserAgent(options) {
1047
+ const headers = options.headers || (options.headers = {});
1048
+ const existingKey = Object.keys(headers).find(
1049
+ (name) => name.toLowerCase() === "user-agent",
1050
+ );
1051
+ if (existingKey && headers[existingKey] !== getUserAgent()) {
1052
+ return;
1053
+ }
1054
+ if (existingKey) {
1055
+ delete headers[existingKey];
1056
+ }
1057
+ headers["user-agent"] = getUserAgent(options.url?.hostname);
1058
+ }
1059
+
945
1060
  /** The shared got-compatible HTTP client for all outbound requests, enforcing dry-run, host-allow-list, and secure-mode policies via beforeRequest/afterResponse/beforeError hooks. */
946
1061
  export const cdxgenAgent = createHttpClient({
947
1062
  headers: {
948
- "user-agent": `@CycloneDX/cdxgen ${CDXGEN_VERSION}`,
1063
+ // Per-host overrides are applied in beforeRequest; this is the fallback for
1064
+ // any code path that reads the client's configured headers directly.
1065
+ "user-agent": getUserAgent(),
949
1066
  },
950
1067
  // Bound every request so an unresponsive host cannot hang a scan. Overridable
951
1068
  // via CDXGEN_HTTP_TIMEOUT_MS; individual calls may pass a smaller `timeout`.
@@ -959,6 +1076,7 @@ export const cdxgenAgent = createHttpClient({
959
1076
  (options) => {
960
1077
  const networkIntent =
961
1078
  options.context?.activityIntent || inferNetworkIntent(options.url);
1079
+ applyUserAgent(options);
962
1080
  const allowedHostsEnv = readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS");
963
1081
  const hostAllowed = isAllowedHttpHost(
964
1082
  options.url.hostname,
package/lib/core/env.js CHANGED
@@ -256,6 +256,19 @@ if (readEnvironmentVariable("CARGO_CMD")) {
256
256
  CARGO_CMD = readEnvironmentVariable("CARGO_CMD");
257
257
  }
258
258
 
259
+ /**
260
+ * Target triple the cargo resolve graph is filtered to, so that a
261
+ * cross-compiled project can be described for the triple it ships on rather
262
+ * than for the union of every triple its lock file covers.
263
+ *
264
+ * `CDXGEN_CARGO_TARGET` is cdxgen's own name for it; `CARGO_BUILD_TARGET` is
265
+ * cargo's standard variable, which a cross-compiling build already sets.
266
+ */
267
+ export const CARGO_TARGET =
268
+ readEnvironmentVariable("CDXGEN_CARGO_TARGET") ||
269
+ readEnvironmentVariable("CARGO_BUILD_TARGET") ||
270
+ undefined;
271
+
259
272
  /** Resolved Clojure CLI executable command (CLJ_CMD env var, or "clj"). */
260
273
  export let CLJ_CMD = "clj";
261
274
  if (readEnvironmentVariable("CLJ_CMD")) {
@@ -307,6 +320,7 @@ export const PROJECT_TYPE_ALIASES = {
307
320
  "java24",
308
321
  "java25",
309
322
  "java26",
323
+ "java27",
310
324
  "groovy",
311
325
  "kotlin",
312
326
  "kt",
@@ -339,6 +353,9 @@ export const PROJECT_TYPE_ALIASES = {
339
353
  "nodejs20",
340
354
  "nodejs22",
341
355
  "nodejs23",
356
+ "nodejs24",
357
+ "nodejs25",
358
+ "nodejs26",
342
359
  "node",
343
360
  "node8",
344
361
  "node10",
@@ -349,11 +366,16 @@ export const PROJECT_TYPE_ALIASES = {
349
366
  "node20",
350
367
  "node22",
351
368
  "node23",
369
+ "node24",
370
+ "node25",
371
+ "node26",
352
372
  "js",
353
373
  "javascript",
354
374
  "typescript",
355
375
  "ts",
356
376
  "tsx",
377
+ "svelte",
378
+ "sveltekit",
357
379
  "yarn",
358
380
  "rush",
359
381
  "bun",
@@ -361,6 +383,7 @@ export const PROJECT_TYPE_ALIASES = {
361
383
  ],
362
384
  mcp: ["mcp"],
363
385
  "ai-skill": ["ai-skill", "skill", "skills"],
386
+ agentic: ["agentic", "agentic-cli", "ai-tools", "ai-cli"],
364
387
  py: [
365
388
  "py",
366
389
  "python",
@@ -372,6 +395,8 @@ export const PROJECT_TYPE_ALIASES = {
372
395
  "python311",
373
396
  "python312",
374
397
  "python313",
398
+ "python314",
399
+ "python315",
375
400
  "pixi",
376
401
  "pip",
377
402
  "poetry",
@@ -398,6 +423,8 @@ export const PROJECT_TYPE_ALIASES = {
398
423
  "dotnet7",
399
424
  "dotnet8",
400
425
  "dotnet9",
426
+ "dotnet10",
427
+ "dotnet11",
401
428
  "dotnet-framework",
402
429
  "dotnet-framework47",
403
430
  "dotnet-framework48",
@@ -417,8 +444,19 @@ export const PROJECT_TYPE_ALIASES = {
417
444
  "hmiproj",
418
445
  ],
419
446
  dart: ["dart", "flutter", "pub"],
420
- haskell: ["haskell", "hackage", "cabal"],
447
+ haskell: ["haskell", "hackage", "cabal", "stack", "stackage"],
421
448
  elixir: ["elixir", "hex", "mix"],
449
+ erlang: ["erlang", "rebar", "rebar3"],
450
+ julia: ["julia"],
451
+ r: ["r", "cran", "renv"],
452
+ ocaml: ["ocaml", "opam", "dune"],
453
+ elm: ["elm"],
454
+ crystal: ["crystal", "shards"],
455
+ nim: ["nim", "nimble"],
456
+ lua: ["lua", "luarocks"],
457
+ perl: ["perl", "cpan", "carton", "cpanfile"],
458
+ spack: ["spack"],
459
+ terraform: ["terraform", "opentofu", "tf"],
422
460
  c: ["c", "cpp", "c++", "conan", "collider"],
423
461
  clojure: ["clojure", "edn", "clj", "leiningen"],
424
462
  github: ["github", "actions"],
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Outbound `user-agent` construction.
3
+ *
4
+ * Package registries police automated traffic through this header, and their
5
+ * requirements differ. GitHub rejects an absent one with a 403; crates.io
6
+ * rejects an absent one and additionally blocks a configurable list of exact
7
+ * strings and prefixes at the CDN edge; Packagist asks for a `mailto=` contact;
8
+ * PyPI asks for a value that uniquely identifies the consumer, with contact
9
+ * information; pub.dev documents a `name/version (+url)` shape. The default
10
+ * built here satisfies all of them at once.
11
+ *
12
+ * Two escape hatches exist for operators whose traffic a registry has taken
13
+ * issue with: `CDXGEN_USER_AGENT` replaces the header everywhere, and
14
+ * `CDXGEN_USER_AGENT_<REGISTRY>` replaces it for one destination.
15
+ */
16
+
17
+ import { CDXGEN_VERSION } from "./state.js";
18
+
19
+ /** Project home page offered as the contact URL in the default user-agent. */
20
+ const CDXGEN_HOME_URL = "https://github.com/cdxgen/cdxgen";
21
+
22
+ /**
23
+ * Contact address offered in the default user-agent. Packagist asks API
24
+ * consumers to include one so an operator can reach the project rather than
25
+ * block its traffic; crates.io and PyPI make the same recommendation.
26
+ * Overridable with CDXGEN_CONTACT_EMAIL.
27
+ */
28
+ const CDXGEN_CONTACT_EMAIL = "cloud@appthreat.com";
29
+
30
+ /**
31
+ * Registry keys and the hosts they cover, used to resolve the
32
+ * `CDXGEN_USER_AGENT_<REGISTRY>` override for a request. A host matches its
33
+ * entry exactly or as a subdomain of it.
34
+ */
35
+ const REGISTRY_HOSTS = Object.freeze({
36
+ CRATES: ["crates.io", "static.crates.io", "index.crates.io"],
37
+ DOCKER: ["docker.io", "docker.com"],
38
+ ELM: ["package.elm-lang.org"],
39
+ GITHUB: ["github.com", "githubusercontent.com", "ghcr.io"],
40
+ GITLAB: ["gitlab.com"],
41
+ GOPROXY: ["proxy.golang.org", "sum.golang.org", "pkg.go.dev"],
42
+ HUGGINGFACE: ["huggingface.co"],
43
+ JSR: ["jsr.io"],
44
+ MAVEN: [
45
+ "repo1.maven.org",
46
+ "repo.maven.apache.org",
47
+ "search.maven.org",
48
+ "central.sonatype.com",
49
+ "oss.sonatype.org",
50
+ ],
51
+ NPM: ["registry.npmjs.org", "npmjs.org", "npmjs.com"],
52
+ NUGET: ["nuget.org"],
53
+ PACKAGIST: ["packagist.org"],
54
+ PUB: ["pub.dev"],
55
+ PYPI: ["pypi.org", "pythonhosted.org"],
56
+ RUBYGEMS: ["rubygems.org"],
57
+ });
58
+
59
+ /**
60
+ * Substrings that crates.io blocks at the edge, taken from the client
61
+ * libraries its middleware singles out. A user-agent containing one of these
62
+ * is refused with an empty-bodied 403, so an override carrying one would fail
63
+ * silently against that registry.
64
+ */
65
+ const BLOCKED_SUBSTRINGS = Object.freeze([
66
+ "curl",
67
+ "python-requests",
68
+ "Go-http-client",
69
+ ]);
70
+
71
+ /**
72
+ * The registry key covering a host, if any.
73
+ *
74
+ * @param {String} hostname Lower-case hostname of the request
75
+ * @returns {String|undefined} Registry key such as `CRATES`, or undefined
76
+ */
77
+ export function registryKeyForHost(hostname) {
78
+ if (!hostname) {
79
+ return undefined;
80
+ }
81
+ const host = hostname.toLowerCase().replace(/\.$/, "");
82
+ for (const [key, hosts] of Object.entries(REGISTRY_HOSTS)) {
83
+ for (const candidate of hosts) {
84
+ if (host === candidate || host.endsWith(`.${candidate}`)) {
85
+ return key;
86
+ }
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ /**
93
+ * The default user-agent: `cdxgen/<version> (+<home url>; mailto=<address>)`.
94
+ * Setting CDXGEN_CONTACT_EMAIL substitutes a different address, which is what
95
+ * an operator running cdxgen at scale should do so that a registry contacts
96
+ * them rather than the project.
97
+ *
98
+ * The `@scope/name` npm spelling is deliberately avoided. crates.io blocks
99
+ * user-agents beginning `@CycloneDX/cdxgen` outright, and a leading `@` is
100
+ * unusual enough elsewhere to invite the same treatment.
101
+ *
102
+ * @returns {String} Default user-agent for outbound requests
103
+ */
104
+ export function defaultUserAgent() {
105
+ const email =
106
+ (process.env.CDXGEN_CONTACT_EMAIL || "").trim() || CDXGEN_CONTACT_EMAIL;
107
+ return `cdxgen/${CDXGEN_VERSION} (+${CDXGEN_HOME_URL}; mailto=${email})`;
108
+ }
109
+
110
+ /**
111
+ * The user-agent to send to a host. A `CDXGEN_USER_AGENT_<REGISTRY>` override
112
+ * wins for the hosts of that registry, then the global `CDXGEN_USER_AGENT`,
113
+ * then the default. Blank overrides are ignored: several registries reject an
114
+ * empty header outright, so falling back is safer than honouring it.
115
+ *
116
+ * @param {String} [hostname] Hostname the request is bound for
117
+ * @returns {String} User-agent header value
118
+ */
119
+ export function getUserAgent(hostname) {
120
+ const key = registryKeyForHost(hostname);
121
+ const perRegistry = key
122
+ ? (process.env[`CDXGEN_USER_AGENT_${key}`] || "").trim()
123
+ : "";
124
+ if (perRegistry) {
125
+ return perRegistry;
126
+ }
127
+ const global = (process.env.CDXGEN_USER_AGENT || "").trim();
128
+ return global || defaultUserAgent();
129
+ }
130
+
131
+ /**
132
+ * Whether a user-agent carries a substring that crates.io blocks. Useful for
133
+ * warning an operator whose override would be refused there.
134
+ *
135
+ * @param {String} userAgent User-agent value to test
136
+ * @returns {String|undefined} The offending substring, or undefined
137
+ */
138
+ export function blockedUserAgentSubstring(userAgent) {
139
+ if (!userAgent) {
140
+ return undefined;
141
+ }
142
+ const lowered = userAgent.toLowerCase();
143
+ return BLOCKED_SUBSTRINGS.find((candidate) =>
144
+ lowered.includes(candidate.toLowerCase()),
145
+ );
146
+ }
147
+
148
+ /** Registry keys that `CDXGEN_USER_AGENT_<REGISTRY>` accepts. */
149
+ export const USER_AGENT_REGISTRY_KEYS = Object.freeze(
150
+ Object.keys(REGISTRY_HOSTS),
151
+ );