@cyclonedx/cdxgen 12.2.0 → 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 (181) hide show
  1. package/README.md +242 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +532 -168
  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 +276 -68
  33. package/lib/cli/index.poku.js +368 -0
  34. package/lib/helpers/analyzer.js +1052 -5
  35. package/lib/helpers/analyzer.poku.js +301 -0
  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/depsUtils.js +16 -0
  48. package/lib/helpers/depsUtils.poku.js +58 -1
  49. package/lib/helpers/display.js +245 -61
  50. package/lib/helpers/display.poku.js +162 -2
  51. package/lib/helpers/exportUtils.js +123 -0
  52. package/lib/helpers/exportUtils.poku.js +60 -0
  53. package/lib/helpers/formulationParsers.js +69 -0
  54. package/lib/helpers/formulationParsers.poku.js +44 -0
  55. package/lib/helpers/gtfobins.js +189 -0
  56. package/lib/helpers/gtfobins.poku.js +49 -0
  57. package/lib/helpers/lolbas.js +267 -0
  58. package/lib/helpers/lolbas.poku.js +39 -0
  59. package/lib/helpers/osqueryTransform.js +84 -0
  60. package/lib/helpers/osqueryTransform.poku.js +49 -0
  61. package/lib/helpers/provenanceUtils.js +193 -0
  62. package/lib/helpers/provenanceUtils.poku.js +145 -0
  63. package/lib/helpers/pylockutils.js +281 -0
  64. package/lib/helpers/pylockutils.poku.js +48 -0
  65. package/lib/helpers/registryProvenance.js +793 -0
  66. package/lib/helpers/registryProvenance.poku.js +452 -0
  67. package/lib/helpers/remote/dependency-track.js +84 -0
  68. package/lib/helpers/remote/dependency-track.poku.js +119 -0
  69. package/lib/helpers/source.js +1267 -0
  70. package/lib/helpers/source.poku.js +771 -0
  71. package/lib/helpers/spdxUtils.js +97 -0
  72. package/lib/helpers/spdxUtils.poku.js +70 -0
  73. package/lib/helpers/table.js +384 -0
  74. package/lib/helpers/table.poku.js +186 -0
  75. package/lib/helpers/unicodeScan.js +147 -0
  76. package/lib/helpers/unicodeScan.poku.js +45 -0
  77. package/lib/helpers/utils.js +882 -136
  78. package/lib/helpers/utils.poku.js +995 -91
  79. package/lib/managers/binary.js +29 -5
  80. package/lib/managers/docker.js +179 -52
  81. package/lib/managers/docker.poku.js +327 -28
  82. package/lib/managers/oci.js +107 -23
  83. package/lib/managers/oci.poku.js +132 -0
  84. package/lib/server/openapi.yaml +50 -0
  85. package/lib/server/server.js +228 -331
  86. package/lib/server/server.poku.js +220 -5
  87. package/lib/stages/postgen/annotator.js +7 -0
  88. package/lib/stages/postgen/annotator.poku.js +40 -0
  89. package/lib/stages/postgen/auditBom.js +20 -5
  90. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  91. package/lib/stages/postgen/postgen.js +40 -0
  92. package/lib/stages/postgen/postgen.poku.js +47 -0
  93. package/lib/stages/postgen/ruleEngine.js +80 -2
  94. package/lib/stages/postgen/spdxConverter.js +796 -0
  95. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  96. package/lib/validator/bomValidator.js +232 -0
  97. package/lib/validator/bomValidator.poku.js +70 -0
  98. package/lib/validator/complianceRules.js +70 -7
  99. package/lib/validator/complianceRules.poku.js +30 -0
  100. package/lib/validator/reporters/annotations.js +2 -2
  101. package/lib/validator/reporters/console.js +13 -2
  102. package/lib/validator/reporters.poku.js +13 -0
  103. package/package.json +10 -8
  104. package/types/bin/audit.d.ts +3 -0
  105. package/types/bin/audit.d.ts.map +1 -0
  106. package/types/bin/convert.d.ts +3 -0
  107. package/types/bin/convert.d.ts.map +1 -0
  108. package/types/bin/repl.d.ts.map +1 -1
  109. package/types/lib/audit/index.d.ts +115 -0
  110. package/types/lib/audit/index.d.ts.map +1 -0
  111. package/types/lib/audit/progress.d.ts +27 -0
  112. package/types/lib/audit/progress.d.ts.map +1 -0
  113. package/types/lib/audit/reporters.d.ts +35 -0
  114. package/types/lib/audit/reporters.d.ts.map +1 -0
  115. package/types/lib/audit/scoring.d.ts +35 -0
  116. package/types/lib/audit/scoring.d.ts.map +1 -0
  117. package/types/lib/audit/targets.d.ts +63 -0
  118. package/types/lib/audit/targets.d.ts.map +1 -0
  119. package/types/lib/cli/index.d.ts +8 -0
  120. package/types/lib/cli/index.d.ts.map +1 -1
  121. package/types/lib/helpers/analyzer.d.ts +13 -0
  122. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  123. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  124. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  125. package/types/lib/helpers/bomUtils.d.ts +5 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  127. package/types/lib/helpers/chromextutils.d.ts +97 -0
  128. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  129. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  130. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  131. package/types/lib/helpers/containerRisk.d.ts +17 -0
  132. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  133. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  134. package/types/lib/helpers/display.d.ts +4 -1
  135. package/types/lib/helpers/display.d.ts.map +1 -1
  136. package/types/lib/helpers/exportUtils.d.ts +40 -0
  137. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  138. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  139. package/types/lib/helpers/gtfobins.d.ts +17 -0
  140. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  141. package/types/lib/helpers/lolbas.d.ts +16 -0
  142. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  143. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  144. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  145. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  146. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  147. package/types/lib/helpers/pylockutils.d.ts +51 -0
  148. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  149. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  150. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  151. package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
  152. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
  153. package/types/lib/helpers/source.d.ts +141 -0
  154. package/types/lib/helpers/source.d.ts.map +1 -0
  155. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  156. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  157. package/types/lib/helpers/table.d.ts +6 -0
  158. package/types/lib/helpers/table.d.ts.map +1 -0
  159. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  160. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  161. package/types/lib/helpers/utils.d.ts +30 -11
  162. package/types/lib/helpers/utils.d.ts.map +1 -1
  163. package/types/lib/managers/binary.d.ts.map +1 -1
  164. package/types/lib/managers/docker.d.ts.map +1 -1
  165. package/types/lib/managers/oci.d.ts.map +1 -1
  166. package/types/lib/server/server.d.ts +0 -35
  167. package/types/lib/server/server.d.ts.map +1 -1
  168. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  170. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  171. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  172. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  173. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  174. package/types/lib/validator/bomValidator.d.ts +1 -0
  175. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  176. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  177. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  178. package/types/bin/dependencies.d.ts +0 -3
  179. package/types/bin/dependencies.d.ts.map +0 -1
  180. package/types/bin/licenses.d.ts +0 -3
  181. 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 =
