@cyclonedx/cdxgen 12.1.5 → 12.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 (181) hide show
  1. package/README.md +47 -39
  2. package/bin/cdxgen.js +175 -96
  3. package/bin/evinse.js +4 -4
  4. package/bin/repl.js +1 -1
  5. package/bin/sign.js +102 -0
  6. package/bin/validate.js +233 -0
  7. package/bin/verify.js +69 -28
  8. package/data/queries.json +1 -1
  9. package/data/rules/ci-permissions.yaml +186 -0
  10. package/data/rules/dependency-sources.yaml +123 -0
  11. package/data/rules/package-integrity.yaml +135 -0
  12. package/data/rules/vscode-extensions.yaml +228 -0
  13. package/lib/cli/index.js +327 -372
  14. package/lib/evinser/db.js +137 -0
  15. package/lib/{helpers → evinser}/db.poku.js +2 -6
  16. package/lib/evinser/evinser.js +2 -14
  17. package/lib/helpers/bomSigner.js +312 -0
  18. package/lib/helpers/bomSigner.poku.js +156 -0
  19. package/lib/helpers/ciParsers/azurePipelines.js +295 -0
  20. package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
  21. package/lib/helpers/ciParsers/circleCi.js +286 -0
  22. package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
  23. package/lib/helpers/ciParsers/common.js +24 -0
  24. package/lib/helpers/ciParsers/githubActions.js +636 -0
  25. package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
  26. package/lib/helpers/ciParsers/gitlabCi.js +213 -0
  27. package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
  28. package/lib/helpers/ciParsers/jenkins.js +181 -0
  29. package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
  30. package/lib/helpers/depsUtils.js +203 -0
  31. package/lib/helpers/depsUtils.poku.js +150 -0
  32. package/lib/helpers/display.js +423 -4
  33. package/lib/helpers/envcontext.js +18 -3
  34. package/lib/helpers/formulationParsers.js +351 -0
  35. package/lib/helpers/logger.js +14 -0
  36. package/lib/helpers/protobom.js +9 -9
  37. package/lib/helpers/pythonutils.js +9 -0
  38. package/lib/helpers/utils.js +681 -406
  39. package/lib/helpers/utils.poku.js +55 -255
  40. package/lib/helpers/versutils.js +202 -0
  41. package/lib/helpers/versutils.poku.js +315 -0
  42. package/lib/helpers/vsixutils.js +1061 -0
  43. package/lib/helpers/vsixutils.poku.js +2247 -0
  44. package/lib/managers/binary.js +19 -19
  45. package/lib/managers/docker.js +108 -1
  46. package/lib/managers/oci.js +10 -0
  47. package/lib/managers/piptree.js +3 -9
  48. package/lib/parsers/npmrc.js +17 -13
  49. package/lib/parsers/npmrc.poku.js +41 -5
  50. package/lib/server/openapi.yaml +1 -1
  51. package/lib/server/server.js +40 -11
  52. package/lib/server/server.poku.js +123 -144
  53. package/lib/stages/postgen/annotator.js +1 -1
  54. package/lib/stages/postgen/auditBom.js +197 -0
  55. package/lib/stages/postgen/auditBom.poku.js +378 -0
  56. package/lib/stages/postgen/postgen.js +54 -1
  57. package/lib/stages/postgen/postgen.poku.js +90 -1
  58. package/lib/stages/postgen/ruleEngine.js +369 -0
  59. package/lib/stages/pregen/envAudit.js +299 -0
  60. package/lib/stages/pregen/envAudit.poku.js +572 -0
  61. package/lib/stages/pregen/pregen.js +12 -8
  62. package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
  63. package/lib/validator/complianceEngine.js +241 -0
  64. package/lib/validator/complianceEngine.poku.js +168 -0
  65. package/lib/validator/complianceRules.js +1610 -0
  66. package/lib/validator/complianceRules.poku.js +328 -0
  67. package/lib/validator/index.js +222 -0
  68. package/lib/validator/index.poku.js +144 -0
  69. package/lib/validator/reporters/annotations.js +121 -0
  70. package/lib/validator/reporters/console.js +149 -0
  71. package/lib/validator/reporters/index.js +41 -0
  72. package/lib/validator/reporters/json.js +37 -0
  73. package/lib/validator/reporters/sarif.js +184 -0
  74. package/lib/validator/reporters.poku.js +150 -0
  75. package/package.json +8 -8
  76. package/types/bin/sign.d.ts +3 -0
  77. package/types/bin/sign.d.ts.map +1 -0
  78. package/types/bin/validate.d.ts +3 -0
  79. package/types/bin/validate.d.ts.map +1 -0
  80. package/types/helpers/utils.d.ts +0 -1
  81. package/types/lib/cli/index.d.ts +49 -52
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/evinser/db.d.ts +34 -0
  84. package/types/lib/evinser/db.d.ts.map +1 -0
  85. package/types/lib/evinser/evinser.d.ts +63 -16
  86. package/types/lib/evinser/evinser.d.ts.map +1 -1
  87. package/types/lib/helpers/bomSigner.d.ts +27 -0
  88. package/types/lib/helpers/bomSigner.d.ts.map +1 -0
  89. package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
  90. package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
  91. package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
  92. package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
  93. package/types/lib/helpers/ciParsers/common.d.ts +11 -0
  94. package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
  95. package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
  96. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
  97. package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
  98. package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
  99. package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
  100. package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
  101. package/types/lib/helpers/depsUtils.d.ts +21 -0
  102. package/types/lib/helpers/depsUtils.d.ts.map +1 -0
  103. package/types/lib/helpers/display.d.ts +111 -11
  104. package/types/lib/helpers/display.d.ts.map +1 -1
  105. package/types/lib/helpers/envcontext.d.ts +19 -7
  106. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  107. package/types/lib/helpers/formulationParsers.d.ts +50 -0
  108. package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
  109. package/types/lib/helpers/logger.d.ts +15 -1
  110. package/types/lib/helpers/logger.d.ts.map +1 -1
  111. package/types/lib/helpers/protobom.d.ts +2 -2
  112. package/types/lib/helpers/pythonutils.d.ts +10 -1
  113. package/types/lib/helpers/pythonutils.d.ts.map +1 -1
  114. package/types/lib/helpers/utils.d.ts +532 -128
  115. package/types/lib/helpers/utils.d.ts.map +1 -1
  116. package/types/lib/helpers/versutils.d.ts +8 -0
  117. package/types/lib/helpers/versutils.d.ts.map +1 -0
  118. package/types/lib/helpers/vsixutils.d.ts +130 -0
  119. package/types/lib/helpers/vsixutils.d.ts.map +1 -0
  120. package/types/lib/managers/docker.d.ts +12 -31
  121. package/types/lib/managers/docker.d.ts.map +1 -1
  122. package/types/lib/managers/oci.d.ts +11 -1
  123. package/types/lib/managers/oci.d.ts.map +1 -1
  124. package/types/lib/managers/piptree.d.ts.map +1 -1
  125. package/types/lib/parsers/npmrc.d.ts +4 -1
  126. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  127. package/types/lib/server/server.d.ts +21 -2
  128. package/types/lib/server/server.d.ts.map +1 -1
  129. package/types/lib/stages/postgen/auditBom.d.ts +20 -0
  130. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
  131. package/types/lib/stages/postgen/postgen.d.ts +8 -1
  132. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  133. package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
  134. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
  135. package/types/lib/stages/pregen/envAudit.d.ts +8 -0
  136. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
  137. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  138. package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
  139. package/types/lib/validator/bomValidator.d.ts.map +1 -0
  140. package/types/lib/validator/complianceEngine.d.ts +66 -0
  141. package/types/lib/validator/complianceEngine.d.ts.map +1 -0
  142. package/types/lib/validator/complianceRules.d.ts +70 -0
  143. package/types/lib/validator/complianceRules.d.ts.map +1 -0
  144. package/types/lib/validator/index.d.ts +70 -0
  145. package/types/lib/validator/index.d.ts.map +1 -0
  146. package/types/lib/validator/reporters/annotations.d.ts +31 -0
  147. package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
  148. package/types/lib/validator/reporters/console.d.ts +30 -0
  149. package/types/lib/validator/reporters/console.d.ts.map +1 -0
  150. package/types/lib/validator/reporters/index.d.ts +21 -0
  151. package/types/lib/validator/reporters/index.d.ts.map +1 -0
  152. package/types/lib/validator/reporters/json.d.ts +11 -0
  153. package/types/lib/validator/reporters/json.d.ts.map +1 -0
  154. package/types/lib/validator/reporters/sarif.d.ts +16 -0
  155. package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
  156. package/lib/helpers/db.js +0 -162
  157. package/lib/stages/pregen/env-audit.js +0 -34
  158. package/lib/stages/pregen/env-audit.poku.js +0 -290
  159. package/types/helpers/db.d.ts +0 -35
  160. package/types/helpers/db.d.ts.map +0 -1
  161. package/types/lib/helpers/db.d.ts +0 -35
  162. package/types/lib/helpers/db.d.ts.map +0 -1
  163. package/types/lib/helpers/validator.d.ts.map +0 -1
  164. package/types/lib/stages/pregen/env-audit.d.ts +0 -2
  165. package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
  166. package/types/managers/binary.d.ts +0 -37
  167. package/types/managers/binary.d.ts.map +0 -1
  168. package/types/managers/docker.d.ts +0 -56
  169. package/types/managers/docker.d.ts.map +0 -1
  170. package/types/managers/oci.d.ts +0 -2
  171. package/types/managers/oci.d.ts.map +0 -1
  172. package/types/managers/piptree.d.ts +0 -2
  173. package/types/managers/piptree.d.ts.map +0 -1
  174. package/types/server/server.d.ts +0 -34
  175. package/types/server/server.d.ts.map +0 -1
  176. package/types/stages/postgen/annotator.d.ts +0 -27
  177. package/types/stages/postgen/annotator.d.ts.map +0 -1
  178. package/types/stages/postgen/postgen.d.ts +0 -51
  179. package/types/stages/postgen/postgen.d.ts.map +0 -1
  180. package/types/stages/pregen/pregen.d.ts +0 -59
  181. package/types/stages/pregen/pregen.d.ts.map +0 -1
