@cdxgen/cdxgen 13.4.0 → 13.5.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 (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -3,12 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import process from "node:process";
4
4
  import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
5
5
 
6
- import {
7
- Agent,
8
- EnvHttpProxyAgent,
9
- interceptors,
10
- request as undiciRequest,
11
- } from "undici";
6
+ import { Agent, EnvHttpProxyAgent, request as undiciRequest } from "undici";
12
7
 
13
8
  import { registerRunReset } from "./runState.js";
14
9
 
@@ -534,20 +529,13 @@ function cacheKeyFor(method, url, headers = {}, responseType = undefined) {
534
529
  // The maximum number of redirects to follow when `followRedirect` is enabled.
535
530
  const MAX_REDIRECTIONS = 10;
536
531
 
537
- // Bun ships its own bundled implementation of the `undici` module which, as of
538
- // Bun 1.2, does not expose the `Agent.prototype.compose` composition API nor the
539
- // `interceptors.redirect` interceptor that undici v7 uses for redirect handling
540
- // (see cdxgen issue #4245). Detect the Bun runtime so we can fall back to plain
541
- // dispatchers and follow redirects manually instead of crashing at module load.
532
+ // Bun ships its own bundled implementation of the `undici` module, which lacks
533
+ // `EnvHttpProxyAgent` (see cdxgen issue #4245).
542
534
  const isBun = typeof globalThis.Bun !== "undefined";
543
535
 
544
- // Reusable pooled dispatchers. On Node.js, undici composes redirect handling as
545
- // an interceptor (there is no per-request `maxRedirections` option in undici v7),
546
- // so we pre-build the small set of dispatcher variants we need instead of
547
- // allocating a fresh Agent per request (which would defeat connection pooling).
548
- // On Bun, `compose`/`interceptors.redirect` are unavailable, so the redirect
549
- // dispatchers are just the plain agents and redirects are handled in
550
- // `requestWithRedirects` below.
536
+ // Reusable pooled dispatchers, one per TLS verification setting, so requests
537
+ // share connections instead of allocating a fresh Agent each time. Redirects
538
+ // are followed in `requestWithRedirects` below on every runtime.
551
539
  // Whether standard proxy environment variables are set. When they are, requests
552
540
  // must be routed through the proxy. Because this client always passes an
553
541
  // explicit per-request `dispatcher`, it overrides Node's global (potentially
@@ -584,51 +572,72 @@ const defaultAgent = createBaseAgent();
584
572
  const insecureAgent = createBaseAgent({
585
573
  connect: { rejectUnauthorized: false },
586
574
  });
587
- const redirectDispatcher = isBun
588
- ? defaultAgent
589
- : defaultAgent.compose(
590
- interceptors.redirect({ maxRedirections: MAX_REDIRECTIONS }),
591
- );
592
- const insecureRedirectDispatcher = isBun
593
- ? insecureAgent
594
- : insecureAgent.compose(
595
- interceptors.redirect({ maxRedirections: MAX_REDIRECTIONS }),
596
- );
597
575
 
598
576
  /**
599
- * Pick the undici dispatcher matching the request's redirect and TLS
600
- * verification preferences.
577
+ * Pick the undici dispatcher matching the request's TLS verification
578
+ * preference. Redirects are followed by `requestWithRedirects`, not by the
579
+ * dispatcher.
601
580
  *
602
- * @param {boolean} followRedirect Whether HTTP redirects should be followed.
603
581
  * @param {boolean} insecure Whether TLS certificate verification is disabled.
604
582
  * @returns {import("undici").Dispatcher} The dispatcher to use for the request.
605
583
  */
606
- function selectDispatcher(followRedirect, insecure) {
607
- if (insecure) {
608
- return followRedirect ? insecureRedirectDispatcher : insecureAgent;
609
- }
610
- return followRedirect ? redirectDispatcher : defaultAgent;
584
+ function selectDispatcher(insecure) {
585
+ return insecure ? insecureAgent : defaultAgent;
611
586
  }
612
587
 
613
588
  // HTTP status codes that represent a redirect with a `location` header.
614
589
  const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
615
590
 
591
+ // Request headers that may follow a redirect to a different origin. Everything
592
+ // else, such as Authorization, cookies and registry API keys like X-Api-Key or
593
+ // PRIVATE-TOKEN, stays with the origin the caller addressed.
594
+ const CROSS_ORIGIN_REDIRECT_HEADERS = new Set([
595
+ "accept",
596
+ "accept-encoding",
597
+ "accept-language",
598
+ "user-agent",
599
+ ]);
600
+
616
601
  /**
617
- * Issue an undici request, following redirects manually when running under Bun.
602
+ * Keep only the request headers that may be sent to a different origin.
618
603
  *
619
- * On Node.js the redirect-following interceptor baked into the dispatcher does
620
- * the work, so this simply forwards to `undiciRequest`. On Bun (where the
621
- * interceptor API is unavailable) we loop on 3xx responses, draining each
622
- * intermediate body and resolving relative `location` headers, up to
604
+ * @param {Object} headers Request headers.
605
+ * @returns {Object} Headers safe to send cross-origin.
606
+ */
607
+ function crossOriginRedirectHeaders(headers = {}) {
608
+ const kept = {};
609
+ for (const [name, value] of Object.entries(headers)) {
610
+ if (CROSS_ORIGIN_REDIRECT_HEADERS.has(name.toLowerCase())) {
611
+ kept[name] = value;
612
+ }
613
+ }
614
+ return kept;
615
+ }
616
+
617
+ /**
618
+ * Issue an undici request and follow redirects one hop at a time.
619
+ *
620
+ * Redirects are followed here rather than by undici's redirect interceptor so
621
+ * that every hop goes through `beforeRedirect`, which runs the same host
622
+ * allowlist, protocol and dry-run checks as the first request, and so that
623
+ * credentials are not forwarded to another origin. Each intermediate body is
624
+ * drained and relative `location` headers are resolved, up to
623
625
  * `MAX_REDIRECTIONS` hops.
624
626
  *
625
627
  * @param {URL} url Request URL.
626
628
  * @param {Object} requestOptions Options passed to `undiciRequest`.
627
629
  * @param {boolean} followRedirect Whether redirects should be followed.
630
+ * @param {function(URL): Promise<void>} beforeRedirect Called with the next URL
631
+ * before each hop; throws to stop the request.
628
632
  * @returns {Promise<Object>} The final undici response.
629
633
  */
630
- async function requestWithRedirects(url, requestOptions, followRedirect) {
631
- if (!isBun || !followRedirect) {
634
+ async function requestWithRedirects(
635
+ url,
636
+ requestOptions,
637
+ followRedirect,
638
+ beforeRedirect,
639
+ ) {
640
+ if (!followRedirect) {
632
641
  return await undiciRequest(url, requestOptions);
633
642
  }
634
643
  let currentUrl = url;
@@ -649,7 +658,14 @@ async function requestWithRedirects(url, requestOptions, followRedirect) {
649
658
  } catch {
650
659
  // Ignore drain failures; the socket will be discarded.
651
660
  }
652
- currentUrl = new URL(location, currentUrl);
661
+ const nextUrl = new URL(location, currentUrl);
662
+ await beforeRedirect(nextUrl);
663
+ if (nextUrl.origin !== currentUrl.origin) {
664
+ requestOptions.headers = crossOriginRedirectHeaders(
665
+ requestOptions.headers,
666
+ );
667
+ }
668
+ currentUrl = nextUrl;
653
669
  redirections += 1;
654
670
  // 303, and 301/302 for anything other than GET/HEAD, must switch to GET
655
671
  // without a body, matching how undici's redirect interceptor behaves.
@@ -738,9 +754,21 @@ async function doRequest(mergedOptions) {
738
754
  method,
739
755
  headers,
740
756
  body: method === "GET" || method === "HEAD" ? undefined : body,
741
- dispatcher: selectDispatcher(followRedirect, insecure),
757
+ dispatcher: selectDispatcher(insecure),
742
758
  signal,
743
759
  };
760
+ // A redirect target must pass the same beforeRequest checks, such as the
761
+ // host allowlist, as the URL the caller asked for.
762
+ const beforeRedirect = async (nextUrl) => {
763
+ const hopOptions = {
764
+ ...options,
765
+ url: nextUrl,
766
+ context: { ...options.context },
767
+ };
768
+ for (const hook of options.hooks?.beforeRequest || []) {
769
+ await hook(hopOptions);
770
+ }
771
+ };
744
772
 
745
773
  let undiciResponse;
746
774
  try {
@@ -748,6 +776,7 @@ async function doRequest(mergedOptions) {
748
776
  url,
749
777
  requestOptions,
750
778
  followRedirect,
779
+ beforeRedirect,
751
780
  );
752
781
  } catch (err) {
753
782
  const requestError = new RequestError(err, options);
@@ -45,6 +45,7 @@ const ANNOUNCED_OPTIONS = new Set([
45
45
  "api-key",
46
46
  "include-formulation",
47
47
  "install-deps",
48
+ "introspect",
48
49
  "deep",
49
50
  "tea-publish",
50
51
  "tea-fetch",
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Policy for running the scanned project's own build tooling.
3
+ *
4
+ * Build tools such as Maven, Gradle, Poetry, MSBuild, SwiftPM, Leiningen and
5
+ * CocoaPods read configuration or run code that ships with the project
6
+ * (`.mvn/jvm.config`, `.poetry/plugins`, `Directory.Build.rsp`, `Package.swift`,
7
+ * `project.clj`, podspecs). Running them on an untrusted project runs that
8
+ * project's code, so they belong to the same surface as dependency
9
+ * installation and are skipped whenever installation is disabled.
10
+ */
11
+ import { isSecureMode, readEnvironmentVariable } from "./activity.js";
12
+ import { recordDegradation } from "./buildLedger.js";
13
+
14
+ /**
15
+ * Whether secure mode is on right now. `isSecureMode` is read once when the
16
+ * modules load, but the `cdxgen-secure` command turns secure mode on later,
17
+ * by setting CDXGEN_SECURE_MODE before the scan starts.
18
+ *
19
+ * @returns {boolean} true when secure mode is on
20
+ */
21
+ export function isSecureModeActive() {
22
+ return (
23
+ isSecureMode ||
24
+ ["true", "1"].includes(readEnvironmentVariable("CDXGEN_SECURE_MODE"))
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Whether the scan may run the scanned project's own build tooling, such as
30
+ * wrapper scripts or build definitions that Maven, Gradle, sbt, Mill, Poetry,
31
+ * PDM, MSBuild, SwiftPM, Leiningen and CocoaPods evaluate.
32
+ *
33
+ * Tooling follows `options.installDeps`, which defaults to off in secure mode
34
+ * when a caller does not set it. Introspection mode exists to observe the
35
+ * project's own toolchain, so it keeps that tooling on even when dependency
36
+ * installation is disabled, as long as the operator asked for it rather than
37
+ * a config file in the scanned project. Secure mode still wins:
38
+ * CDXGEN_INTROSPECT alone must not make a secure-mode scan run the scanned
39
+ * project's code.
40
+ *
41
+ * @param {Object} [options] CLI options
42
+ * @returns {boolean} true when project tooling may run
43
+ */
44
+ export function isProjectToolingAllowed(options) {
45
+ const secureMode = isSecureModeActive();
46
+ const installDeps = options?.installDeps ?? !secureMode;
47
+ if (installDeps !== false) {
48
+ return true;
49
+ }
50
+ return (
51
+ options?.introspect === true &&
52
+ options?.introspectFromConfigFile !== true &&
53
+ !secureMode
54
+ );
55
+ }
56
+
57
+ // The skips already reported for each scan, keyed by the scan's options.
58
+ // Callers without options share one key.
59
+ const reportedSkips = new WeakMap();
60
+ const noOptionsKey = {};
61
+
62
+ /**
63
+ * Tell the user that a tool was skipped because project tooling is not
64
+ * allowed, and record the matching degradation for the fidelity report.
65
+ * Each skip is reported once per scan, however many manifests it applies to.
66
+ * A skip that only costs evidence, not BOM content, passes no remediation id
67
+ * and is only shown.
68
+ *
69
+ * @param {Object} options CLI options of the scan
70
+ * @param {string|null} remediationId Remediation id from
71
+ * data/remediations.json, or null for a message only
72
+ * @param {string} message Console message shown to the user
73
+ * @param {Object} [fields] Degradation fields (ecosystem, tool, impact, detail)
74
+ * @returns {void}
75
+ */
76
+ export function reportProjectToolingSkipped(
77
+ options,
78
+ remediationId,
79
+ message,
80
+ fields,
81
+ ) {
82
+ const key = options && typeof options === "object" ? options : noOptionsKey;
83
+ let reported = reportedSkips.get(key);
84
+ if (!reported) {
85
+ reported = new Set();
86
+ reportedSkips.set(key, reported);
87
+ }
88
+ const skipKey = remediationId || message;
89
+ if (reported.has(skipKey)) {
90
+ return;
91
+ }
92
+ reported.add(skipKey);
93
+ console.log(message);
94
+ if (remediationId) {
95
+ recordDegradation(remediationId, fields);
96
+ }
97
+ }
@@ -26,6 +26,15 @@ const JSON_PROPERTY_NAMES = new Set([
26
26
  ]);
27
27
  const URL_PATTERN = /https?:\/\/[^\s<>"'),\]}]+/giu;
28
28
  const MAX_STRUCTURE_DEPTH = 6;
29
+ // What an RFC 3986 path may hold as it is: unreserved characters, sub-delims,
30
+ // ":", "@", "/" and well-formed percent-escapes. A query or fragment may also
31
+ // hold "?". Everything else is percent-encoded.
32
+ const URI_PATH_UNSAFE =
33
+ /%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/%]/gu;
34
+ const URI_QUERY_UNSAFE =
35
+ /%(?![0-9A-Fa-f]{2})|[^A-Za-z0-9\-._~!$&'()*+,;=:@/?%]/gu;
36
+ const URI_SCHEME_AUTHORITY = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*/u;
37
+ const UTF8_ENCODER = new TextEncoder();
29
38
 
30
39
  function sanitizeUrlForBom(value) {
31
40
  const input = String(value || "").trim();
@@ -146,6 +155,69 @@ export function sanitizeBomUrl(value) {
146
155
  return sanitizeUrlForBom(value);
147
156
  }
148
157
 
158
+ function percentEncodeUnsafe(text, unsafe) {
159
+ return text.replace(unsafe, (character) =>
160
+ Array.from(
161
+ UTF8_ENCODER.encode(character),
162
+ (byte) => `%${byte.toString(16).toUpperCase().padStart(2, "0")}`,
163
+ ).join(""),
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Percent-encode what an RFC 3986 URI reference cannot hold as it is, so a
169
+ * route template or URL can be emitted where CycloneDX expects an
170
+ * iri-reference, such as `services[].endpoints[]`. The braces of a route
171
+ * template, spaces, `<>`, `[]` outside an IPv6 host, `|`, `^`, non-ASCII
172
+ * characters and a `%` that starts no escape are encoded:
173
+ * `/users/{id}` becomes `/users/%7Bid%7D`. The scheme and authority of an
174
+ * absolute URL are kept as written, the query and fragment stay a query and
175
+ * fragment, and existing escapes are kept, so encoding twice changes
176
+ * nothing. Nothing is removed; strip credentials with {@link sanitizeBomUrl}
177
+ * first.
178
+ *
179
+ * @param {unknown} value Route template, path or URL
180
+ * @returns {string} The encoded reference
181
+ */
182
+ export function encodeUriReference(value) {
183
+ const text = String(value ?? "");
184
+ const authority = URI_SCHEME_AUTHORITY.exec(text)?.[0] ?? "";
185
+ const rest = text.slice(authority.length);
186
+ const queryStart = rest.indexOf("?");
187
+ const fragmentStart = rest.indexOf("#");
188
+ const pathEnd = Math.min(
189
+ queryStart < 0 ? rest.length : queryStart,
190
+ fragmentStart < 0 ? rest.length : fragmentStart,
191
+ );
192
+ let encoded =
193
+ authority + percentEncodeUnsafe(rest.slice(0, pathEnd), URI_PATH_UNSAFE);
194
+ if (pathEnd === queryStart) {
195
+ const queryEnd = fragmentStart > queryStart ? fragmentStart : rest.length;
196
+ encoded += `?${percentEncodeUnsafe(rest.slice(queryStart + 1, queryEnd), URI_QUERY_UNSAFE)}`;
197
+ }
198
+ if (fragmentStart >= 0) {
199
+ encoded += `#${percentEncodeUnsafe(rest.slice(fragmentStart + 1), URI_QUERY_UNSAFE)}`;
200
+ }
201
+ return encoded;
202
+ }
203
+
204
+ /**
205
+ * Whether a value carries a token shape the BOM sanitizer redacts: cloud
206
+ * access keys, bearer tokens, GitHub, Slack and npm tokens, JWTs and the
207
+ * like. Callers that cannot splice a `[redacted]` marker into the value, such
208
+ * as a URL path segment, use it to drop the value instead.
209
+ *
210
+ * @param {unknown} value Value to check
211
+ * @returns {boolean} true when the value holds an inline credential
212
+ */
213
+ export function hasInlineCredential(value) {
214
+ const text = String(value ?? "");
215
+ return INLINE_CREDENTIAL_PATTERNS.some((pattern) => {
216
+ pattern.lastIndex = 0;
217
+ return pattern.test(text);
218
+ });
219
+ }
220
+
149
221
  /**
150
222
  * Sanitize a property value before serializing it into BOM properties.
151
223
  *