@@ -36,10 +50,15 @@ const ALLOWED_PARAMS = [
36
50
  "projectGroup",
37
51
  "projectTag",
38
52
  "projectVersion",
53
+ "autoCreate",
54
+ "isLatest",
39
55
  "parentUUID",
56
+ "parentProjectName",
57
+ "parentProjectVersion",
40
58
  "serverUrl",
41
59
  "apiKey",
42
60
  "specVersion",
61
+ "format",
43
62
  "filter",
44
63
  "only",
45
64
  "autoCompositions",
@@ -57,270 +76,37 @@ const ALLOWED_PARAMS = [
57
76
 
58
77
  const app = connect();
59
78
 
60
- app.use(
61
- bodyParser.json({
62
- deflate: true,
63
- limit: "1mb",
64
- }),
65
- );
66
- app.use(compression());
67
-
68
- /**
69
- * Return git allow protocol string from the environment variables.
70
- *
71
- * @returns {string} git allow protocol string
72
- */
73
- function getGitAllowProtocol() {
74
- return (
75
- process.env.GIT_ALLOW_PROTOCOL ||
76
- process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL ||
77
- (isSecureMode ? "https:ssh" : "https:git:ssh")
78
- );
79
- }
80
-
81
- /**
82
- * Checks the given hostname against the allowed list.
83
- *
84
- * @param {string} hostname Host name to check
85
- * @returns {boolean} true if the hostname in its entirety is allowed. false otherwise.
86
- */
87
- export function isAllowedHost(hostname) {
88
- if (!process.env.CDXGEN_SERVER_ALLOWED_HOSTS) {
79
+ function isAllowedHttpHost(hostname) {
80
+ if (!process.env.CDXGEN_ALLOWED_HOSTS) {
89
81
  return true;
90
82
  }
91
- // Guard against dangerous Unicode characters
92
- if (hasDangerousUnicode(hostname)) {
83
+ if (!hostname || hasDangerousUnicode(hostname)) {
93
84
  return false;
94
85
  }
95
- return (process.env.CDXGEN_SERVER_ALLOWED_HOSTS || "")
96
- .split(",")
97
- .includes(hostname);
98
- }
99
-
100
- /**
101
- * Checks the given path string to belong to a drive in Windows.
102
- *
103
- * @param {string} p Path string to check
104
- * @returns {boolean} true if the windows path belongs to a drive. false otherwise (device names)
105
- */
106
- export function isAllowedWinPath(p) {
107
- if (typeof p !== "string") {
108
- return false;
109
- }
110
- if (p === "") {
111
- return true;
112
- }
113
- // Guard against dangerous Unicode characters
114
- if (hasDangerousUnicode(p)) {
115
- return false;
116
- }
117
- try {
118
- const normalized = path.normalize(p);
119
- // Check the entire normalized path for dangerous patterns
120
- if (hasDangerousUnicode(normalized)) {
121
- return false;
122
- }
123
- const { root } = path.parse(normalized);
124
- // Both Relative paths and invalid windows device names are resulting in an empty root
125
- // To keep things simple, we don't accept relative paths for Windows server-mode users at all
126
-
127
- // Invocations with unix-style paths result in "\\" as the root on windows
128
- // path.parse(path.normalize("/foo/bar"))
129
- // { root: '\\', dir: '\\foo', base: 'bar', ext: '', name: 'bar' }
130
- 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) {
131
91
  return true;
132
92
  }
133
- // Check for device/UNC paths - these should always return false
134
- if (root.startsWith("\\\\")) {
135
- return false;
136
- }
137
- // Strict validation for drive letter format
138
- return isValidDriveRoot(root);
139
- } catch (_err) {
140
- return false;
141
- }
142
- }
143
-
144
- /**
145
- * Checks the given path against the allowed list.
146
- *
147
- * @param {string} p Path string to check
148
- * @returns {boolean} true if the path is present in the allowed paths. false otherwise.
149
- */
150
- export function isAllowedPath(p) {
151
- if (typeof p !== "string") {
152
- return false;
153
- }
154
- // Guard against dangerous Unicode characters
155
- if (hasDangerousUnicode(p)) {
156
- return false;
157
- }
158
- if (!process.env.CDXGEN_SERVER_ALLOWED_PATHS) {
159
- return true;
160
- }
161
- // Handle CVE-2025-27210 without relying entirely on node blocklists
162
- if (isWin && !isAllowedWinPath(p)) {
163
- return false;
164
- }
165
- return (process.env.CDXGEN_SERVER_ALLOWED_PATHS || "")
166
- .split(",")
167
- .filter(Boolean)
168
- .some((ap) => {
169
- const resolvedP = path.resolve(p);
170
- const resolvedAp = path.resolve(ap);
171
- const relativePath = path.relative(resolvedAp, resolvedP);
172
- return (
173
- relativePath === "" ||
174
- (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
175
- );
176
- });
177
- }
178
-
179
- /**
180
- * Determine if the file path could be a remote URL.
181
- *
182
- * @param {string} filePath The Git URL or local path
183
- * @returns {Boolean} True if the file path is a remote URL. false otherwise.
184
- */
185
- export function maybeRemotePath(filePath) {
186
- return /^[a-zA-Z0-9+.-]+:\/\//.test(filePath) || filePath.startsWith("git@");
187
- }
188
-
189
- /**
190
- * Validates a given Git URL/Path against dangerous protocols and allowed hosts.
191
- *
192
- * @param {string} filePath The Git URL or local path
193
- * @returns {Object|null} Error object if invalid, or null if valid
194
- */
195
- export function validateAndRejectGitSource(filePath) {
196
- if (/^(ext|fd)::/i.test(filePath)) {
197
- return {
198
- status: 400,
199
- error: "Invalid Protocol",
200
- details: "The provided protocol is not allowed.",
201
- };
202
- }
203
- if (maybeRemotePath(filePath)) {
204
- let gitUrlObj;
205
- try {
206
- let urlToParse = filePath;
207
- if (filePath.startsWith("git@") && !filePath.includes("://")) {
208
- urlToParse = `ssh://${filePath.replace(":", "/")}`;
209
- }
210
- gitUrlObj = new URL(urlToParse);
211
- } catch (_err) {
212
- return {
213
- status: 400,
214
- error: "Invalid URL Format",
215
- details: "The provided Git URL is malformed.",
216
- };
217
- }
218
- const gitAllowProtocol = getGitAllowProtocol();
219
- const allowedSchemes = gitAllowProtocol
220
- .split(":")
221
- .filter(Boolean)
222
- .map((p) => `${p.toLowerCase()}:`);
223
-
224
93
  if (
225
- allowedSchemes.includes("ssh:") &&
226
- !allowedSchemes.includes("git+ssh:")
94
+ allowedHost.startsWith("*.") &&
95
+ hostname.endsWith(allowedHost.slice(1))
227
96
  ) {
228
- allowedSchemes.push("git+ssh:");
229
- }
230
-
231
- if (!allowedSchemes.includes(gitUrlObj.protocol)) {
232
- return {
233
- status: 400,
234
- error: "Protocol Not Allowed",
235
- details: `The protocol '${gitUrlObj.protocol}' is not permitted by GIT_ALLOW_PROTOCOL.`,
236
- };
237
- }
238
-
239
- if (gitUrlObj.href.includes("::")) {
240
- return {
241
- status: 400,
242
- error: "Invalid URL Syntax",
243
- details: "Git remote helper syntax (::) is not allowed.",
244
- };
245
- }
246
-
247
- if (!isAllowedHost(gitUrlObj.hostname)) {
248
- return {
249
- status: 403,
250
- error: "Host Not Allowed",
251
- details: "The Git URL host is not allowed as per the allowlist.",
252
- };
97
+ return true;
253
98
  }
254
99
  }
255
-
256
- return null;
100
+ return false;
257
101
  }
258
102
 
259
- function gitClone(repoUrl, branch = null) {
260
- let baseDirName = path.basename(repoUrl);
261
- if (!/^[a-zA-Z0-9_-]+$/.test(baseDirName)) {
262
- baseDirName = "repo-";
263
- }
264
- const tempDir = fs.mkdtempSync(path.join(getTmpDir(), baseDirName));
265
-
266
- const gitArgs = [
267
- "-c",
268
- "alias.clone=",
269
- "-c",
270
- "core.fsmonitor=false",
271
- "-c",
272
- "safe.bareRepository=explicit",
273
- "clone",
274
- repoUrl,
275
- "--depth",
276
- "1",
277
- tempDir,
278
- ];
279
- if (branch) {
280
- const firstBranchStr = Array.isArray(branch) ? branch[0] : String(branch);
281
- if (firstBranchStr.startsWith("-")) {
282
- console.log(
283
- `Skipping branch clone: invalid branch name ${firstBranchStr}`,
284
- );
285
- } else {
286
- const cloneIndex = gitArgs.indexOf("clone");
287
- gitArgs.splice(cloneIndex + 1, 0, "--branch", firstBranchStr);
288
- }
289
- }
290
- console.log(
291
- `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
292
- );
293
- const gitAllowProtocol = getGitAllowProtocol();
294
- const envConfigs = {
295
- GIT_CONFIG_COUNT: "2",
296
- GIT_CONFIG_KEY_0: "core.fsmonitor",
297
- GIT_CONFIG_VALUE_0: "false",
298
- GIT_CONFIG_KEY_1: "safe.bareRepository",
299
- GIT_CONFIG_VALUE_1: "explicit",
300
- };
301
- const env = isSecureMode
302
- ? {
303
- ...process.env,
304
- ...envConfigs,
305
- GIT_CONFIG_NOSYSTEM: "1",
306
- GIT_CONFIG_NOGLOBAL: "1",
307
- GIT_ALLOW_PROTOCOL: gitAllowProtocol,
308
- }
309
- : {
310
- ...process.env,
311
- ...envConfigs,
312
- GIT_ALLOW_PROTOCOL: gitAllowProtocol,
313
- };
314
- const result = safeSpawnSync("git", gitArgs, {
315
- shell: false,
316
- env,
317
- });
318
- if (result.status !== 0) {
319
- console.log(result.stderr);
320
- }
321
-
322
- return tempDir;
323
- }
103
+ app.use(
104
+ bodyParser.json({
105
+ deflate: true,
106
+ limit: "1mb",
107
+ }),
108
+ );
109
+ app.use(compression());
324
110
 
325
111
  function sanitizeStr(s) {
326
112
  return s ? s.replace(/[\r\n]/g, "") : s;
@@ -504,7 +290,10 @@ const start = (options) => {
504
290
  process.exit(1);
505
291
  }
506
292
  }
507
- if (!process.env.CDXGEN_SERVER_ALLOWED_HOSTS) {
293
+ if (
294
+ !process.env.CDXGEN_GIT_ALLOWED_HOSTS &&
295
+ !process.env.CDXGEN_SERVER_ALLOWED_HOSTS
296
+ ) {
508
297
  console.log(
509
298
  "No allowlist for git hosts has been specified. This is a security risk that could expose the system to SSRF vulnerabilities!",
510
299
  );
@@ -512,7 +301,11 @@ const start = (options) => {
512
301
  process.exit(1);
513
302
  }
514
303
  }
515
- 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
+ ) {
516
309
  console.log(
517
310
  "No allowlist for paths has been specified. This is a security risk that could expose the filesystem and internal secrets!",
518
311
  );
@@ -580,98 +373,202 @@ const start = (options) => {
580
373
  }),
581
374
  );
582
375
  }
583
- const validationError = validateAndRejectGitSource(filePath);
584
- if (validationError) {
585
- res.writeHead(validationError.status, {
586
- "Content-Type": "application/json",
587
- });
588
- return res.end(
589
- JSON.stringify({
590
- error: validationError.error,
591
- details: validationError.details,
592
- }),
593
- );
594
- }
376
+ let cloneDir;
595
377
  let srcDir;
596
- if (maybeRemotePath(filePath)) {
597
- srcDir = gitClone(filePath, reqOptions.gitBranch);
598
- cleanup = true;
599
- } else {
600
- srcDir = filePath;
601
- if (
602
- !isAllowedPath(path.resolve(srcDir)) ||
603
- (isWin && !isAllowedWinPath(srcDir))
604
- ) {
605
- 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
+ });
606
421
  return res.end(
607
422
  JSON.stringify({
608
- error: "Path Not Allowed",
609
- details: "Path is not allowed as per the allowlist.",
423
+ error: validationError.error,
424
+ details: validationError.details,
610
425
  }),
611
426
  );
612
427
  }
613
- }
614
- if (srcDir !== path.resolve(srcDir)) {
615
- res.writeHead(500, { "Content-Type": "application/json" });
616
- return res.end(
617
- JSON.stringify({
618
- error: "Absolute path needed",
619
- details: "Relative paths are not supported in server mode.",
620
- }),
621
- );
622
- }
623
- console.log("Generating SBOM for", srcDir);
624
- let bomNSData = (await createBom(srcDir, reqOptions)) || {};
625
- bomNSData = postProcess(bomNSData, reqOptions);
626
- if (reqOptions.serverUrl && reqOptions.apiKey) {
627
- if (!isAllowedHost(reqOptions.serverUrl)) {
628
- 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" });
629
478
  return res.end(
630
479
  JSON.stringify({
631
- error: "Host Not Allowed",
632
- 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.",
633
482
  }),
634
483
  );
635
484
  }
636
- if (isSecureMode && !reqOptions.serverUrl?.startsWith("https://")) {
637
- console.log(
638
- "Dependency Track API server is used with a non-https url, which poses a security risk.",
639
- );
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);
640
495
  }
641
- console.log(
642
- `Publishing SBOM ${reqOptions.projectName} to Dependency Track`,
643
- reqOptions.serverUrl,
644
- );
645
- try {
646
- await submitBom(reqOptions, bomNSData.bomJson);
647
- } catch (error) {
648
- const errorMessages = error.response?.body?.errors;
649
- if (errorMessages) {
650
- 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" });
651
503
  return res.end(
652
504
  JSON.stringify({
653
- error: "Unable to submit the SBOM to the Dependency-Track server",
654
- details: errorMessages,
505
+ error: "Invalid Server URL",
506
+ details: "The Dependency-Track server URL is invalid.",
655
507
  }),
656
508
  );
657
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
+ }
658
543
  }
659
- }
660
- res.writeHead(200, { "Content-Type": "application/json" });
661
- if (bomNSData.bomJson) {
662
- if (
663
- typeof bomNSData.bomJson === "string" ||
664
- bomNSData.bomJson instanceof String
665
- ) {
666
- res.write(bomNSData.bomJson);
667
- } else {
668
- 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);
669
571
  }
670
- }
671
- res.end("\n");
672
- if (cleanup && srcDir?.startsWith(getTmpDir()) && fs.rmSync) {
673
- console.log(`Cleaning up ${srcDir}`);
674
- fs.rmSync(srcDir, { recursive: true, force: true });
675
572
  }
676
573
  });
677
574
  };