@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
@@ -0,0 +1,1267 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { URL } from "node:url";
5
+
6
+ import { PackageURL } from "packageurl-js";
7
+ import { coerce, diff, prerelease } from "semver";
8
+
9
+ import { thoughtLog } from "./logger.js";
10
+ import {
11
+ cdxgenAgent,
12
+ DEBUG_MODE,
13
+ fetchPomXmlAsJson,
14
+ getTmpDir,
15
+ hasDangerousUnicode,
16
+ isSecureMode,
17
+ isValidDriveRoot,
18
+ isWin,
19
+ safeSpawnSync,
20
+ } from "./utils.js";
21
+
22
+ export const PURL_REGISTRY_LOOKUP_WARNING =
23
+ "Resolved repository URL from package registry metadata. This source can be inaccurate or malicious; review before trusting results.";
24
+
25
+ export const SUPPORTED_PURL_SOURCE_TYPES = [
26
+ "npm",
27
+ "pypi",
28
+ "gem",
29
+ "cargo",
30
+ "pub",
31
+ "github",
32
+ "bitbucket",
33
+ "maven",
34
+ "composer",
35
+ "generic",
36
+ ];
37
+
38
+ const MAX_MONOREPO_PACKAGE_JSON_FILES = 2000;
39
+ const MAX_MONOREPO_DIRECTORIES = 5000;
40
+ const MAX_RELEASE_NOTE_RESOLVES = 50;
41
+
42
+ /**
43
+ * Build a scoped npm package name.
44
+ *
45
+ * @param {string|undefined} namespace package namespace
46
+ * @param {string|undefined} name package name
47
+ * @returns {string|undefined} scoped package name
48
+ */
49
+ function buildScopedNpmPackageName(namespace, name) {
50
+ if (!name) {
51
+ return undefined;
52
+ }
53
+ if (!namespace) {
54
+ return name;
55
+ }
56
+ return `${namespace.startsWith("@") ? namespace : `@${namespace}`}/${name}`;
57
+ }
58
+
59
+ /**
60
+ * Validate git ref names used as branch/tag values.
61
+ *
62
+ * @param {string|undefined} refName branch or tag name
63
+ * @returns {boolean} true if safe
64
+ */
65
+ function isSafeGitRefName(refName) {
66
+ if (!refName || typeof refName !== "string") {
67
+ return false;
68
+ }
69
+ if (refName.startsWith("-")) {
70
+ return false;
71
+ }
72
+ return /^[A-Za-z0-9._/@+-]+$/.test(refName);
73
+ }
74
+
75
+ /**
76
+ * Execute git with hardened defaults.
77
+ *
78
+ * @param {string[]} args git arguments
79
+ * @param {Object} options command options
80
+ * @param {string|undefined} options.cwd working directory
81
+ * @returns {Object} spawn result
82
+ */
83
+ export function hardenedGitCommand(args, options = {}) {
84
+ const gitAllowProtocol = getGitAllowProtocol();
85
+ const envConfigs = {
86
+ GIT_CONFIG_COUNT: "2",
87
+ GIT_CONFIG_KEY_0: "core.fsmonitor",
88
+ GIT_CONFIG_VALUE_0: "false",
89
+ GIT_CONFIG_KEY_1: "safe.bareRepository",
90
+ GIT_CONFIG_VALUE_1: "explicit",
91
+ GIT_TERMINAL_PROMPT: "0",
92
+ };
93
+ const env = isSecureMode
94
+ ? {
95
+ ...process.env,
96
+ ...envConfigs,
97
+ GIT_CONFIG_NOSYSTEM: "1",
98
+ GIT_CONFIG_GLOBAL: "/dev/null",
99
+ GIT_ALLOW_PROTOCOL: gitAllowProtocol,
100
+ }
101
+ : {
102
+ ...process.env,
103
+ ...envConfigs,
104
+ GIT_ALLOW_PROTOCOL: gitAllowProtocol,
105
+ };
106
+ return safeSpawnSync("git", args, {
107
+ shell: false,
108
+ cwd: options.cwd,
109
+ env,
110
+ });
111
+ }
112
+
113
+ function normalizeTagName(tagName) {
114
+ if (!tagName || typeof tagName !== "string") {
115
+ return undefined;
116
+ }
117
+ return tagName
118
+ .trim()
119
+ .replace(/^refs\/tags\//, "")
120
+ .replace(/\^\{\}$/, "");
121
+ }
122
+
123
+ function releaseTypeFromTags(currentTag, previousTag) {
124
+ const currentVersion = coerce(currentTag || "");
125
+ const previousVersion = coerce(previousTag || "");
126
+ if (!currentVersion || !previousVersion) {
127
+ return "internal";
128
+ }
129
+ if (prerelease(currentVersion.version)?.length) {
130
+ return "pre-release";
131
+ }
132
+ const versionDiff = diff(previousVersion.version, currentVersion.version);
133
+ if (versionDiff === "major") {
134
+ return "major";
135
+ }
136
+ if (versionDiff === "minor") {
137
+ return "minor";
138
+ }
139
+ if (versionDiff === "patch") {
140
+ return "patch";
141
+ }
142
+ return "internal";
143
+ }
144
+
145
+ function parseTagList(output) {
146
+ return (output || "")
147
+ .split("\n")
148
+ .map((line) => normalizeTagName(line))
149
+ .filter((tagName) => isSafeGitRefName(tagName))
150
+ .filter(Boolean);
151
+ }
152
+
153
+ function parseLsRemoteTags(output) {
154
+ const tags = [];
155
+ for (const line of (output || "").split("\n")) {
156
+ const ref = normalizeTagName(line.trim().split(/\s+/)[1]);
157
+ if (ref && isSafeGitRefName(ref)) {
158
+ tags.push(ref);
159
+ }
160
+ }
161
+ return Array.from(new Set(tags)).sort((a, b) => {
162
+ const av = coerce(a || "");
163
+ const bv = coerce(b || "");
164
+ if (av && bv) {
165
+ return bv.compare(av);
166
+ }
167
+ return b.localeCompare(a);
168
+ });
169
+ }
170
+
171
+ function issueTypeFromCommitMessage(message) {
172
+ const normalized = (message || "").toLowerCase();
173
+ if (
174
+ normalized.includes("security") ||
175
+ normalized.includes("cve-") ||
176
+ normalized.includes("vuln")
177
+ ) {
178
+ return "security";
179
+ }
180
+ if (
181
+ normalized.startsWith("fix") ||
182
+ normalized.includes(" bug") ||
183
+ normalized.includes("defect")
184
+ ) {
185
+ return "defect";
186
+ }
187
+ return "enhancement";
188
+ }
189
+
190
+ /**
191
+ * Build CycloneDX release notes from git tags and commits.
192
+ *
193
+ * @param {string|undefined} repoPath local repository path
194
+ * @param {Object} options options carrying release notes hints
195
+ * @returns {Object|undefined} releaseNotes object
196
+ */
197
+ export function buildReleaseNotesFromGit(repoPath, options = {}) {
198
+ let currentTag = normalizeTagName(options.releaseNotesCurrentTag);
199
+ let previousTag = normalizeTagName(options.releaseNotesPreviousTag);
200
+ let remoteUrl;
201
+ let localRepoAvailable = false;
202
+ if (repoPath && !maybeRemotePath(repoPath)) {
203
+ const repoCheck = hardenedGitCommand(
204
+ ["rev-parse", "--is-inside-work-tree"],
205
+ {
206
+ cwd: repoPath,
207
+ },
208
+ );
209
+ localRepoAvailable = repoCheck.status === 0;
210
+ if (localRepoAvailable) {
211
+ const localTagsResult = hardenedGitCommand(
212
+ ["tag", "--sort=-creatordate", "--merged", "HEAD"],
213
+ { cwd: repoPath },
214
+ );
215
+ const localTags = parseTagList(localTagsResult.stdout);
216
+ if (!currentTag && localTags.length) {
217
+ currentTag = localTags[0];
218
+ }
219
+ if (!previousTag && localTags.length > 1) {
220
+ previousTag = localTags.find((t) => t !== currentTag);
221
+ }
222
+ const remoteResult = hardenedGitCommand(
223
+ ["config", "--get", "remote.origin.url"],
224
+ { cwd: repoPath },
225
+ );
226
+ if (remoteResult.status === 0 && remoteResult.stdout) {
227
+ remoteUrl = remoteResult.stdout.toString().trim();
228
+ }
229
+ }
230
+ }
231
+ remoteUrl = remoteUrl || options.releaseNotesGitUrl;
232
+ const remoteUrlValidationError =
233
+ typeof remoteUrl === "string"
234
+ ? validateAndRejectGitSource(remoteUrl)
235
+ : null;
236
+ const canDiscoverRemoteTags =
237
+ (!currentTag || !previousTag) &&
238
+ typeof remoteUrl === "string" &&
239
+ !remoteUrl.startsWith("-") &&
240
+ !remoteUrlValidationError &&
241
+ /github\.com[:/]/i.test(remoteUrl);
242
+ if (canDiscoverRemoteTags) {
243
+ const remoteTagsResult = hardenedGitCommand(
244
+ [
245
+ "-c",
246
+ "alias.ls-remote=",
247
+ "-c",
248
+ "core.fsmonitor=false",
249
+ "-c",
250
+ "safe.bareRepository=explicit",
251
+ "-c",
252
+ "core.hooksPath=/dev/null",
253
+ "ls-remote",
254
+ "--refs",
255
+ "--tags",
256
+ "--",
257
+ remoteUrl,
258
+ ],
259
+ {},
260
+ );
261
+ const remoteTags = parseLsRemoteTags(remoteTagsResult.stdout);
262
+ if (!currentTag && remoteTags.length) {
263
+ currentTag = remoteTags[0];
264
+ }
265
+ if (!previousTag && remoteTags.length > 1) {
266
+ previousTag = remoteTags.find((t) => t !== currentTag);
267
+ }
268
+ }
269
+ if (!currentTag) {
270
+ return undefined;
271
+ }
272
+ if (!isSafeGitRefName(currentTag)) {
273
+ return undefined;
274
+ }
275
+ if (previousTag && !isSafeGitRefName(previousTag)) {
276
+ previousTag = undefined;
277
+ }
278
+ let timestamp;
279
+ if (localRepoAvailable) {
280
+ const tsResult = hardenedGitCommand(
281
+ ["log", "-1", "--format=%cI", currentTag],
282
+ {
283
+ cwd: repoPath,
284
+ },
285
+ );
286
+ if (tsResult.status === 0 && tsResult.stdout) {
287
+ timestamp = tsResult.stdout.toString().trim();
288
+ }
289
+ }
290
+ if (!timestamp) {
291
+ timestamp = new Date().toISOString();
292
+ }
293
+ const resolves = [];
294
+ if (localRepoAvailable && previousTag) {
295
+ const logResult = hardenedGitCommand(
296
+ ["log", "--pretty=format:%H%x09%s", `${previousTag}..${currentTag}`],
297
+ { cwd: repoPath },
298
+ );
299
+ if (logResult.status === 0 && logResult.stdout) {
300
+ // Keep the resolves list bounded to avoid excessive metadata growth.
301
+ for (const line of logResult.stdout
302
+ .toString()
303
+ .split("\n")
304
+ .filter(Boolean)) {
305
+ const [sha, ...rest] = line.split("\t");
306
+ const message = rest.join("\t").trim();
307
+ if (!sha || !message) {
308
+ continue;
309
+ }
310
+ resolves.push({
311
+ type: issueTypeFromCommitMessage(message),
312
+ id: sha.substring(0, 12),
313
+ name: message,
314
+ description: message,
315
+ });
316
+ }
317
+ if (resolves.length > MAX_RELEASE_NOTE_RESOLVES) {
318
+ resolves.length = MAX_RELEASE_NOTE_RESOLVES;
319
+ }
320
+ }
321
+ }
322
+ const tags = [currentTag];
323
+ if (previousTag && previousTag !== currentTag) {
324
+ tags.push(previousTag);
325
+ }
326
+ return {
327
+ type: releaseTypeFromTags(currentTag, previousTag),
328
+ title: `Release notes for ${currentTag}`,
329
+ description: previousTag
330
+ ? `Changes between ${previousTag} and ${currentTag}.`
331
+ : `Release notes for ${currentTag}.`,
332
+ timestamp,
333
+ tags,
334
+ resolves,
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Return git allow protocol string from the environment variables.
340
+ *
341
+ * @returns {string} git allow protocol string
342
+ */
343
+ export function getGitAllowProtocol() {
344
+ return (
345
+ process.env.GIT_ALLOW_PROTOCOL ||
346
+ process.env.CDXGEN_GIT_ALLOW_PROTOCOL ||
347
+ process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL ||
348
+ (isSecureMode ? "https:ssh" : "https:git:ssh")
349
+ );
350
+ }
351
+
352
+ /**
353
+ * Return configured allowed git hosts.
354
+ *
355
+ * @returns {string[]} list of configured hosts
356
+ */
357
+ function getAllowedHosts() {
358
+ const configuredHosts =
359
+ process.env.CDXGEN_GIT_ALLOWED_HOSTS ||
360
+ process.env.CDXGEN_SERVER_ALLOWED_HOSTS ||
361
+ "";
362
+ return configuredHosts
363
+ .split(",")
364
+ .map((h) => h.trim())
365
+ .filter(Boolean);
366
+ }
367
+
368
+ /**
369
+ * Checks the given hostname against the allowed list.
370
+ *
371
+ * @param {string} hostname Host name to check
372
+ * @returns {boolean} true if the hostname in its entirety is allowed. false otherwise.
373
+ */
374
+ export function isAllowedHost(hostname) {
375
+ const allowedHosts = getAllowedHosts();
376
+ if (!allowedHosts.length) {
377
+ return true;
378
+ }
379
+ if (hasDangerousUnicode(hostname)) {
380
+ return false;
381
+ }
382
+ return allowedHosts.includes(hostname);
383
+ }
384
+
385
+ /**
386
+ * Return configured allowed paths.
387
+ *
388
+ * @returns {string[]} list of configured paths
389
+ */
390
+ function getAllowedPaths() {
391
+ const configuredPaths =
392
+ process.env.CDXGEN_ALLOWED_PATHS ||
393
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS ||
394
+ "";
395
+ return configuredPaths
396
+ .split(",")
397
+ .map((p) => p.trim())
398
+ .filter(Boolean);
399
+ }
400
+
401
+ /**
402
+ * Checks the given path string to belong to a drive in Windows.
403
+ *
404
+ * @param {string} p Path string to check
405
+ * @returns {boolean} true if the windows path belongs to a drive. false otherwise (device names)
406
+ */
407
+ export function isAllowedWinPath(p) {
408
+ if (typeof p !== "string") {
409
+ return false;
410
+ }
411
+ if (p === "") {
412
+ return true;
413
+ }
414
+ if (hasDangerousUnicode(p)) {
415
+ return false;
416
+ }
417
+ if (!isWin) {
418
+ return true;
419
+ }
420
+ try {
421
+ const normalized = path.normalize(p);
422
+ if (hasDangerousUnicode(normalized)) {
423
+ return false;
424
+ }
425
+ const { root } = path.parse(normalized);
426
+ if (root === "\\") {
427
+ return true;
428
+ }
429
+ if (root.startsWith("\\\\")) {
430
+ return false;
431
+ }
432
+ return isValidDriveRoot(root);
433
+ } catch (_err) {
434
+ return false;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Checks the given path against the allowed list.
440
+ *
441
+ * @param {string} p Path string to check
442
+ * @returns {boolean} true if the path is present in the allowed paths. false otherwise.
443
+ */
444
+ export function isAllowedPath(p) {
445
+ if (typeof p !== "string") {
446
+ return false;
447
+ }
448
+ if (hasDangerousUnicode(p)) {
449
+ return false;
450
+ }
451
+ const allowedPaths = getAllowedPaths();
452
+ if (!allowedPaths.length) {
453
+ return true;
454
+ }
455
+ if (isWin && !isAllowedWinPath(p)) {
456
+ return false;
457
+ }
458
+ return allowedPaths.some((ap) => {
459
+ const resolvedP = path.resolve(p);
460
+ const resolvedAp = path.resolve(ap);
461
+ const relativePath = path.relative(resolvedAp, resolvedP);
462
+ return (
463
+ relativePath === "" ||
464
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
465
+ );
466
+ });
467
+ }
468
+
469
+ /**
470
+ * Determine if the path could be a package URL.
471
+ *
472
+ * @param {string} filePath Path or URL
473
+ * @returns {boolean} true if the file path looks like a purl
474
+ */
475
+ export function maybePurlSource(filePath) {
476
+ return typeof filePath === "string" && filePath.startsWith("pkg:");
477
+ }
478
+
479
+ /**
480
+ * Determine if the file path could be a remote URL.
481
+ *
482
+ * @param {string} filePath The Git URL or local path
483
+ * @returns {boolean} true if the file path is a remote URL. false otherwise.
484
+ */
485
+ export function maybeRemotePath(filePath) {
486
+ return /^[a-zA-Z0-9+.-]+:\/\//.test(filePath) || filePath.startsWith("git@");
487
+ }
488
+
489
+ /**
490
+ * Validates a given Git URL/Path against dangerous protocols and allowed hosts.
491
+ *
492
+ * @param {string} filePath The Git URL or local path
493
+ * @returns {Object|null} Error object if invalid, or null if valid
494
+ */
495
+ export function validateAndRejectGitSource(filePath) {
496
+ if (/^(ext|fd)::/i.test(filePath)) {
497
+ return {
498
+ status: 400,
499
+ error: "Invalid Protocol",
500
+ details: "The provided protocol is not allowed.",
501
+ };
502
+ }
503
+ if (maybeRemotePath(filePath)) {
504
+ let gitUrlObj;
505
+ try {
506
+ let urlToParse = filePath;
507
+ if (filePath.startsWith("git@") && !filePath.includes("://")) {
508
+ urlToParse = `ssh://${filePath.replace(":", "/")}`;
509
+ }
510
+ gitUrlObj = new URL(urlToParse);
511
+ } catch (_err) {
512
+ return {
513
+ status: 400,
514
+ error: "Invalid URL Format",
515
+ details: "The provided Git URL is malformed.",
516
+ };
517
+ }
518
+ const gitAllowProtocol = getGitAllowProtocol();
519
+ const allowedSchemes = gitAllowProtocol
520
+ .split(":")
521
+ .filter(Boolean)
522
+ .map((p) => `${p.toLowerCase()}:`);
523
+
524
+ if (
525
+ allowedSchemes.includes("ssh:") &&
526
+ !allowedSchemes.includes("git+ssh:")
527
+ ) {
528
+ allowedSchemes.push("git+ssh:");
529
+ }
530
+
531
+ if (!allowedSchemes.includes(gitUrlObj.protocol)) {
532
+ return {
533
+ status: 400,
534
+ error: "Protocol Not Allowed",
535
+ details: `The protocol '${gitUrlObj.protocol}' is not permitted by GIT_ALLOW_PROTOCOL.`,
536
+ };
537
+ }
538
+
539
+ if (gitUrlObj.href.includes("::")) {
540
+ return {
541
+ status: 400,
542
+ error: "Invalid URL Syntax",
543
+ details: "Git remote helper syntax (::) is not allowed.",
544
+ };
545
+ }
546
+
547
+ if (!isAllowedHost(gitUrlObj.hostname)) {
548
+ return {
549
+ status: 403,
550
+ error: "Host Not Allowed",
551
+ details: "The Git URL host is not allowed as per the allowlist.",
552
+ };
553
+ }
554
+ }
555
+
556
+ return null;
557
+ }
558
+
559
+ /**
560
+ * Clone a git repository into a temporary directory.
561
+ *
562
+ * @param {string} repoUrl Repository URL
563
+ * @param {string|string[]|null} branch Branch name
564
+ * @returns {string} cloned directory path
565
+ */
566
+ export function gitClone(repoUrl, branch = null) {
567
+ let baseDirName = path.basename(repoUrl);
568
+ if (!/^[a-zA-Z0-9_-]+$/.test(baseDirName)) {
569
+ baseDirName = "repo-";
570
+ }
571
+ const tempDir = fs.mkdtempSync(path.join(getTmpDir(), baseDirName));
572
+
573
+ const gitArgs = [
574
+ "-c",
575
+ "alias.clone=",
576
+ "-c",
577
+ "core.fsmonitor=false",
578
+ "-c",
579
+ "safe.bareRepository=explicit",
580
+ "-c",
581
+ "core.hooksPath=/dev/null",
582
+ "clone",
583
+ "--template=",
584
+ repoUrl,
585
+ "--depth",
586
+ "1",
587
+ tempDir,
588
+ ];
589
+ if (branch) {
590
+ const firstBranchStr = Array.isArray(branch) ? branch[0] : String(branch);
591
+ if (!isSafeGitRefName(firstBranchStr)) {
592
+ console.warn("Skipping branch clone: invalid branch name");
593
+ } else {
594
+ const cloneIndex = gitArgs.indexOf("clone");
595
+ gitArgs.splice(cloneIndex + 1, 0, "--branch", firstBranchStr);
596
+ }
597
+ }
598
+ thoughtLog(
599
+ `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
600
+ );
601
+ const result = hardenedGitCommand(gitArgs);
602
+ if (result.status !== 0) {
603
+ console.error(result.stderr);
604
+ }
605
+
606
+ return tempDir;
607
+ }
608
+
609
+ /**
610
+ * Sanitize remote URL for logging.
611
+ *
612
+ * @param {string|undefined} remoteUrl Repository URL
613
+ * @returns {string|undefined} sanitized URL
614
+ */
615
+ export function sanitizeRemoteUrlForLogs(remoteUrl) {
616
+ if (!remoteUrl || typeof remoteUrl !== "string") {
617
+ return remoteUrl;
618
+ }
619
+ try {
620
+ const parsed = new URL(remoteUrl);
621
+ if (parsed.username || parsed.password) {
622
+ parsed.username = "***";
623
+ parsed.password = "***";
624
+ }
625
+ return parsed.toString();
626
+ } catch (_err) {
627
+ return remoteUrl;
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Find a matching git ref for a package version.
633
+ *
634
+ * @param {string} repoUrl Repository URL
635
+ * @param {Object|undefined} purlResolution purl resolution metadata
636
+ * @returns {string|undefined} matching tag or branch reference
637
+ */
638
+ export function findGitRefForPurlVersion(repoUrl, purlResolution) {
639
+ const packageVersion = purlResolution?.version;
640
+ if (!packageVersion) {
641
+ return undefined;
642
+ }
643
+ const purlType = purlResolution?.type;
644
+ const purlNamespace = purlResolution?.namespace;
645
+ const purlName = purlResolution?.name;
646
+ const refCandidates = [packageVersion, `v${packageVersion}`];
647
+ if (purlType === "npm" && purlName) {
648
+ const scopedName = buildScopedNpmPackageName(purlNamespace, purlName);
649
+ refCandidates.push(
650
+ `${purlName}@${packageVersion}`,
651
+ `${scopedName}@${packageVersion}`,
652
+ `${purlName}-v${packageVersion}`,
653
+ `${scopedName}-v${packageVersion}`,
654
+ );
655
+ }
656
+ const filteredCandidates = [...new Set(refCandidates)].filter((candidate) =>
657
+ isSafeGitRefName(candidate),
658
+ );
659
+ if (!repoUrl || repoUrl.startsWith("-")) {
660
+ return undefined;
661
+ }
662
+ if (validateAndRejectGitSource(repoUrl)) {
663
+ return undefined;
664
+ }
665
+ const result = hardenedGitCommand(
666
+ [
667
+ "-c",
668
+ "alias.ls-remote=",
669
+ "-c",
670
+ "core.fsmonitor=false",
671
+ "-c",
672
+ "safe.bareRepository=explicit",
673
+ "-c",
674
+ "core.hooksPath=/dev/null",
675
+ "ls-remote",
676
+ "--refs",
677
+ "--tags",
678
+ "--heads",
679
+ "--",
680
+ repoUrl,
681
+ ],
682
+ {},
683
+ );
684
+ if (result.status !== 0 || !result.stdout) {
685
+ return undefined;
686
+ }
687
+ const availableRefs = result.stdout
688
+ .split("\n")
689
+ .map((line) => line.trim().split(/\s+/)[1])
690
+ .filter(Boolean)
691
+ .map((ref) => ref.replace(/^refs\/(?:tags|heads)\//, ""));
692
+ for (const candidate of filteredCandidates) {
693
+ if (availableRefs.includes(candidate)) {
694
+ return candidate;
695
+ }
696
+ }
697
+ return undefined;
698
+ }
699
+
700
+ /**
701
+ * Find the best source directory for purl-based npm monorepo scans.
702
+ *
703
+ * @param {string} srcDir cloned source directory
704
+ * @param {Object|undefined} purlResolution purl resolution metadata
705
+ * @returns {string|undefined} preferred source directory
706
+ */
707
+ export function resolvePurlSourceDirectory(srcDir, purlResolution) {
708
+ if (purlResolution?.type !== "npm" || !purlResolution?.name || !srcDir) {
709
+ return undefined;
710
+ }
711
+ const purlNamespace = purlResolution?.namespace;
712
+ const packageNameCandidates = [purlResolution.name];
713
+ if (purlNamespace) {
714
+ packageNameCandidates.push(
715
+ `${purlNamespace}/${purlResolution.name}`,
716
+ buildScopedNpmPackageName(purlNamespace, purlResolution.name),
717
+ );
718
+ }
719
+ const uniquePackageNameCandidates = [
720
+ ...new Set(packageNameCandidates.filter(Boolean)),
721
+ ];
722
+ const skipDirectories = new Set([
723
+ ".git",
724
+ ".idea",
725
+ ".vscode",
726
+ "build",
727
+ "dist",
728
+ "node_modules",
729
+ "out",
730
+ "target",
731
+ "vendor",
732
+ ]);
733
+ const queue = [srcDir];
734
+ const matches = new Set();
735
+ let packageJsonCount = 0;
736
+ let currentIndex = 0;
737
+ while (
738
+ currentIndex < queue.length &&
739
+ packageJsonCount < MAX_MONOREPO_PACKAGE_JSON_FILES
740
+ ) {
741
+ if (currentIndex >= MAX_MONOREPO_DIRECTORIES) {
742
+ break;
743
+ }
744
+ const currentDir = queue[currentIndex];
745
+ currentIndex += 1;
746
+ if (!currentDir) {
747
+ continue;
748
+ }
749
+ let entries = [];
750
+ try {
751
+ entries = fs.readdirSync(currentDir, {
752
+ withFileTypes: true,
753
+ });
754
+ } catch (_err) {
755
+ continue;
756
+ }
757
+ for (const entry of entries) {
758
+ if (entry.isDirectory()) {
759
+ if (skipDirectories.has(entry.name)) {
760
+ continue;
761
+ }
762
+ queue.push(path.join(currentDir, entry.name));
763
+ continue;
764
+ }
765
+ if (!entry.isFile() || entry.name !== "package.json") {
766
+ continue;
767
+ }
768
+ packageJsonCount += 1;
769
+ const packageJsonPath = path.join(currentDir, entry.name);
770
+ try {
771
+ const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
772
+ if (uniquePackageNameCandidates.includes(pkgJson?.name)) {
773
+ matches.add(currentDir);
774
+ }
775
+ } catch (_err) {
776
+ // Ignore invalid package.json files in monorepos.
777
+ }
778
+ }
779
+ }
780
+ if (!matches.size) {
781
+ return undefined;
782
+ }
783
+ const dedupedMatches = [...matches];
784
+ dedupedMatches.sort((a, b) => a.length - b.length);
785
+ return dedupedMatches[0];
786
+ }
787
+
788
+ /**
789
+ * Normalize repository URL values from registry metadata to a cloneable URL.
790
+ *
791
+ * @param {string|undefined} candidateUrl raw URL candidate
792
+ * @returns {string|undefined} normalized URL
793
+ */
794
+ function normalizeRepositoryUrl(candidateUrl) {
795
+ if (!candidateUrl || typeof candidateUrl !== "string") {
796
+ return undefined;
797
+ }
798
+ let repoUrl = candidateUrl.trim();
799
+ if (!repoUrl) {
800
+ return undefined;
801
+ }
802
+ if (/^git\s+/.test(repoUrl)) {
803
+ repoUrl = repoUrl.replace(/^git\s+/, "git+");
804
+ }
805
+ if (repoUrl.startsWith("git+")) {
806
+ repoUrl = repoUrl.slice(4);
807
+ }
808
+ if (repoUrl.startsWith("scm:git:")) {
809
+ repoUrl = repoUrl.slice(8);
810
+ }
811
+ if (repoUrl.startsWith("github:")) {
812
+ repoUrl = `https://github.com/${repoUrl.slice("github:".length)}`;
813
+ }
814
+ if (repoUrl.startsWith("gitlab:")) {
815
+ repoUrl = `https://gitlab.com/${repoUrl.slice("gitlab:".length)}`;
816
+ }
817
+ if (repoUrl.startsWith("bitbucket:")) {
818
+ repoUrl = `https://bitbucket.org/${repoUrl.slice("bitbucket:".length)}`;
819
+ }
820
+ if (
821
+ !repoUrl.includes("://") &&
822
+ /^(?:[^/]+\.)?github\.com\/.+/.test(repoUrl)
823
+ ) {
824
+ repoUrl = `https://${repoUrl}`;
825
+ }
826
+ if (!repoUrl.includes("://") && repoUrl.startsWith("www.")) {
827
+ repoUrl = `https://${repoUrl}`;
828
+ }
829
+ const hashIndex = repoUrl.indexOf("#");
830
+ if (hashIndex > -1) {
831
+ repoUrl = repoUrl.slice(0, hashIndex);
832
+ }
833
+ return repoUrl;
834
+ }
835
+
836
+ /**
837
+ * Normalize repository URL values represented as string/object metadata fields.
838
+ *
839
+ * @param {string|Object|undefined} candidate repository field value
840
+ * @returns {string|undefined} normalized URL
841
+ */
842
+ function normalizeRepositoryObject(candidate) {
843
+ if (!candidate) {
844
+ return undefined;
845
+ }
846
+ if (typeof candidate === "string") {
847
+ return normalizeRepositoryUrl(candidate);
848
+ }
849
+ if (typeof candidate === "object") {
850
+ return normalizeRepositoryUrl(candidate.url);
851
+ }
852
+ return undefined;
853
+ }
854
+
855
+ /**
856
+ * Build a cloneable repository URL from known download URL patterns.
857
+ *
858
+ * @param {string|undefined} candidateUrl raw download URL
859
+ * @returns {string|undefined} normalized repository URL
860
+ */
861
+ function normalizeDownloadRepositoryUrl(candidateUrl) {
862
+ const normalized = normalizeRepositoryUrl(candidateUrl);
863
+ if (!normalized) {
864
+ return undefined;
865
+ }
866
+ let parsed;
867
+ try {
868
+ parsed = new URL(normalized);
869
+ } catch (_err) {
870
+ return undefined;
871
+ }
872
+ const segments = parsed.pathname.split("/").filter(Boolean);
873
+ if (parsed.hostname === "github.com" && segments.length >= 2) {
874
+ return `https://github.com/${segments[0]}/${segments[1]}`;
875
+ }
876
+ if (parsed.hostname === "codeload.github.com" && segments.length >= 2) {
877
+ return `https://github.com/${segments[0]}/${segments[1]}`;
878
+ }
879
+ if (parsed.hostname === "gitlab.com" && segments.length >= 2) {
880
+ return `https://gitlab.com/${segments[0]}/${segments[1]}`;
881
+ }
882
+ if (normalized.endsWith(".git")) {
883
+ return normalized;
884
+ }
885
+ return undefined;
886
+ }
887
+
888
+ /**
889
+ * Build package name key for registry lookup from a parsed package URL.
890
+ *
891
+ * @param {PackageURL} purlObj parsed package URL object
892
+ * @returns {string} package name suitable for registry API lookup
893
+ */
894
+ function packageNameForLookup(purlObj) {
895
+ const namespace = purlObj.namespace;
896
+ if (!namespace) {
897
+ return purlObj.name;
898
+ }
899
+ return `${namespace}/${purlObj.name}`;
900
+ }
901
+
902
+ /**
903
+ * Validate package URL source input and return an error object when invalid.
904
+ *
905
+ * @param {string} purlString package URL string
906
+ * @returns {{status:number,error:string,details:string}|null} validation error or null
907
+ */
908
+ export function validatePurlSource(purlString) {
909
+ if (!maybePurlSource(purlString)) {
910
+ return null;
911
+ }
912
+ let purlObj;
913
+ try {
914
+ purlObj = PackageURL.fromString(purlString);
915
+ } catch (_err) {
916
+ return {
917
+ status: 400,
918
+ error: "Invalid purl source",
919
+ details: "The provided package URL is malformed.",
920
+ };
921
+ }
922
+ const purlType = purlObj?.type;
923
+ if (!SUPPORTED_PURL_SOURCE_TYPES.includes(purlType)) {
924
+ return {
925
+ status: 400,
926
+ error: "Unsupported purl source type",
927
+ details: `Supported purl types for automatic git URL detection: ${SUPPORTED_PURL_SOURCE_TYPES.join(", ")}.`,
928
+ };
929
+ }
930
+ if (!purlObj?.name) {
931
+ return {
932
+ status: 400,
933
+ error: "Invalid purl source",
934
+ details: "The provided package URL does not include a package name.",
935
+ };
936
+ }
937
+ const purlQualifiers = purlObj?.qualifiers || {};
938
+ if (
939
+ ["github", "bitbucket", "maven", "composer"].includes(purlType) &&
940
+ !purlObj?.namespace
941
+ ) {
942
+ return {
943
+ status: 400,
944
+ error: "Invalid purl source",
945
+ details: `The provided ${purlType} package URL must include a namespace.`,
946
+ };
947
+ }
948
+ if (purlType === "maven" && !purlObj?.version) {
949
+ return {
950
+ status: 400,
951
+ error: "Invalid purl source",
952
+ details: "The provided maven package URL must include a version.",
953
+ };
954
+ }
955
+ if (
956
+ purlType === "generic" &&
957
+ !purlQualifiers.vcs_url &&
958
+ !purlQualifiers.download_url
959
+ ) {
960
+ return {
961
+ status: 400,
962
+ error: "Unsupported generic purl source",
963
+ details:
964
+ "generic purl sources must include a vcs_url or download_url qualifier.",
965
+ };
966
+ }
967
+ return null;
968
+ }
969
+
970
+ /**
971
+ * Resolve a git repository URL from a package URL by querying package registries.
972
+ *
973
+ * Supported purl types:
974
+ * - npm -> registry.npmjs.org
975
+ * - pypi -> pypi.org
976
+ * - gem -> rubygems.org
977
+ * - cargo -> crates.io
978
+ * - pub -> pub.dev
979
+ * - github -> github.com/{namespace}/{name}
980
+ * - bitbucket -> bitbucket.org/{namespace}/{name}
981
+ * - maven -> repo1.maven.org POM scm metadata
982
+ * - composer -> repo.packagist.org p2 metadata
983
+ * - generic -> qualifiers: vcs_url, download_url
984
+ *
985
+ * @param {string} purlString package URL string
986
+ * @returns {Promise<{repoUrl:string|undefined, registry:string|undefined, type:string}|undefined>} resolution result
987
+ */
988
+ export async function resolveGitUrlFromPurl(purlString) {
989
+ if (!maybePurlSource(purlString)) {
990
+ return undefined;
991
+ }
992
+ const validationError = validatePurlSource(purlString);
993
+ if (validationError) {
994
+ return undefined;
995
+ }
996
+ let purlObj;
997
+ try {
998
+ purlObj = PackageURL.fromString(purlString);
999
+ } catch (err) {
1000
+ if (DEBUG_MODE) {
1001
+ const errorMessage = err?.message || String(err);
1002
+ thoughtLog("Unable to resolve repository URL for purl.", {
1003
+ purlString,
1004
+ errorMessage,
1005
+ });
1006
+ }
1007
+ return undefined;
1008
+ }
1009
+ if (!purlObj?.type || !purlObj?.name) {
1010
+ return undefined;
1011
+ }
1012
+
1013
+ const packageName = packageNameForLookup(purlObj);
1014
+ const packageVersion = purlObj.version;
1015
+ let repoUrl;
1016
+ let registry;
1017
+ const logPurlResolutionError = (err) => {
1018
+ const errorMessage = err?.message || String(err);
1019
+ const errorDetails = [];
1020
+ if (err?.code) {
1021
+ errorDetails.push(`code=${err.code}`);
1022
+ }
1023
+ if (typeof err?.statusCode === "number") {
1024
+ errorDetails.push(`status=${err.statusCode}`);
1025
+ }
1026
+ if (err?.hostname) {
1027
+ errorDetails.push(`host=${err.hostname}`);
1028
+ }
1029
+ const sanitizedRegistry = sanitizeRemoteUrlForLogs(registry);
1030
+ console.error(
1031
+ `Unable to resolve repository URL for purl '${purlString}'${sanitizedRegistry ? ` using registry '${sanitizedRegistry}'` : ""}: ${errorMessage}${errorDetails.length ? ` (${errorDetails.join(", ")})` : ""}`,
1032
+ );
1033
+ if (DEBUG_MODE) {
1034
+ thoughtLog("Unable to resolve repository URL for purl.", {
1035
+ purlString,
1036
+ errorMessage,
1037
+ errorDetails,
1038
+ registry: sanitizedRegistry,
1039
+ });
1040
+ }
1041
+ };
1042
+
1043
+ try {
1044
+ switch (purlObj.type) {
1045
+ case "npm": {
1046
+ registry = process.env.NPM_URL || "https://registry.npmjs.org/";
1047
+ const res = await cdxgenAgent.get(`${registry}${packageName}`, {
1048
+ responseType: "json",
1049
+ });
1050
+ const body = res.body;
1051
+ const versionBody = packageVersion
1052
+ ? body.versions?.[packageVersion]
1053
+ : undefined;
1054
+ repoUrl =
1055
+ normalizeRepositoryObject(versionBody?.repository) ||
1056
+ normalizeRepositoryObject(body.repository) ||
1057
+ normalizeRepositoryUrl(versionBody?.homepage) ||
1058
+ normalizeRepositoryUrl(body.homepage);
1059
+ break;
1060
+ }
1061
+ case "pypi": {
1062
+ registry = process.env.PYPI_URL || "https://pypi.org/pypi/";
1063
+ const suffix = packageVersion
1064
+ ? `${purlObj.name}/${packageVersion}/json`
1065
+ : `${purlObj.name}/json`;
1066
+ const res = await cdxgenAgent.get(`${registry}${suffix}`, {
1067
+ responseType: "json",
1068
+ });
1069
+ const info = res.body?.info || {};
1070
+ const projectUrls = info.project_urls || {};
1071
+ repoUrl =
1072
+ normalizeRepositoryUrl(projectUrls.Source) ||
1073
+ normalizeRepositoryUrl(projectUrls.Repository) ||
1074
+ normalizeRepositoryUrl(projectUrls["Source Code"]) ||
1075
+ normalizeRepositoryUrl(projectUrls.Code) ||
1076
+ normalizeRepositoryUrl(info.home_page);
1077
+ break;
1078
+ }
1079
+ case "gem": {
1080
+ const v1Url =
1081
+ process.env.RUBYGEMS_V1_URL || "https://rubygems.org/api/v1/gems/";
1082
+ const v2Url =
1083
+ process.env.RUBYGEMS_V2_URL ||
1084
+ "https://rubygems.org/api/v2/rubygems/";
1085
+ registry = packageVersion ? v2Url : v1Url;
1086
+ const endpoint = packageVersion
1087
+ ? `${v2Url}${purlObj.name}/versions/${packageVersion}.json`
1088
+ : `${v1Url}${purlObj.name}.json`;
1089
+ const res = await cdxgenAgent.get(endpoint, {
1090
+ responseType: "json",
1091
+ });
1092
+ const body = Array.isArray(res.body) ? res.body[0] : res.body;
1093
+ repoUrl =
1094
+ normalizeRepositoryUrl(body?.metadata?.source_code_uri) ||
1095
+ normalizeRepositoryUrl(body?.source_code_uri) ||
1096
+ normalizeRepositoryUrl(body?.homepage_uri);
1097
+ break;
1098
+ }
1099
+ case "cargo": {
1100
+ registry =
1101
+ process.env.RUST_CRATES_URL || "https://crates.io/api/v1/crates/";
1102
+ const res = await cdxgenAgent.get(`${registry}${purlObj.name}`, {
1103
+ responseType: "json",
1104
+ });
1105
+ repoUrl = normalizeRepositoryUrl(res.body?.crate?.repository);
1106
+ break;
1107
+ }
1108
+ case "pub": {
1109
+ registry = process.env.PUB_DEV_URL || "https://pub.dev";
1110
+ const endpoint = packageVersion
1111
+ ? `${registry}/api/packages/${purlObj.name}/versions/${packageVersion}`
1112
+ : `${registry}/api/packages/${purlObj.name}`;
1113
+ const res = await cdxgenAgent.get(endpoint, {
1114
+ responseType: "json",
1115
+ headers: {
1116
+ Accept: "application/vnd.pub.v2+json",
1117
+ },
1118
+ });
1119
+ const pubspec = res.body?.pubspec || res.body?.latest?.pubspec || {};
1120
+ repoUrl =
1121
+ normalizeRepositoryUrl(pubspec.repository) ||
1122
+ normalizeRepositoryUrl(pubspec.homepage);
1123
+ break;
1124
+ }
1125
+ case "github": {
1126
+ registry = "https://github.com";
1127
+ if (purlObj.namespace) {
1128
+ repoUrl = normalizeRepositoryUrl(
1129
+ `${registry}/${purlObj.namespace}/${purlObj.name}`,
1130
+ );
1131
+ }
1132
+ break;
1133
+ }
1134
+ case "bitbucket": {
1135
+ registry = "https://bitbucket.org";
1136
+ if (purlObj.namespace) {
1137
+ repoUrl = normalizeRepositoryUrl(
1138
+ `${registry}/${purlObj.namespace}/${purlObj.name}`,
1139
+ );
1140
+ }
1141
+ break;
1142
+ }
1143
+ case "maven": {
1144
+ if (!purlObj.namespace || !packageVersion) {
1145
+ break;
1146
+ }
1147
+ const mavenCentralUrl =
1148
+ process.env.MAVEN_CENTRAL_URL || "https://repo1.maven.org/maven2/";
1149
+ registry = mavenCentralUrl.endsWith("/")
1150
+ ? mavenCentralUrl
1151
+ : `${mavenCentralUrl}/`;
1152
+ const pomJson = await fetchPomXmlAsJson({
1153
+ urlPrefix: registry,
1154
+ group: purlObj.namespace,
1155
+ name: purlObj.name,
1156
+ version: packageVersion,
1157
+ });
1158
+ repoUrl =
1159
+ normalizeRepositoryUrl(pomJson?.scm?.url?._) ||
1160
+ normalizeRepositoryUrl(pomJson?.scm?.connection?._) ||
1161
+ normalizeRepositoryUrl(pomJson?.scm?.developerConnection?._);
1162
+ break;
1163
+ }
1164
+ case "composer": {
1165
+ const packagistUrl =
1166
+ process.env.PACKAGIST_URL || "https://repo.packagist.org/p2/";
1167
+ registry = packagistUrl.endsWith("/")
1168
+ ? packagistUrl
1169
+ : `${packagistUrl}/`;
1170
+ const endpoint = `${registry}${packageName}.json`;
1171
+ const res = await cdxgenAgent.get(endpoint, {
1172
+ responseType: "json",
1173
+ });
1174
+ const packageVersions = res.body?.packages?.[packageName];
1175
+ if (!Array.isArray(packageVersions) || !packageVersions.length) {
1176
+ break;
1177
+ }
1178
+ const selectedVersion = packageVersion
1179
+ ? packageVersions.find(
1180
+ (v) =>
1181
+ v?.version === packageVersion ||
1182
+ v?.version_normalized === packageVersion,
1183
+ )
1184
+ : packageVersions[0];
1185
+ repoUrl =
1186
+ normalizeRepositoryUrl(selectedVersion?.source?.url) ||
1187
+ normalizeRepositoryUrl(selectedVersion?.homepage);
1188
+ break;
1189
+ }
1190
+ case "generic": {
1191
+ const genericVcsUrl = purlObj.qualifiers?.vcs_url;
1192
+ const genericDownloadUrl = purlObj.qualifiers?.download_url;
1193
+ repoUrl =
1194
+ normalizeRepositoryUrl(genericVcsUrl) ||
1195
+ normalizeDownloadRepositoryUrl(genericDownloadUrl);
1196
+ break;
1197
+ }
1198
+ default:
1199
+ return undefined;
1200
+ }
1201
+ } catch (err) {
1202
+ logPurlResolutionError(err);
1203
+ return undefined;
1204
+ }
1205
+
1206
+ if (!repoUrl) {
1207
+ return undefined;
1208
+ }
1209
+ if (!maybeRemotePath(repoUrl)) {
1210
+ if (DEBUG_MODE) {
1211
+ console.log(
1212
+ `Ignoring non-remote repository URL '${repoUrl}' from purl lookup.`,
1213
+ );
1214
+ }
1215
+ return undefined;
1216
+ }
1217
+
1218
+ return {
1219
+ type: purlObj.type,
1220
+ registry,
1221
+ repoUrl,
1222
+ version: purlObj.version,
1223
+ namespace: purlObj.namespace,
1224
+ name: purlObj.name,
1225
+ };
1226
+ }
1227
+
1228
+ /**
1229
+ * Clean up cloned source directories.
1230
+ *
1231
+ * @param {string} srcDir directory path to remove
1232
+ */
1233
+ export function cleanupSourceDir(srcDir) {
1234
+ const normalizePath = (candidatePath) => {
1235
+ if (!candidatePath) {
1236
+ return undefined;
1237
+ }
1238
+ try {
1239
+ return fs.realpathSync.native
1240
+ ? fs.realpathSync.native(candidatePath)
1241
+ : fs.realpathSync(candidatePath);
1242
+ } catch {
1243
+ return path.resolve(candidatePath);
1244
+ }
1245
+ };
1246
+ const normalizedSrcDir = normalizePath(srcDir);
1247
+ const tempRoots = [getTmpDir()];
1248
+ if (process.platform !== "win32") {
1249
+ tempRoots.push("/tmp");
1250
+ tempRoots.push("/private/tmp");
1251
+ }
1252
+ const normalizedTmpDirs = tempRoots
1253
+ .map((candidatePath) => normalizePath(candidatePath))
1254
+ .filter(Boolean);
1255
+ if (
1256
+ normalizedSrcDir &&
1257
+ normalizedTmpDirs.some(
1258
+ (normalizedTmpDir) =>
1259
+ normalizedSrcDir === normalizedTmpDir ||
1260
+ normalizedSrcDir.startsWith(`${normalizedTmpDir}${path.sep}`),
1261
+ ) &&
1262
+ fs.rmSync
1263
+ ) {
1264
+ thoughtLog(`Cleaning up ${srcDir}`);
1265
+ fs.rmSync(srcDir, { recursive: true, force: true });
1266
+ }
1267
+ }