@cyclonedx/cdxgen 12.2.1 → 12.3.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 (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
@@ -1,4 +1,3 @@
1
- import fs from "node:fs";
2
1
  import http from "node:http";
3
2
  import path from "node:path";
4
3
  import process from "node:process";
@@ -9,16 +8,31 @@ import compression from "compression";
9
8
  import connect from "connect";
10
9
 
11
10
  import { createBom, submitBom } from "../cli/index.js";
11
+ import { normalizeOutputFormats } from "../helpers/exportUtils.js";
12
+ import {
13
+ cleanupSourceDir,
14
+ findGitRefForPurlVersion,
15
+ getGitAllowProtocol,
16
+ gitClone,
17
+ isAllowedPath,
18
+ isAllowedWinPath,
19
+ maybePurlSource,
20
+ maybeRemotePath,
21
+ PURL_REGISTRY_LOOKUP_WARNING,
22
+ resolveGitUrlFromPurl,
23
+ resolvePurlSourceDirectory,
24
+ sanitizeRemoteUrlForLogs,
25
+ validateAndRejectGitSource,
26
+ validatePurlSource,
27
+ } from "../helpers/source.js";
12
28
  import {
13
29
  CDXGEN_VERSION,
14
- getTmpDir,
15
30
  hasDangerousUnicode,
16
31
  isSecureMode,
17
- isValidDriveRoot,
18
32
  isWin,
19
- safeSpawnSync,
20
33
  } from "../helpers/utils.js";
21
34
  import { postProcess } from "../stages/postgen/postgen.js";
35
+ import { convertCycloneDxToSpdx } from "../stages/postgen/spdxConverter.js";
22
36
 
23
37
  // Timeout milliseconds. Default 10 mins
24
38
  const TIMEOUT_MS =
@@ -44,6 +58,7 @@ const ALLOWED_PARAMS = [
44
58
  "serverUrl",
45
59
  "apiKey",
46
60
  "specVersion",
61
+ "format",
47
62
  "filter",
48
63
  "only",
49
64
  "autoCompositions",
@@ -61,274 +76,37 @@ const ALLOWED_PARAMS = [
61
76
 
62
77
  const app = connect();
63
78
 
64
- app.use(
65
- bodyParser.json({
66
- deflate: true,
67
- limit: "1mb",
68
- }),
69
- );
70
- app.use(compression());
71
-
72
- /**
73
- * Return git allow protocol string from the environment variables.
74
- *
75
- * @returns {string} git allow protocol string
76
- */
77
- function getGitAllowProtocol() {
78
- return (
79
- process.env.GIT_ALLOW_PROTOCOL ||
80
- process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL ||
81
- (isSecureMode ? "https:ssh" : "https:git:ssh")
82
- );
83
- }
84
-
85
- /**
86
- * Checks the given hostname against the allowed list.
87
- *
88
- * @param {string} hostname Host name to check
89
- * @returns {boolean} true if the hostname in its entirety is allowed. false otherwise.
90
- */
91
- export function isAllowedHost(hostname) {
92
- if (!process.env.CDXGEN_SERVER_ALLOWED_HOSTS) {
79
+ function isAllowedHttpHost(hostname) {
80
+ if (!process.env.CDXGEN_ALLOWED_HOSTS) {
93
81
  return true;
94
82
  }
95
- // Guard against dangerous Unicode characters
96
- if (hasDangerousUnicode(hostname)) {
83
+ if (!hostname || hasDangerousUnicode(hostname)) {
97
84
  return false;
98
85
  }
99
- return (process.env.CDXGEN_SERVER_ALLOWED_HOSTS || "")
100
- .split(",")
101
- .includes(hostname);
102
- }
103
-
104
- /**
105
- * Checks the given path string to belong to a drive in Windows.
106
- *
107
- * @param {string} p Path string to check
108
- * @returns {boolean} true if the windows path belongs to a drive. false otherwise (device names)
109
- */
110
- export function isAllowedWinPath(p) {
111
- if (typeof p !== "string") {
112
- return false;
113
- }
114
- if (p === "") {
115
- return true;
116
- }
117
- // Guard against dangerous Unicode characters
118
- if (hasDangerousUnicode(p)) {
119
- return false;
120
- }
121
- try {
122
- const normalized = path.normalize(p);
123
- // Check the entire normalized path for dangerous patterns
124
- if (hasDangerousUnicode(normalized)) {
125
- return false;
126
- }
127
- const { root } = path.parse(normalized);
128
- // Both Relative paths and invalid windows device names are resulting in an empty root
129
- // To keep things simple, we don't accept relative paths for Windows server-mode users at all
130
-
131
- // Invocations with unix-style paths result in "\\" as the root on windows
132
- // path.parse(path.normalize("/foo/bar"))
133
- // { root: '\\', dir: '\\foo', base: 'bar', ext: '', name: 'bar' }
134
- if (root === "\\") {
86
+ const allowHosts = process.env.CDXGEN_ALLOWED_HOSTS.split(",")
87
+ .map((host) => host.trim())
88
+ .filter(Boolean);
89
+ for (const allowedHost of allowHosts) {
90
+ if (hostname === allowedHost) {
135
91
  return true;
136
92
  }
137
- // Check for device/UNC paths - these should always return false
138
- if (root.startsWith("\\\\")) {
139
- return false;
140
- }
141
- // Strict validation for drive letter format
142
- return isValidDriveRoot(root);
143
- } catch (_err) {
144
- return false;
145
- }
146
- }
147
-
148
- /**
149
- * Checks the given path against the allowed list.
150
- *
151
- * @param {string} p Path string to check
152
- * @returns {boolean} true if the path is present in the allowed paths. false otherwise.
153
- */
154
- export function isAllowedPath(p) {
155
- if (typeof p !== "string") {
156
- return false;
157
- }
158
- // Guard against dangerous Unicode characters
159
- if (hasDangerousUnicode(p)) {
160
- return false;
161
- }
162
- if (!process.env.CDXGEN_SERVER_ALLOWED_PATHS) {
163
- return true;
164
- }
165
- // Handle CVE-2025-27210 without relying entirely on node blocklists
166
- if (isWin && !isAllowedWinPath(p)) {
167
- return false;
168
- }
169
- return (process.env.CDXGEN_SERVER_ALLOWED_PATHS || "")
170
- .split(",")
171
- .filter(Boolean)
172
- .some((ap) => {
173
- const resolvedP = path.resolve(p);
174
- const resolvedAp = path.resolve(ap);
175
- const relativePath = path.relative(resolvedAp, resolvedP);
176
- return (
177
- relativePath === "" ||
178
- (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
179
- );
180
- });
181
- }
182
-
183
- /**
184
- * Determine if the file path could be a remote URL.
185
- *
186
- * @param {string} filePath The Git URL or local path
187
- * @returns {Boolean} True if the file path is a remote URL. false otherwise.
188
- */
189
- export function maybeRemotePath(filePath) {
190
- return /^[a-zA-Z0-9+.-]+:\/\//.test(filePath) || filePath.startsWith("git@");
191
- }
192
-
193
- /**
194
- * Validates a given Git URL/Path against dangerous protocols and allowed hosts.
195
- *
196
- * @param {string} filePath The Git URL or local path
197
- * @returns {Object|null} Error object if invalid, or null if valid
198
- */
199
- export function validateAndRejectGitSource(filePath) {
200
- if (/^(ext|fd)::/i.test(filePath)) {
201
- return {
202
- status: 400,
203
- error: "Invalid Protocol",
204
- details: "The provided protocol is not allowed.",
205
- };
206
- }
207
- if (maybeRemotePath(filePath)) {
208
- let gitUrlObj;
209
- try {
210
- let urlToParse = filePath;
211
- if (filePath.startsWith("git@") && !filePath.includes("://")) {
212
- urlToParse = `ssh://${filePath.replace(":", "/")}`;
213
- }
214
- gitUrlObj = new URL(urlToParse);
215
- } catch (_err) {
216
- return {
217
- status: 400,
218
- error: "Invalid URL Format",
219
- details: "The provided Git URL is malformed.",
220
- };
221
- }
222
- const gitAllowProtocol = getGitAllowProtocol();
223
- const allowedSchemes = gitAllowProtocol
224
- .split(":")
225
- .filter(Boolean)
226
- .map((p) => `${p.toLowerCase()}:`);
227
-
228
93
  if (
229
- allowedSchemes.includes("ssh:") &&
230
- !allowedSchemes.includes("git+ssh:")
94
+ allowedHost.startsWith("*.") &&
95
+ hostname.endsWith(allowedHost.slice(1))
231
96
  ) {
232
- allowedSchemes.push("git+ssh:");
233
- }
234
-
235
- if (!allowedSchemes.includes(gitUrlObj.protocol)) {
236
- return {
237
- status: 400,
238
- error: "Protocol Not Allowed",
239
- details: `The protocol '${gitUrlObj.protocol}' is not permitted by GIT_ALLOW_PROTOCOL.`,
240
- };
241
- }
242
-
243
- if (gitUrlObj.href.includes("::")) {
244
- return {
245
- status: 400,
246
- error: "Invalid URL Syntax",
247
- details: "Git remote helper syntax (::) is not allowed.",
248
- };
249
- }
250
-
251
- if (!isAllowedHost(gitUrlObj.hostname)) {
252
- return {
253
- status: 403,
254
- error: "Host Not Allowed",
255
- details: "The Git URL host is not allowed as per the allowlist.",
256
- };
97
+ return true;
257
98
  }
258
99
  }
259
-
260
- return null;
100
+ return false;
261
101
  }
262
102
 
263
- function gitClone(repoUrl, branch = null) {
264
- let baseDirName = path.basename(repoUrl);
265
- if (!/^[a-zA-Z0-9_-]+$/.test(baseDirName)) {
266
- baseDirName = "repo-";
267
- }
268
- const tempDir = fs.mkdtempSync(path.join(getTmpDir(), baseDirName));
269
-
270
- const gitArgs = [
271
- "-c",
272
- "alias.clone=",
273
- "-c",
274
- "core.fsmonitor=false",
275
- "-c",
276
- "safe.bareRepository=explicit",
277
- "-c",
278
- "core.hooksPath=/dev/null",
279
- "clone",
280
- "--template=",
281
- repoUrl,
282
- "--depth",
283
- "1",
284
- tempDir,
285
- ];
286
- if (branch) {
287
- const firstBranchStr = Array.isArray(branch) ? branch[0] : String(branch);
288
- if (firstBranchStr.startsWith("-")) {
289
- console.log(
290
- `Skipping branch clone: invalid branch name ${firstBranchStr}`,
291
- );
292
- } else {
293
- const cloneIndex = gitArgs.indexOf("clone");
294
- gitArgs.splice(cloneIndex + 1, 0, "--branch", firstBranchStr);
295
- }
296
- }
297
- console.log(
298
- `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
299
- );
300
- const gitAllowProtocol = getGitAllowProtocol();
301
- const envConfigs = {
302
- GIT_CONFIG_COUNT: "2",
303
- GIT_CONFIG_KEY_0: "core.fsmonitor",
304
- GIT_CONFIG_VALUE_0: "false",
305
- GIT_CONFIG_KEY_1: "safe.bareRepository",
306
- GIT_CONFIG_VALUE_1: "explicit",
307
- GIT_TERMINAL_PROMPT: "0",
308
- };
309
- const env = isSecureMode
310
- ? {
311
- ...process.env,
312
- ...envConfigs,
313
- GIT_CONFIG_NOSYSTEM: "1",
314
- GIT_CONFIG_GLOBAL: "/dev/null",
315
- GIT_ALLOW_PROTOCOL: gitAllowProtocol,
316
- }
317
- : {
318
- ...process.env,
319
- ...envConfigs,
320
- GIT_ALLOW_PROTOCOL: gitAllowProtocol,
321
- };
322
- const result = safeSpawnSync("git", gitArgs, {
323
- shell: false,
324
- env,
325
- });
326
- if (result.status !== 0) {
327
- console.log(result.stderr);
328
- }
329
-
330
- return tempDir;
331
- }
103
+ app.use(
104
+ bodyParser.json({
105
+ deflate: true,
106
+ limit: "1mb",
107
+ }),
108
+ );
109
+ app.use(compression());
332
110
 
333
111
  function sanitizeStr(s) {
334
112
  return s ? s.replace(/[\r\n]/g, "") : s;
@@ -512,7 +290,10 @@ const start = (options) => {
512
290
  process.exit(1);
513
291
  }
514
292
  }
515
- if (!process.env.CDXGEN_SERVER_ALLOWED_HOSTS) {
293
+ if (
294
+ !process.env.CDXGEN_GIT_ALLOWED_HOSTS &&
295
+ !process.env.CDXGEN_SERVER_ALLOWED_HOSTS
296
+ ) {
516
297
  console.log(
517
298
  "No allowlist for git hosts has been specified. This is a security risk that could expose the system to SSRF vulnerabilities!",
518
299
  );
@@ -520,7 +301,11 @@ const start = (options) => {
520
301
  process.exit(1);
521
302
  }
522
303
  }
523
- if (isSecureMode && !process.env.CDXGEN_SERVER_ALLOWED_PATHS) {
304
+ if (
305
+ isSecureMode &&
306
+ !process.env.CDXGEN_ALLOWED_PATHS &&
307
+ !process.env.CDXGEN_SERVER_ALLOWED_PATHS
308
+ ) {
524
309
  console.log(
525
310
  "No allowlist for paths has been specified. This is a security risk that could expose the filesystem and internal secrets!",
526
311
  );
@@ -588,100 +373,204 @@ const start = (options) => {
588
373
  }),
589
374
  );
590
375
  }
591
- const validationError = validateAndRejectGitSource(filePath);
592
- if (validationError) {
593
- res.writeHead(validationError.status, {
594
- "Content-Type": "application/json",
595
- });
596
- return res.end(
597
- JSON.stringify({
598
- error: validationError.error,
599
- details: validationError.details,
600
- }),
601
- );
602
- }
376
+ let cloneDir;
603
377
  let srcDir;
604
- if (maybeRemotePath(filePath)) {
605
- srcDir = gitClone(filePath, reqOptions.gitBranch);
606
- cleanup = true;
607
- } else {
608
- srcDir = filePath;
609
- if (
610
- !isAllowedPath(path.resolve(srcDir)) ||
611
- (isWin && !isAllowedWinPath(srcDir))
612
- ) {
613
- res.writeHead(403, { "Content-Type": "application/json" });
378
+ try {
379
+ let sourcePath = filePath;
380
+ let purlResolution;
381
+ if (maybePurlSource(sourcePath)) {
382
+ const purlValidationError = validatePurlSource(sourcePath);
383
+ if (purlValidationError) {
384
+ res.writeHead(purlValidationError.status, {
385
+ "Content-Type": "application/json",
386
+ });
387
+ return res.end(
388
+ JSON.stringify({
389
+ error: purlValidationError.error,
390
+ details: purlValidationError.details,
391
+ }),
392
+ );
393
+ }
394
+ purlResolution = await resolveGitUrlFromPurl(sourcePath);
395
+ if (!purlResolution?.repoUrl) {
396
+ res.writeHead(400, { "Content-Type": "application/json" });
397
+ return res.end(
398
+ JSON.stringify({
399
+ error: "Unsupported purl source",
400
+ details:
401
+ "Unable to resolve the provided package URL to a repository URL.",
402
+ }),
403
+ );
404
+ }
405
+ if (purlResolution.registry) {
406
+ console.warn(
407
+ `${PURL_REGISTRY_LOOKUP_WARNING} Registry: ${purlResolution.registry}, purl type: ${purlResolution.type}, resolved URL: ${sanitizeRemoteUrlForLogs(purlResolution.repoUrl)}`,
408
+ );
409
+ } else {
410
+ console.warn(
411
+ `Resolved repository URL from purl metadata. purl type: ${purlResolution.type}, resolved URL: ${sanitizeRemoteUrlForLogs(purlResolution.repoUrl)}`,
412
+ );
413
+ }
414
+ sourcePath = purlResolution.repoUrl;
415
+ }
416
+ const validationError = validateAndRejectGitSource(sourcePath);
417
+ if (validationError) {
418
+ res.writeHead(validationError.status, {
419
+ "Content-Type": "application/json",
420
+ });
614
421
  return res.end(
615
422
  JSON.stringify({
616
- error: "Path Not Allowed",
617
- details: "Path is not allowed as per the allowlist.",
423
+ error: validationError.error,
424
+ details: validationError.details,
618
425
  }),
619
426
  );
620
427
  }
621
- }
622
- if (srcDir !== path.resolve(srcDir)) {
623
- res.writeHead(500, { "Content-Type": "application/json" });
624
- return res.end(
625
- JSON.stringify({
626
- error: "Absolute path needed",
627
- details: "Relative paths are not supported in server mode.",
628
- }),
629
- );
630
- }
631
- console.log("Generating SBOM for", srcDir);
632
- let bomNSData = (await createBom(srcDir, reqOptions)) || {};
633
- bomNSData = postProcess(bomNSData, reqOptions);
634
- if (reqOptions.serverUrl && reqOptions.apiKey) {
635
- if (!isAllowedHost(reqOptions.serverUrl)) {
636
- res.writeHead(403, { "Content-Type": "application/json" });
428
+ if (maybeRemotePath(sourcePath)) {
429
+ let gitRef = reqOptions.gitBranch;
430
+ if (!gitRef && purlResolution?.version) {
431
+ gitRef = findGitRefForPurlVersion(sourcePath, purlResolution);
432
+ if (!gitRef) {
433
+ console.warn(
434
+ `Unable to find a matching git tag for version '${purlResolution.version}'. Falling back to repository default branch.`,
435
+ );
436
+ }
437
+ }
438
+ cloneDir = gitClone(sourcePath, gitRef);
439
+ srcDir = cloneDir;
440
+ if (purlResolution?.type === "npm") {
441
+ const cloneRootDir = cloneDir;
442
+ const purlSourceDir = resolvePurlSourceDirectory(
443
+ srcDir,
444
+ purlResolution,
445
+ );
446
+ if (purlSourceDir && purlSourceDir !== cloneRootDir) {
447
+ const relativeDir = path.relative(cloneRootDir, purlSourceDir);
448
+ if (relativeDir.startsWith("..") || path.isAbsolute(relativeDir)) {
449
+ console.warn(
450
+ `Ignoring detected npm package directory outside clone root: ${purlSourceDir}`,
451
+ );
452
+ } else {
453
+ console.warn(
454
+ `Using npm package directory '${purlSourceDir}' for purl '${purlResolution.namespace ? `${purlResolution.namespace}/` : ""}${purlResolution.name}'.`,
455
+ );
456
+ srcDir = purlSourceDir;
457
+ }
458
+ }
459
+ }
460
+ cleanup = true;
461
+ } else {
462
+ srcDir = sourcePath;
463
+ if (
464
+ !isAllowedPath(path.resolve(srcDir)) ||
465
+ (isWin && !isAllowedWinPath(srcDir))
466
+ ) {
467
+ res.writeHead(403, { "Content-Type": "application/json" });
468
+ return res.end(
469
+ JSON.stringify({
470
+ error: "Path Not Allowed",
471
+ details: "Path is not allowed as per the allowlist.",
472
+ }),
473
+ );
474
+ }
475
+ }
476
+ if (srcDir !== path.resolve(srcDir)) {
477
+ res.writeHead(500, { "Content-Type": "application/json" });
637
478
  return res.end(
638
479
  JSON.stringify({
639
- error: "Host Not Allowed",
640
- details: "The URL host is not allowed as per the allowlist.",
480
+ error: "Absolute path needed",
481
+ details: "Relative paths are not supported in server mode.",
641
482
  }),
642
483
  );
643
484
  }
644
- if (isSecureMode && !reqOptions.serverUrl?.startsWith("https://")) {
645
- console.log(
646
- "Dependency Track API server is used with a non-https url, which poses a security risk.",
647
- );
485
+ console.log("Generating SBOM for", srcDir);
486
+ let bomNSData = (await createBom(srcDir, reqOptions)) || {};
487
+ bomNSData = postProcess(bomNSData, reqOptions, srcDir);
488
+ const requestedFormats = normalizeOutputFormats(reqOptions.format);
489
+ let responseBomJson = bomNSData.bomJson;
490
+ if (
491
+ requestedFormats.includes("spdx") &&
492
+ bomNSData?.bomJson?.bomFormat === "CycloneDX"
493
+ ) {
494
+ responseBomJson = convertCycloneDxToSpdx(bomNSData.bomJson, reqOptions);
648
495
  }
649
- console.log(
650
- `Publishing SBOM ${reqOptions.projectName} to Dependency Track`,
651
- reqOptions.serverUrl,
652
- );
653
- try {
654
- await submitBom(reqOptions, bomNSData.bomJson);
655
- } catch (error) {
656
- const errorMessages = error.response?.body?.errors;
657
- if (errorMessages) {
658
- res.writeHead(500, { "Content-Type": "application/json" });
496
+ if (reqOptions.serverUrl && reqOptions.apiKey) {
497
+ let serverHostname;
498
+ try {
499
+ serverHostname = new URL(reqOptions.serverUrl).hostname;
500
+ } catch (err) {
501
+ console.log("Invalid Dependency-Track server URL", err);
502
+ res.writeHead(400, { "Content-Type": "application/json" });
659
503
  return res.end(
660
504
  JSON.stringify({
661
- error: "Unable to submit the SBOM to the Dependency-Track server",
662
- details: errorMessages,
505
+ error: "Invalid Server URL",
506
+ details: "The Dependency-Track server URL is invalid.",
663
507
  }),
664
508
  );
665
509
  }
510
+ if (!isAllowedHttpHost(serverHostname)) {
511
+ res.writeHead(403, { "Content-Type": "application/json" });
512
+ return res.end(
513
+ JSON.stringify({
514
+ error: "Host Not Allowed",
515
+ details: "The URL host is not allowed as per the allowlist.",
516
+ }),
517
+ );
518
+ }
519
+ if (isSecureMode && !reqOptions.serverUrl?.startsWith("https://")) {
520
+ console.log(
521
+ "Dependency Track API server is used with a non-https url, which poses a security risk.",
522
+ );
523
+ }
524
+ console.log(
525
+ `Publishing SBOM ${reqOptions.projectName} to Dependency Track`,
526
+ reqOptions.serverUrl,
527
+ );
528
+ try {
529
+ await submitBom(reqOptions, bomNSData.bomJson);
530
+ } catch (error) {
531
+ const errorMessages = error.response?.body?.errors;
532
+ if (errorMessages) {
533
+ res.writeHead(500, { "Content-Type": "application/json" });
534
+ return res.end(
535
+ JSON.stringify({
536
+ error:
537
+ "Unable to submit the SBOM to the Dependency-Track server",
538
+ details: errorMessages,
539
+ }),
540
+ );
541
+ }
542
+ }
666
543
  }
667
- }
668
- res.writeHead(200, { "Content-Type": "application/json" });
669
- if (bomNSData.bomJson) {
670
- if (
671
- typeof bomNSData.bomJson === "string" ||
672
- bomNSData.bomJson instanceof String
673
- ) {
674
- res.write(bomNSData.bomJson);
675
- } else {
676
- res.write(JSON.stringify(bomNSData.bomJson, null, null));
544
+ res.writeHead(200, { "Content-Type": "application/json" });
545
+ if (responseBomJson) {
546
+ if (
547
+ typeof responseBomJson === "string" ||
548
+ responseBomJson instanceof String
549
+ ) {
550
+ res.write(responseBomJson);
551
+ } else {
552
+ res.write(JSON.stringify(responseBomJson, null, null));
553
+ }
554
+ }
555
+ res.end("\n");
556
+ } catch (err) {
557
+ if (!res.headersSent) {
558
+ console.log("Unable to generate SBOM", err);
559
+ res.writeHead(500, { "Content-Type": "application/json" });
560
+ return res.end(
561
+ JSON.stringify({
562
+ error: "Unable to generate SBOM",
563
+ details: "Unexpected server error while generating SBOM.",
564
+ }),
565
+ );
566
+ }
567
+ console.log("Error while generating SBOM response", err);
568
+ } finally {
569
+ if (cleanup && cloneDir) {
570
+ cleanupSourceDir(cloneDir);
677
571
  }
678
- }
679
- res.end("\n");
680
- if (cleanup && srcDir?.startsWith(getTmpDir()) && fs.rmSync) {
681
- console.log(`Cleaning up ${srcDir}`);
682
- fs.rmSync(srcDir, { recursive: true, force: true });
683
572
  }
684
573
  });
685
574
  };
686
575
 
687
- export { configureServer, gitClone, start };
576
+ export { configureServer, start };