@@ -1,5 +1,4 @@
1
1
  import {
2
- existsSync,
3
2
  lstatSync,
4
3
  mkdtempSync,
5
4
  readFileSync,
@@ -31,6 +30,7 @@ import {
31
30
  isSpdxLicenseExpression,
32
31
  multiChecksumFile,
33
32
  retrieveCdxgenPluginVersion,
33
+ safeExistsSync,
34
34
  safeMkdirSync,
35
35
  safeSpawnSync,
36
36
  } from "../helpers/utils.js";
@@ -82,8 +82,8 @@ let extraNMBinPath;
82
82
  // Is there a non-empty local plugins directory
83
83
  if (
84
84
  !CDXGEN_PLUGINS_DIR &&
85
- existsSync(join(dirName, "plugins")) &&
86
- existsSync(join(dirName, "plugins", "trivy"))
85
+ safeExistsSync(join(dirName, "plugins")) &&
86
+ safeExistsSync(join(dirName, "plugins", "trivy"))
87
87
  ) {
88
88
  CDXGEN_PLUGINS_DIR = join(dirName, "plugins");
89
89
  }
@@ -91,7 +91,7 @@ if (
91
91
  // Is there a non-empty local node_modules directory
92
92
  if (
93
93
  !CDXGEN_PLUGINS_DIR &&
94
- existsSync(
94
+ safeExistsSync(
95
95
  join(
96
96
  dirName,
97
97
  "node_modules",
@@ -100,7 +100,7 @@ if (
100
100
  "plugins",
101
101
  ),
102
102
  ) &&
103
- existsSync(
103
+ safeExistsSync(
104
104
  join(
105
105
  dirName,
106
106
  "node_modules",
@@ -118,7 +118,7 @@ if (
118
118
  `cdxgen-plugins-bin${pluginsBinSuffix}`,
119
119
  "plugins",
120
120
  );
121
- if (existsSync(join(dirName, "node_modules", ".bin"))) {
121
+ if (safeExistsSync(join(dirName, "node_modules", ".bin"))) {
122
122
  extraNMBinPath = join(dirName, "node_modules", ".bin");
123
123
  }
124
124
  }
@@ -168,7 +168,7 @@ if (!CDXGEN_PLUGINS_DIR) {
168
168
  `cdxgen-plugins-bin${pluginsBinSuffix}`,
169
169
  "plugins",
170
170
  );
171
- if (existsSync(join(tmpA[0], "node_modules", ".bin"))) {
171
+ if (safeExistsSync(join(tmpA[0], "node_modules", ".bin"))) {
172
172
  extraNMBinPath = join(tmpA[0], "node_modules", ".bin");
173
173
  }
174
174
  } else if (dirName.includes(join(".pnpm", "@cyclonedx+cdxgen"))) {
@@ -183,7 +183,7 @@ if (!CDXGEN_PLUGINS_DIR) {
183
183
  `cdxgen-plugins-bin${pluginsBinSuffix}`,
184
184
  "plugins",
185
185
  );
186
- if (existsSync(join(tmpA[0], ".bin"))) {
186
+ if (safeExistsSync(join(tmpA[0], ".bin"))) {
187
187
  extraNMBinPath = join(tmpA[0], ".bin");
188
188
  }
189
189
  } else if (dirName.includes(join("caxa", "applications"))) {
@@ -201,12 +201,12 @@ if (!CDXGEN_PLUGINS_DIR) {
201
201
  extraNMBinPath = join(dirName, "node_modules", ".bin");
202
202
  }
203
203
  // Set the plugins directory
204
- if (globalPlugins && existsSync(globalPlugins)) {
204
+ if (globalPlugins && safeExistsSync(globalPlugins)) {
205
205
  CDXGEN_PLUGINS_DIR = globalPlugins;
206
206
  if (DEBUG_MODE) {
207
207
  console.log("Found global plugins", CDXGEN_PLUGINS_DIR);
208
208
  }
209
- } else if (altGlobalPlugins && existsSync(altGlobalPlugins)) {
209
+ } else if (altGlobalPlugins && safeExistsSync(altGlobalPlugins)) {
210
210
  CDXGEN_PLUGINS_DIR = altGlobalPlugins;
211
211
  // To help detect bin commands such as atom, astgen, etc, we need to set this to the PATH variable.
212
212
  if (DEBUG_MODE) {
@@ -227,7 +227,7 @@ if (!CDXGEN_PLUGINS_DIR) {
227
227
  CDXGEN_PLUGINS_DIR = "";
228
228
  }
229
229
  let TRIVY_BIN = process.env.TRIVY_CMD;
230
- if (existsSync(join(CDXGEN_PLUGINS_DIR, "trivy"))) {
230
+ if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "trivy"))) {
231
231
  TRIVY_BIN = join(
232
232
  CDXGEN_PLUGINS_DIR,
233
233
  "trivy",
@@ -235,7 +235,7 @@ if (existsSync(join(CDXGEN_PLUGINS_DIR, "trivy"))) {
235
235
  );
236
236
  }
237
237
  let CARGO_AUDITABLE_BIN = process.env.CARGO_AUDITABLE_CMD;
238
- if (existsSync(join(CDXGEN_PLUGINS_DIR, "cargo-auditable"))) {
238
+ if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "cargo-auditable"))) {
239
239
  CARGO_AUDITABLE_BIN = join(
240
240
  CDXGEN_PLUGINS_DIR,
241
241
  "cargo-auditable",
@@ -243,7 +243,7 @@ if (existsSync(join(CDXGEN_PLUGINS_DIR, "cargo-auditable"))) {
243
243
  );
244
244
  }
245
245
  let OSQUERY_BIN = process.env.OSQUERY_CMD;
246
- if (existsSync(join(CDXGEN_PLUGINS_DIR, "osquery"))) {
246
+ if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "osquery"))) {
247
247
  OSQUERY_BIN = join(
248
248
  CDXGEN_PLUGINS_DIR,
249
249
  "osquery",
@@ -255,7 +255,7 @@ if (existsSync(join(CDXGEN_PLUGINS_DIR, "osquery"))) {
255
255
  }
256
256
  }
257
257
  let DOSAI_BIN = process.env.DOSAI_CMD;
258
- if (existsSync(join(CDXGEN_PLUGINS_DIR, "dosai"))) {
258
+ if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "dosai"))) {
259
259
  DOSAI_BIN = join(
260
260
  CDXGEN_PLUGINS_DIR,
261
261
  "dosai",
@@ -268,7 +268,7 @@ const BLINT_BIN = process.env.BLINT_CMD || "blint";
268
268
 
269
269
  // sourcekitten
270
270
  let SOURCEKITTEN_BIN = process.env.SOURCEKITTEN_CMD;
271
- if (existsSync(join(CDXGEN_PLUGINS_DIR, "sourcekitten"))) {
271
+ if (safeExistsSync(join(CDXGEN_PLUGINS_DIR, "sourcekitten"))) {
272
272
  SOURCEKITTEN_BIN = join(CDXGEN_PLUGINS_DIR, "sourcekitten", "sourcekitten");
273
273
  }
274
274
 
@@ -476,7 +476,7 @@ export async function getOSPackages(src, imageConfig) {
476
476
  } catch (_err) {
477
477
  // ignore errors
478
478
  }
479
- if (existsSync(src)) {
479
+ if (safeExistsSync(src)) {
480
480
  imageType = "rootfs";
481
481
  }
482
482
  const tempDir = mkdtempSync(join(getTmpDir(), "trivy-cdxgen-"));
@@ -513,7 +513,7 @@ export async function getOSPackages(src, imageConfig) {
513
513
  console.error(result.stdout, result.stderr);
514
514
  }
515
515
  }
516
- if (existsSync(bomJsonFile)) {
516
+ if (safeExistsSync(bomJsonFile)) {
517
517
  let tmpBom = {};
518
518
  try {
519
519
  tmpBom = JSON.parse(
@@ -536,9 +536,9 @@ export async function getOSPackages(src, imageConfig) {
536
536
  const osReleaseData = {};
537
537
  let osReleaseFile;
538
538
  // Let's try to read the os-release file from various locations
539
- if (existsSync(join(src, "etc", "os-release"))) {
539
+ if (safeExistsSync(join(src, "etc", "os-release"))) {
540
540
  osReleaseFile = join(src, "etc", "os-release");
541
- } else if (existsSync(join(src, "usr", "lib", "os-release"))) {
541
+ } else if (safeExistsSync(join(src, "usr", "lib", "os-release"))) {
542
542
  osReleaseFile = join(src, "usr", "lib", "os-release");
543
543
  }
544
544
  if (osReleaseFile) {
@@ -71,7 +71,14 @@ if (
71
71
  isContainerd = true;
72
72
  }
73
73
 
74
- // Taken from https://github.com/isaacs/node-tar/blob/main/src/strip-absolute-path.ts
74
+ /**
75
+ * Strip absolute path prefixes from a path string, handling both Unix and
76
+ * Windows paths (including UNC and extended-length paths such as //?/C:/).
77
+ * Taken from https://github.com/isaacs/node-tar/blob/main/src/strip-absolute-path.ts
78
+ *
79
+ * @param {string} path The path to strip
80
+ * @returns {string} The path with its absolute root removed
81
+ */
75
82
  export const stripAbsolutePath = (path) => {
76
83
  // This appears to be a most frequent case, so let's return quickly.
77
84
  if (path === "/") {
@@ -165,6 +172,18 @@ export function detectRancherDesktop() {
165
172
  // Cache the registry auth keys
166
173
  const registry_auth_keys = {};
167
174
  const REQUEST_TIMEOUT_SECS = 60000;
175
+ /**
176
+ * Build a `got` options object for Docker / registry API requests. Resolves
177
+ * authentication headers by consulting (in order) the DOCKER_AUTH_CONFIG
178
+ * environment variable, DOCKER_USER/DOCKER_PASSWORD/DOCKER_EMAIL environment
179
+ * variables, hardcoded tokens in ~/.docker/config.json, credential helpers
180
+ * listed in credHelpers/credsStore, and finally TLS certificate files pointed
181
+ * to by DOCKER_CERT_PATH.
182
+ *
183
+ * @param {string} [forRegistry] Registry hostname (e.g. "registry-1.docker.io").
184
+ * Defaults to DOCKER_SERVER_ADDRESS env var or "docker.io".
185
+ * @returns {Object} Options object suitable for passing to `got`
186
+ */
168
187
  const getDefaultOptions = (forRegistry) => {
169
188
  let authTokenSet = false;
170
189
  if (!forRegistry) {
@@ -358,6 +377,20 @@ const getDefaultOptions = (forRegistry) => {
358
377
  return opts;
359
378
  };
360
379
 
380
+ /**
381
+ * Establish (or reuse) a `got` client connected to the local Docker or Podman
382
+ * daemon. Tries multiple socket / URL candidates in order: the default Docker
383
+ * socket, the rootless Docker socket, the Windows TCP endpoint, the rootless
384
+ * Podman socket, and the root Podman socket. Sets the module-level flags
385
+ * `isPodman`, `isPodmanRootless`, `isDockerRootless`, and `isWinLocalTLS` as a
386
+ * side-effect. Returns `undefined` when containerd / nerdctl is in use or no
387
+ * daemon could be reached.
388
+ *
389
+ * @param {Object} options Additional `got` options to merge into the connection
390
+ * @param {string} [forRegistry] Registry hostname forwarded to `getDefaultOptions`
391
+ * @returns {Promise<import("got").Got|undefined>} A `got` instance bound to the
392
+ * daemon base URL, or `undefined`
393
+ */
361
394
  export const getConnection = async (options, forRegistry) => {
362
395
  if (isContainerd || isNerdctl) {
363
396
  return undefined;
@@ -461,6 +494,17 @@ export const getConnection = async (options, forRegistry) => {
461
494
  return dockerConn;
462
495
  };
463
496
 
497
+ /**
498
+ * Send a single HTTP request to the Docker / Podman daemon via the `got`
499
+ * client returned by {@link getConnection}. GET requests are parsed as JSON;
500
+ * all other methods receive a Buffer response body.
501
+ *
502
+ * @param {string} path API path relative to the daemon base URL (e.g. "images/ubuntu:latest/json")
503
+ * @param {string} method HTTP method (e.g. "GET", "POST", "DELETE")
504
+ * @param {string} [forRegistry] Registry hostname forwarded to `getDefaultOptions` for auth headers
505
+ * @returns {Promise<Object|Buffer|undefined>} Parsed JSON object for GET
506
+ * requests, raw Buffer for other methods, or `undefined` if no client is available
507
+ */
464
508
  export const makeRequest = async (path, method, forRegistry) => {
465
509
  const client = await getConnection({}, forRegistry);
466
510
  if (!client) {
@@ -815,6 +859,14 @@ function tarFilter(path, entry) {
815
859
  );
816
860
  }
817
861
 
862
+ /**
863
+ * Suppress low-signal tar warnings (TAR_ENTRY_INFO, TAR_LONGLINK) that are
864
+ * expected when extracting container image layers. All other warning codes are
865
+ * logged when DEBUG_MODE is enabled.
866
+ *
867
+ * @param {string} code Tar warning code (e.g. "TAR_ENTRY_INFO")
868
+ * @param {string} message Human-readable warning message
869
+ */
818
870
  function handleTarWarning(code, message) {
819
871
  if (code === "TAR_ENTRY_INFO" || code === "TAR_LONGLINK") {
820
872
  return;
@@ -885,6 +937,18 @@ const EXTRACT_EXCLUDE_TYPES = new Set([
885
937
  "Link",
886
938
  ]);
887
939
 
940
+ /**
941
+ * Extract a container image tar archive into a destination directory.
942
+ * Applies path sanitisation, ownership/permission preservation settings, and
943
+ * an entry filter to skip problematic files and device nodes. Handles common
944
+ * tar errors gracefully, logging only unexpected ones.
945
+ *
946
+ * @param {string} fullImageName Path to the source tar archive
947
+ * @param {string} dir Destination directory to extract into
948
+ * @param {Object} options CLI options (uses `options.failOnError`)
949
+ * @returns {Promise<boolean>} `true` on success, `false` when the archive is
950
+ * empty or a non-fatal error was encountered
951
+ */
888
952
  export const extractTar = async (fullImageName, dir, options) => {
889
953
  try {
890
954
  await stream.pipeline(
@@ -1016,6 +1080,21 @@ export const exportArchive = async (fullImageName, options = {}) => {
1016
1080
  return undefined;
1017
1081
  };
1018
1082
 
1083
+ /**
1084
+ * Parse a Docker/containerd manifest file and extract all image layers into a
1085
+ * single merged directory. Resolves the last layer's config to determine the
1086
+ * container's working directory, and builds the package path list for
1087
+ * subsequent analysis.
1088
+ *
1089
+ * @param {string} manifestFile Path to the manifest.json (or index.json) file
1090
+ * @param {Object} localData Local image inspect data (e.g. from `docker inspect`)
1091
+ * @param {string} tempDir Temporary directory that holds the unpacked image
1092
+ * @param {string} allLayersExplodedDir Directory where all layers are merged
1093
+ * @param {Object} options CLI options (uses `options.failOnError`)
1094
+ * @returns {Promise<Object>} Export data object containing `manifest`,
1095
+ * `allLayersDir`, `allLayersExplodedDir`, `lastLayerConfig`,
1096
+ * `lastWorkingDir`, `binPaths`, and `pkgPathList`
1097
+ */
1019
1098
  export const extractFromManifest = async (
1020
1099
  manifestFile,
1021
1100
  localData,
@@ -1414,10 +1493,29 @@ export const getPkgPathList = (exportData, lastWorkingDir) => {
1414
1493
  return pathList;
1415
1494
  };
1416
1495
 
1496
+ /**
1497
+ * Remove a container image from the local Docker / Podman daemon.
1498
+ *
1499
+ * @param {string} fullImageName Full image name including tag or digest (e.g. "ubuntu:22.04")
1500
+ * @param {boolean} [force=false] When `true`, force-remove the image even if it is in use
1501
+ * @returns {Promise<Buffer|undefined>} Raw response buffer from the daemon, or
1502
+ * `undefined` if no daemon connection is available
1503
+ */
1417
1504
  export const removeImage = async (fullImageName, force = false) => {
1418
1505
  return await makeRequest(`images/${fullImageName}?force=${force}`, "DELETE");
1419
1506
  };
1420
1507
 
1508
+ /**
1509
+ * Retrieve a base64url-encoded authentication token for a registry server by
1510
+ * invoking the `docker-credential-<exeSuffix>` credential helper binary.
1511
+ * Results are cached in `registry_auth_keys` to avoid redundant subprocess
1512
+ * calls.
1513
+ *
1514
+ * @param {string} exeSuffix Credential helper name suffix (e.g. "osxkeychain", "wincred", "pass")
1515
+ * @param {string} serverAddress Registry server address (e.g. "https://index.docker.io/v1/")
1516
+ * @returns {string|undefined} Base64url-encoded JSON auth token, or `undefined`
1517
+ * if the helper is unavailable or returns an error
1518
+ */
1421
1519
  export const getCredsFromHelper = (exeSuffix, serverAddress) => {
1422
1520
  if (registry_auth_keys[serverAddress]) {
1423
1521
  return registry_auth_keys[serverAddress];
@@ -1460,6 +1558,15 @@ export const getCredsFromHelper = (exeSuffix, serverAddress) => {
1460
1558
  return undefined;
1461
1559
  };
1462
1560
 
1561
+ /**
1562
+ * Append skipped source-file entries to the `SrcFile` properties of matching
1563
+ * components. A component matches when its `oci:SrcImage` property value
1564
+ * equals the skipped image's `image` field and the source file path is not
1565
+ * already listed.
1566
+ *
1567
+ * @param {Array<{image: string, src: string}>} skippedImageSrcs List of skipped image/source pairs
1568
+ * @param {Array<Object>} components CycloneDX component objects to update in place
1569
+ */
1463
1570
  export const addSkippedSrcFiles = (skippedImageSrcs, components) => {
1464
1571
  for (const skippedImage of skippedImageSrcs) {
1465
1572
  for (const co of components) {
@@ -9,6 +9,16 @@ import {
9
9
  safeSpawnSync,
10
10
  } from "../helpers/utils.js";
11
11
 
12
+ /**
13
+ * Retrieves a CycloneDX BOM attached to an OCI image using the `oras` CLI tool.
14
+ * Discovers SBOM attachments via `oras discover`, pulls the first matching
15
+ * artifact, and returns the parsed BOM JSON. Retries automatically with a
16
+ * platform-specific manifest when the initial platform-agnostic discovery fails.
17
+ *
18
+ * @param {string} image OCI image reference (e.g. `"registry.example.com/org/app:tag"`)
19
+ * @param {string} [platform] OCI platform string (e.g. `"linux/amd64"`); detected automatically when omitted
20
+ * @returns {Object|undefined} Parsed CycloneDX BOM JSON object, or `undefined` if not found
21
+ */
12
22
  export function getBomWithOras(image, platform = undefined) {
13
23
  const platformArch = arch() === "arm64" ? "arm64" : "amd64";
14
24
  let parameters = [
@@ -4,16 +4,10 @@
4
4
  *
5
5
  * We use the internal pip api to construct the dependency tree for modern python + pip environments
6
6
  */
7
- import {
8
- existsSync,
9
- mkdtempSync,
10
- readFileSync,
11
- rmSync,
12
- writeFileSync,
13
- } from "node:fs";
7
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
14
8
  import { delimiter, join } from "node:path";
15
9
 
16
- import { getTmpDir, safeSpawnSync } from "../helpers/utils.js";
10
+ import { getTmpDir, safeExistsSync, safeSpawnSync } from "../helpers/utils.js";
17
11
 
18
12
  const PIP_TREE_PLUGIN_CONTENT = `
19
13
  import importlib.metadata as importlib_metadata
@@ -254,7 +248,7 @@ export const getTreeWithPlugin = (env, python_cmd, basePath) => {
254
248
  console.log(result.stdout, result.stderr);
255
249
  }
256
250
  }
257
- if (existsSync(pipTreeJson)) {
251
+ if (safeExistsSync(pipTreeJson)) {
258
252
  tree = JSON.parse(
259
253
  readFileSync(pipTreeJson, {
260
254
  encoding: "utf-8",
@@ -56,7 +56,7 @@ export function parseNpmrc(content) {
56
56
  }
57
57
 
58
58
  /**
59
- * Extract npm configuration values from environment variables.
59
+ * Extract npm/pnpm configuration values from environment variables.
60
60
  * See https://docs.npmjs.com/cli/v11/using-npm/config
61
61
  *
62
62
  * npm uses the NPM_CONFIG_ prefix for env var config:
@@ -65,24 +65,28 @@ export function parseNpmrc(content) {
65
65
  * - Simple keys are lowercased; scoped/URI keys preserve case
66
66
  * - Boolean flags without values are treated as true
67
67
  *
68
+ * pnpm v11+ uses the PNPM_CONFIG_ prefix instead of NPM_CONFIG_ for pnpm-specific settings.
69
+ * Both prefixes are supported; pnpm_config_* takes precedence over npm_config_* for the same key.
70
+ * See https://pnpm.io/next/npmrc
68
71
  * @param {Object} env - Environment variables object (defaults to process.env)
69
72
  * @returns {Object} Parsed npm config key-value pairs
70
73
  */
71
74
  export function parseNpmrcFromEnv(env = process.env) {
72
75
  const result = {};
73
- const PREFIX = "npm_config_";
74
-
75
- for (const [fullKey, value] of Object.entries(env)) {
76
- if (!fullKey.toLowerCase().startsWith(PREFIX)) {
77
- continue;
78
- }
79
-
80
- let configKey = fullKey.slice(PREFIX.length);
81
- if (!configKey) continue;
82
- if (!configKey.startsWith("//") && !configKey.startsWith("@")) {
83
- configKey = configKey.toLowerCase();
76
+ const NPM_PREFIX = "npm_config_";
77
+ const PNPM_PREFIX = "pnpm_config_";
78
+ for (const prefix of [NPM_PREFIX, PNPM_PREFIX]) {
79
+ for (const [fullKey, value] of Object.entries(env)) {
80
+ if (!fullKey.toLowerCase().startsWith(prefix)) {
81
+ continue;
82
+ }
83
+ let configKey = fullKey.slice(prefix.length);
84
+ if (!configKey) continue;
85
+ if (!configKey.startsWith("//") && !configKey.startsWith("@")) {
86
+ configKey = configKey.toLowerCase();
87
+ }
88
+ result[configKey] = value === "" || value === undefined ? "true" : value;
84
89
  }
85
- result[configKey] = value === "" || value === undefined ? "true" : value;
86
90
  }
87
91
  return result;
88
92
  }
@@ -415,16 +415,52 @@ const VALID_ENV_CASES = [
415
415
  cache: "/tmp/cache",
416
416
  },
417
417
  },
418
- {
419
- name: "scoped registry auth in env",
420
- env: { "npm_config_//registry.example.com/:_authToken": "secret123" },
421
- expected: { "//registry.example.com/:_authToken": "secret123" },
422
- },
423
418
  {
424
419
  name: "unicode values preserved",
425
420
  env: { npm_config_description: "测试🔐" },
426
421
  expected: { description: "测试🔐" },
427
422
  },
423
+ {
424
+ name: "basic pnpm_config_ prefix",
425
+ env: { pnpm_config_registry: "https://pnpm-registry.example.com" },
426
+ expected: { registry: "https://pnpm-registry.example.com" },
427
+ },
428
+ {
429
+ name: "case-insensitive PNPM_CONFIG_ prefix",
430
+ env: { PNPM_CONFIG_PROXY: "http://proxy.local" },
431
+ expected: { proxy: "http://proxy.local" },
432
+ },
433
+ {
434
+ name: "pnpm_config_ simple key lowercased",
435
+ env: { PNPM_CONFIG_STORE_DIR: "/custom/store" },
436
+ expected: { store_dir: "/custom/store" },
437
+ },
438
+ {
439
+ name: "pnpm_config_ boolean flag with empty value → true",
440
+ env: { pnpm_config_shamefully_hoist: "" },
441
+ expected: { shamefully_hoist: "true" },
442
+ },
443
+ {
444
+ name: "pnpm_config_ overrides npm_config_ for same key",
445
+ env: {
446
+ npm_config_registry: "https://npm-registry.com",
447
+ pnpm_config_registry: "https://pnpm-registry.com",
448
+ },
449
+ expected: { registry: "https://pnpm-registry.com" },
450
+ },
451
+ {
452
+ name: "pnpm_config_ and npm_config_ for different keys are both included",
453
+ env: {
454
+ npm_config_cache: "/npm-cache",
455
+ pnpm_config_store_dir: "/pnpm-store",
456
+ },
457
+ expected: { cache: "/npm-cache", store_dir: "/pnpm-store" },
458
+ },
459
+ {
460
+ name: "empty config key after pnpm_config_ prefix ignored",
461
+ env: { pnpm_config_: "value" },
462
+ expected: {},
463
+ },
428
464
  ];
429
465
 
430
466
  const EDGE_ENV_CASES = [
@@ -264,7 +264,7 @@ components:
264
264
  specVersion:
265
265
  type: string
266
266
  description: CycloneDX Specification version to use
267
- default: "1.6"
267
+ default: "1.7"
268
268
  filter:
269
269
  type: array
270
270
  items:
@@ -372,6 +372,17 @@ export function parseValue(raw) {
372
372
  throw new TypeError(`Invalid value type: ${t}.`);
373
373
  }
374
374
 
375
+ /**
376
+ * Parses allowed query/body parameters into a typed options object.
377
+ * Query parameters take priority over body parameters. Handles the
378
+ * `type` → `projectType` rename, lifecycle-based `installDeps` defaulting,
379
+ * and profile option expansion.
380
+ *
381
+ * @param {Object} q Parsed query string key/value map
382
+ * @param {Object} [body={}] Parsed request body key/value map
383
+ * @param {Object} [options={}] Seed options object to merge results into
384
+ * @returns {Object} Populated options object
385
+ */
375
386
  export function parseQueryString(q, body = {}, options = {}) {
376
387
  // Priority is query params followed by body
377
388
  for (const param of ALLOWED_PARAMS) {
@@ -391,6 +402,14 @@ export function parseQueryString(q, body = {}, options = {}) {
391
402
  return options;
392
403
  }
393
404
 
405
+ /**
406
+ * Extracts query parameters from an incoming HTTP request object.
407
+ * Handles repeated keys by collecting their values into an array.
408
+ * Returns an empty object if the URL cannot be parsed.
409
+ *
410
+ * @param {Object} req Node.js/connect HTTP request object
411
+ * @returns {Object} Key/value map of query parameters from the request URL
412
+ */
394
413
  export function getQueryParams(req) {
395
414
  try {
396
415
  if (!req?.url) {
@@ -402,7 +421,7 @@ export function getQueryParams(req) {
402
421
  const baseUrl = `${protocol}://${host}`;
403
422
 
404
423
  const fullUrl = new URL(req.url, baseUrl);
405
- const params = {};
424
+ const params = Object.create(null);
406
425
 
407
426
  // Convert multiple values to an array
408
427
  for (const [key, value] of fullUrl.searchParams) {
@@ -449,21 +468,25 @@ const configureServer = (cdxgenServer) => {
449
468
  const ALL_INTERFACES = new Set(["0.0.0.0", "::", "::/128", "::/0"]);
450
469
 
451
470
  const start = (options) => {
471
+ if (isSecureMode && !process.permission) {
472
+ console.error(
473
+ "SECURE MODE: Node.js permission model not enabled. Use --permission flag.",
474
+ );
475
+ process.exit(1);
476
+ }
452
477
  console.log(`cdxgen server version ${CDXGEN_VERSION}`);
453
-
454
- console.log(
455
- "Listening on",
456
- options.serverHost,
457
- options.serverPort,
458
- "without authentication!",
459
- );
460
478
  if (ALL_INTERFACES.has(options.serverHost)) {
461
479
  console.log("Exposing cdxgen server on all IP address is a security risk!");
462
480
  if (isSecureMode) {
463
481
  process.exit(1);
464
482
  }
465
483
  }
466
- if (+options.serverPort < 1024) {
484
+ const serverPort = Number(options.serverPort);
485
+ if (!Number.isInteger(serverPort) || serverPort <= 0 || serverPort > 65535) {
486
+ console.log("Invalid server port specified.");
487
+ process.exit(1);
488
+ }
489
+ if (serverPort < 1024) {
467
490
  console.log(
468
491
  "Running cdxgen server with a privileged port is a security risk!",
469
492
  );
@@ -503,9 +526,15 @@ const start = (options) => {
503
526
  process.exit(1);
504
527
  }
505
528
  }
529
+ console.log(
530
+ "Listening on",
531
+ options.serverHost,
532
+ serverPort,
533
+ "without authentication!",
534
+ );
506
535
  const cdxgenServer = http
507
536
  .createServer(app)
508
- .listen(options.serverPort, options.serverHost);
537
+ .listen(serverPort, options.serverHost);
509
538
  configureServer(cdxgenServer);
510
539
 
511
540
  app.use("/health", (_req, res) => {
@@ -525,7 +554,7 @@ const start = (options) => {
525
554
  }
526
555
  const q = getQueryParams(req);
527
556
  let cleanup = false;
528
- let reqOptions = {};
557
+ let reqOptions = Object.create(null);
529
558
  try {
530
559
  reqOptions = parseQueryString(
531
560
  q,