@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.
- package/README.md +242 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +532 -168
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +276 -68
- package/lib/cli/index.poku.js +368 -0
- package/lib/helpers/analyzer.js +1052 -5
- package/lib/helpers/analyzer.poku.js +301 -0
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/depsUtils.js +16 -0
- package/lib/helpers/depsUtils.poku.js +58 -1
- package/lib/helpers/display.js +245 -61
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +882 -136
- package/lib/helpers/utils.poku.js +995 -91
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +50 -0
- package/lib/server/server.js +228 -331
- package/lib/server/server.poku.js +220 -5
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +20 -5
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +13 -2
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -8
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +30 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -35
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- 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
|
+
}
|