verimu 0.0.9 → 0.0.13
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 +9 -6
- package/dist/{cli.mjs → cli.js} +1295 -65
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1503 -182
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +415 -24
- package/dist/index.d.ts +415 -24
- package/dist/index.mjs +1495 -182
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -3
- package/dist/cli.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,63 @@
|
|
|
1
1
|
// src/generate-sbom.ts
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/sbom/shared.ts
|
|
5
|
+
var VERIMU_TOOL_NAME = "verimu";
|
|
6
|
+
var VERIMU_TOOL_WEBSITE = "https://verimu.com";
|
|
7
|
+
var VERIMU_TOOL_DESCRIPTION = "Verimu CRA Compliance Scanner";
|
|
8
|
+
var DEFAULT_TOOL_VERSION = "0.1.0";
|
|
9
|
+
var DEFAULT_PROJECT_VERSION = "0.0.0";
|
|
10
|
+
var DEFAULT_SWID_VERSION = "0.0.0";
|
|
11
|
+
var PURL_TYPE_MAP = {
|
|
12
|
+
npm: "npm",
|
|
13
|
+
nuget: "nuget",
|
|
14
|
+
cargo: "cargo",
|
|
15
|
+
maven: "maven",
|
|
16
|
+
pip: "pypi",
|
|
17
|
+
poetry: "pypi",
|
|
18
|
+
uv: "pypi",
|
|
19
|
+
go: "golang",
|
|
20
|
+
ruby: "gem",
|
|
21
|
+
composer: "composer",
|
|
22
|
+
deno: "deno"
|
|
23
|
+
};
|
|
24
|
+
function buildPurl(name, version, ecosystem) {
|
|
25
|
+
const type = PURL_TYPE_MAP[ecosystem] || ecosystem;
|
|
26
|
+
if (ecosystem === "npm" && name.startsWith("@")) {
|
|
27
|
+
return `pkg:${type}/%40${name.slice(1)}@${version}`;
|
|
28
|
+
}
|
|
29
|
+
return `pkg:${type}/${name}@${version}`;
|
|
30
|
+
}
|
|
31
|
+
function deriveSupplierName(packageName) {
|
|
32
|
+
if (packageName.startsWith("@")) {
|
|
33
|
+
return packageName.split("/")[0];
|
|
34
|
+
}
|
|
35
|
+
return packageName;
|
|
36
|
+
}
|
|
37
|
+
function extractProjectName(projectPath) {
|
|
38
|
+
const parts = projectPath.replace(/\\/g, "/").split("/");
|
|
39
|
+
return parts[parts.length - 1] || "unknown-project";
|
|
40
|
+
}
|
|
41
|
+
function normalizeDependencies(dependencies) {
|
|
42
|
+
return dependencies.map((dep) => ({
|
|
43
|
+
name: dep.name,
|
|
44
|
+
version: dep.version,
|
|
45
|
+
ecosystem: dep.ecosystem,
|
|
46
|
+
direct: dep.direct ?? true,
|
|
47
|
+
purl: dep.purl ?? buildPurl(dep.name, dep.version, dep.ecosystem),
|
|
48
|
+
supplierName: deriveSupplierName(dep.name)
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/generate-sbom.ts
|
|
3
53
|
function generateSbom(input) {
|
|
4
54
|
const {
|
|
5
55
|
projectName,
|
|
6
|
-
projectVersion =
|
|
56
|
+
projectVersion = DEFAULT_PROJECT_VERSION,
|
|
7
57
|
dependencies
|
|
8
58
|
} = input;
|
|
9
59
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10
|
-
const resolvedDeps = dependencies
|
|
11
|
-
...dep,
|
|
12
|
-
direct: dep.direct ?? true,
|
|
13
|
-
purl: dep.purl ?? buildPurl(dep.name, dep.version, dep.ecosystem)
|
|
14
|
-
}));
|
|
60
|
+
const resolvedDeps = normalizeDependencies(dependencies);
|
|
15
61
|
const rootPurl = buildPurl(projectName, projectVersion, "npm");
|
|
16
62
|
const sbom = {
|
|
17
63
|
$schema: "http://cyclonedx.org/schema/bom-1.7.schema.json",
|
|
@@ -25,12 +71,12 @@ function generateSbom(input) {
|
|
|
25
71
|
components: [
|
|
26
72
|
{
|
|
27
73
|
type: "application",
|
|
28
|
-
name:
|
|
74
|
+
name: VERIMU_TOOL_NAME,
|
|
29
75
|
version: "0.0.1",
|
|
30
|
-
description:
|
|
76
|
+
description: VERIMU_TOOL_DESCRIPTION,
|
|
31
77
|
supplier: { name: "Verimu" },
|
|
32
78
|
externalReferences: [
|
|
33
|
-
{ type: "website", url:
|
|
79
|
+
{ type: "website", url: VERIMU_TOOL_WEBSITE }
|
|
34
80
|
]
|
|
35
81
|
}
|
|
36
82
|
]
|
|
@@ -51,7 +97,7 @@ function generateSbom(input) {
|
|
|
51
97
|
purl: dep.purl,
|
|
52
98
|
"bom-ref": dep.purl,
|
|
53
99
|
scope: dep.direct ? "required" : "optional",
|
|
54
|
-
supplier: { name:
|
|
100
|
+
supplier: { name: dep.supplierName }
|
|
55
101
|
})),
|
|
56
102
|
dependencies: [
|
|
57
103
|
{
|
|
@@ -69,33 +115,130 @@ function generateSbom(input) {
|
|
|
69
115
|
generatedAt: timestamp
|
|
70
116
|
};
|
|
71
117
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
|
|
119
|
+
// src/generate-spdx.ts
|
|
120
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
121
|
+
var SPDX_VERSION = "2.3";
|
|
122
|
+
function generateSpdxSbom(input) {
|
|
123
|
+
const {
|
|
124
|
+
projectName,
|
|
125
|
+
projectVersion = DEFAULT_PROJECT_VERSION,
|
|
126
|
+
dependencies
|
|
127
|
+
} = input;
|
|
128
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
129
|
+
const resolvedDeps = normalizeDependencies(dependencies);
|
|
130
|
+
const rootPackageId = "SPDXRef-Package-root";
|
|
131
|
+
const spdx = {
|
|
132
|
+
spdxVersion: `SPDX-${SPDX_VERSION}`,
|
|
133
|
+
dataLicense: "CC0-1.0",
|
|
134
|
+
SPDXID: "SPDXRef-DOCUMENT",
|
|
135
|
+
name: `${projectName}-sbom`,
|
|
136
|
+
documentNamespace: `https://verimu.com/spdxdocs/${projectName}-${randomUUID2()}`,
|
|
137
|
+
creationInfo: {
|
|
138
|
+
created: timestamp,
|
|
139
|
+
creators: [`Tool: ${VERIMU_TOOL_NAME}`]
|
|
140
|
+
},
|
|
141
|
+
documentDescribes: [rootPackageId],
|
|
142
|
+
packages: [
|
|
143
|
+
{
|
|
144
|
+
name: projectName,
|
|
145
|
+
SPDXID: rootPackageId,
|
|
146
|
+
versionInfo: projectVersion,
|
|
147
|
+
supplier: `Organization: ${projectName}`,
|
|
148
|
+
downloadLocation: "NOASSERTION",
|
|
149
|
+
filesAnalyzed: false,
|
|
150
|
+
licenseConcluded: "NOASSERTION",
|
|
151
|
+
licenseDeclared: "NOASSERTION",
|
|
152
|
+
primaryPackagePurpose: "APPLICATION"
|
|
153
|
+
},
|
|
154
|
+
...resolvedDeps.map((dep, index) => ({
|
|
155
|
+
name: dep.name,
|
|
156
|
+
SPDXID: `SPDXRef-Package-${index + 1}`,
|
|
157
|
+
versionInfo: dep.version,
|
|
158
|
+
supplier: `Organization: ${dep.supplierName}`,
|
|
159
|
+
downloadLocation: "NOASSERTION",
|
|
160
|
+
filesAnalyzed: false,
|
|
161
|
+
licenseConcluded: "NOASSERTION",
|
|
162
|
+
licenseDeclared: "NOASSERTION",
|
|
163
|
+
primaryPackagePurpose: "LIBRARY",
|
|
164
|
+
externalRefs: [
|
|
165
|
+
{
|
|
166
|
+
referenceCategory: "PACKAGE-MANAGER",
|
|
167
|
+
referenceType: "purl",
|
|
168
|
+
referenceLocator: dep.purl
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}))
|
|
172
|
+
],
|
|
173
|
+
relationships: [
|
|
174
|
+
{
|
|
175
|
+
spdxElementId: "SPDXRef-DOCUMENT",
|
|
176
|
+
relationshipType: "DESCRIBES",
|
|
177
|
+
relatedSpdxElement: rootPackageId
|
|
178
|
+
},
|
|
179
|
+
...resolvedDeps.map((_dep, index) => ({
|
|
180
|
+
spdxElementId: rootPackageId,
|
|
181
|
+
relationshipType: "DEPENDS_ON",
|
|
182
|
+
relatedSpdxElement: `SPDXRef-Package-${index + 1}`
|
|
183
|
+
}))
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
const content = JSON.stringify(spdx, null, 2);
|
|
187
|
+
return {
|
|
188
|
+
sbom: spdx,
|
|
189
|
+
content,
|
|
190
|
+
componentCount: resolvedDeps.length,
|
|
191
|
+
specVersion: SPDX_VERSION,
|
|
192
|
+
generatedAt: timestamp
|
|
193
|
+
};
|
|
88
194
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
195
|
+
|
|
196
|
+
// src/generate-swid.ts
|
|
197
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
198
|
+
var SWID_SPEC_VERSION = "ISO/IEC 19770-2:2015";
|
|
199
|
+
function generateSwidTag(input) {
|
|
200
|
+
const {
|
|
201
|
+
projectName,
|
|
202
|
+
projectVersion = DEFAULT_PROJECT_VERSION
|
|
203
|
+
} = input;
|
|
204
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
205
|
+
const tagVersion = 1;
|
|
206
|
+
const normalizedVersion = projectVersion || DEFAULT_SWID_VERSION;
|
|
207
|
+
const tagId = `com.verimu:${sanitizeTagId(projectName)}:${normalizedVersion}:${randomUUID3()}`;
|
|
208
|
+
const tag = [
|
|
209
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
210
|
+
"<SoftwareIdentity",
|
|
211
|
+
' xmlns="http://standards.iso.org/iso/19770/-2/2015/schema.xsd"',
|
|
212
|
+
` name="${escapeXml(projectName)}"`,
|
|
213
|
+
` tagId="${escapeXml(tagId)}"`,
|
|
214
|
+
` tagVersion="${tagVersion}"`,
|
|
215
|
+
` version="${escapeXml(normalizedVersion)}"`,
|
|
216
|
+
' versionScheme="semver">',
|
|
217
|
+
` <Entity name="${escapeXml(projectName)}" role="softwareCreator" />`,
|
|
218
|
+
' <Entity name="Verimu" role="tagCreator" />',
|
|
219
|
+
` <Meta product="${escapeXml(projectName)}" generator="${VERIMU_TOOL_NAME}" generated="${timestamp}" />`,
|
|
220
|
+
" <!-- TODO: Consider adding dependency/package evidence if we need richer SWID coverage. -->",
|
|
221
|
+
' <Link rel="describedby" href="https://verimu.com" />',
|
|
222
|
+
"</SoftwareIdentity>"
|
|
223
|
+
].join("\n");
|
|
224
|
+
return {
|
|
225
|
+
tag,
|
|
226
|
+
content: tag,
|
|
227
|
+
componentCount: 1,
|
|
228
|
+
specVersion: SWID_SPEC_VERSION,
|
|
229
|
+
generatedAt: timestamp
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function escapeXml(value) {
|
|
233
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
234
|
+
}
|
|
235
|
+
function sanitizeTagId(value) {
|
|
236
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
94
237
|
}
|
|
95
238
|
|
|
96
239
|
// src/scan.ts
|
|
97
240
|
import { writeFile } from "fs/promises";
|
|
98
|
-
import { basename } from "path";
|
|
241
|
+
import { basename, join, parse } from "path";
|
|
99
242
|
|
|
100
243
|
// src/scanners/npm/npm-scanner.ts
|
|
101
244
|
import { readFile } from "fs/promises";
|
|
@@ -113,7 +256,7 @@ var VerimuError = class extends Error {
|
|
|
113
256
|
var NoLockfileError = class extends VerimuError {
|
|
114
257
|
constructor(projectPath) {
|
|
115
258
|
super(
|
|
116
|
-
`No supported lockfile found in ${projectPath}. Supported: package-lock.json (npm), packages.lock.json (NuGet), Cargo.lock (Rust), requirements.txt / Pipfile.lock (
|
|
259
|
+
`No supported lockfile found in ${projectPath}. Supported: package-lock.json (npm), packages.lock.json (NuGet), Cargo.lock (Rust), requirements.txt / Pipfile.lock (pip), poetry.lock (Poetry), uv.lock (uv), pom.xml (Maven), go.sum (Go), Gemfile.lock (Ruby), composer.lock (Composer), yarn.lock (Yarn), pnpm-lock.yaml (pnpm)`,
|
|
117
260
|
"NO_LOCKFILE"
|
|
118
261
|
);
|
|
119
262
|
this.name = "NoLockfileError";
|
|
@@ -1030,161 +1173,1280 @@ var ComposerScanner = class {
|
|
|
1030
1173
|
}
|
|
1031
1174
|
};
|
|
1032
1175
|
|
|
1033
|
-
// src/scanners/
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
]
|
|
1176
|
+
// src/scanners/yarn/yarn-scanner.ts
|
|
1177
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1178
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1179
|
+
import path9 from "path";
|
|
1180
|
+
import { parse as parseYaml } from "yaml";
|
|
1181
|
+
var YarnScanner = class {
|
|
1182
|
+
ecosystem = "npm";
|
|
1183
|
+
lockfileNames = ["yarn.lock"];
|
|
1184
|
+
async detect(projectPath) {
|
|
1185
|
+
const lockfilePath = path9.join(projectPath, "yarn.lock");
|
|
1186
|
+
return existsSync9(lockfilePath) ? lockfilePath : null;
|
|
1187
|
+
}
|
|
1188
|
+
async scan(projectPath, lockfilePath) {
|
|
1189
|
+
const [lockfileRaw, packageJsonRaw] = await Promise.all([
|
|
1190
|
+
readFile9(lockfilePath, "utf-8"),
|
|
1191
|
+
readFile9(path9.join(projectPath, "package.json"), "utf-8").catch(() => null)
|
|
1192
|
+
]);
|
|
1193
|
+
const directNames = /* @__PURE__ */ new Set();
|
|
1194
|
+
if (packageJsonRaw) {
|
|
1195
|
+
try {
|
|
1196
|
+
const pkg = JSON.parse(packageJsonRaw);
|
|
1197
|
+
for (const name of Object.keys(pkg.dependencies ?? {})) {
|
|
1198
|
+
directNames.add(name);
|
|
1199
|
+
}
|
|
1200
|
+
for (const name of Object.keys(pkg.devDependencies ?? {})) {
|
|
1201
|
+
directNames.add(name);
|
|
1202
|
+
}
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const dependencies = this.parseLockfile(lockfileRaw, lockfilePath, directNames);
|
|
1207
|
+
return {
|
|
1208
|
+
projectPath,
|
|
1209
|
+
ecosystem: "npm",
|
|
1210
|
+
dependencies,
|
|
1211
|
+
lockfilePath,
|
|
1212
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1213
|
+
};
|
|
1047
1214
|
}
|
|
1048
1215
|
/**
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1216
|
+
* Parses yarn.lock file and extracts dependencies.
|
|
1217
|
+
* Automatically detects and handles both v1 (Classic) and v2+ (Berry) formats.
|
|
1051
1218
|
*/
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
if (
|
|
1056
|
-
return
|
|
1219
|
+
parseLockfile(content, lockfilePath, directNames) {
|
|
1220
|
+
try {
|
|
1221
|
+
const isV2Plus = this.isYarnV2Plus(content);
|
|
1222
|
+
if (isV2Plus) {
|
|
1223
|
+
return this.parseLockfileV2Plus(content, lockfilePath, directNames);
|
|
1224
|
+
} else {
|
|
1225
|
+
return this.parseLockfileV1(content, lockfilePath, directNames);
|
|
1057
1226
|
}
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
throw new LockfileParseError(
|
|
1229
|
+
lockfilePath,
|
|
1230
|
+
`Failed to parse yarn.lock: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
1231
|
+
);
|
|
1058
1232
|
}
|
|
1059
|
-
throw new NoLockfileError(projectPath);
|
|
1060
|
-
}
|
|
1061
|
-
/** Returns a specific scanner by ecosystem name */
|
|
1062
|
-
getScanner(ecosystem) {
|
|
1063
|
-
return this.scanners.find((s) => s.ecosystem === ecosystem);
|
|
1064
|
-
}
|
|
1065
|
-
/** Lists all registered ecosystems */
|
|
1066
|
-
listEcosystems() {
|
|
1067
|
-
return this.scanners.map((s) => s.ecosystem);
|
|
1068
1233
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
generate(scanResult, toolVersion = "0.1.0") {
|
|
1076
|
-
const bom = this.buildBom(scanResult, toolVersion);
|
|
1077
|
-
const content = JSON.stringify(bom, null, 2);
|
|
1078
|
-
return {
|
|
1079
|
-
format: "cyclonedx-json",
|
|
1080
|
-
specVersion: "1.7",
|
|
1081
|
-
content,
|
|
1082
|
-
componentCount: scanResult.dependencies.length,
|
|
1083
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1084
|
-
};
|
|
1234
|
+
/**
|
|
1235
|
+
* Detects if the lockfile is Yarn v2+ (Berry) format.
|
|
1236
|
+
* v2+ uses YAML format and contains __metadata section.
|
|
1237
|
+
*/
|
|
1238
|
+
isYarnV2Plus(content) {
|
|
1239
|
+
return content.startsWith("__metadata:") || content.includes("\n__metadata:");
|
|
1085
1240
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
name: projectName
|
|
1116
|
-
},
|
|
1117
|
-
component: {
|
|
1118
|
-
type: "application",
|
|
1119
|
-
name: projectName,
|
|
1120
|
-
"bom-ref": "root-component",
|
|
1121
|
-
supplier: { name: projectName }
|
|
1241
|
+
/**
|
|
1242
|
+
* Parses Yarn v2+ (Berry) lockfile format.
|
|
1243
|
+
*
|
|
1244
|
+
* Yarn v2+ format (YAML):
|
|
1245
|
+
* ```yaml
|
|
1246
|
+
* __metadata:
|
|
1247
|
+
* version: 6
|
|
1248
|
+
*
|
|
1249
|
+
* "package-name@npm:^1.0.0":
|
|
1250
|
+
* version: 1.2.3
|
|
1251
|
+
* resolution: "package-name@npm:1.2.3"
|
|
1252
|
+
* dependencies:
|
|
1253
|
+
* dep1: ^2.0.0
|
|
1254
|
+
* checksum: ...
|
|
1255
|
+
* languageName: node
|
|
1256
|
+
* linkType: hard
|
|
1257
|
+
* ```
|
|
1258
|
+
*/
|
|
1259
|
+
parseLockfileV2Plus(content, lockfilePath, directNames) {
|
|
1260
|
+
const deps = [];
|
|
1261
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1262
|
+
try {
|
|
1263
|
+
const parsed = parseYaml(content);
|
|
1264
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1265
|
+
throw new Error("Invalid YAML format");
|
|
1266
|
+
}
|
|
1267
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1268
|
+
if (key === "__metadata" || key.includes("@workspace:")) {
|
|
1269
|
+
continue;
|
|
1122
1270
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1271
|
+
if (typeof value !== "object" || value === null) {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
const entry = value;
|
|
1275
|
+
let name = null;
|
|
1276
|
+
if (entry.resolution && typeof entry.resolution === "string") {
|
|
1277
|
+
name = this.extractPackageNameFromResolution(entry.resolution);
|
|
1278
|
+
}
|
|
1279
|
+
if (!name) {
|
|
1280
|
+
name = this.extractPackageNameV2Plus(key);
|
|
1281
|
+
}
|
|
1282
|
+
const version = entry.version;
|
|
1283
|
+
if (!name || !version || typeof version !== "string") {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const depKey = `${name}@${version}`;
|
|
1287
|
+
if (seen.has(depKey)) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
seen.set(depKey, true);
|
|
1291
|
+
deps.push({
|
|
1292
|
+
name,
|
|
1293
|
+
version,
|
|
1294
|
+
direct: directNames.has(name),
|
|
1295
|
+
ecosystem: "npm",
|
|
1296
|
+
purl: this.buildPurl(name, version)
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
throw new Error(`Failed to parse Yarn v2+ lockfile: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1301
|
+
}
|
|
1302
|
+
return deps;
|
|
1127
1303
|
}
|
|
1128
|
-
/**
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* Extracts package name from Yarn v2+ resolution field.
|
|
1306
|
+
* The resolution field contains the real package name.
|
|
1307
|
+
* Examples:
|
|
1308
|
+
* "express@npm:4.18.2" → "express"
|
|
1309
|
+
* "@types/node@npm:20.11.5" → "@types/node"
|
|
1310
|
+
* "lodash@npm:4.17.21" → "lodash"
|
|
1311
|
+
*/
|
|
1312
|
+
extractPackageNameFromResolution(resolution) {
|
|
1313
|
+
if (resolution.startsWith("@")) {
|
|
1314
|
+
const match2 = resolution.match(/^(@[^@]+\/[^@]+)@/);
|
|
1315
|
+
if (match2) {
|
|
1316
|
+
return match2[1];
|
|
1140
1317
|
}
|
|
1141
|
-
}
|
|
1318
|
+
}
|
|
1319
|
+
const match = resolution.match(/^([^@]+)@/);
|
|
1320
|
+
if (match) {
|
|
1321
|
+
return match[1];
|
|
1322
|
+
}
|
|
1323
|
+
return null;
|
|
1142
1324
|
}
|
|
1143
1325
|
/**
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1147
|
-
*
|
|
1148
|
-
*
|
|
1149
|
-
* This is the same heuristic used by Syft, Trivy, and other SBOM tools
|
|
1150
|
-
* when registry metadata (author/publisher) isn't available from the lockfile.
|
|
1326
|
+
* Extracts package name from Yarn v2+ package key.
|
|
1327
|
+
* Examples:
|
|
1328
|
+
* "express@npm:^4.18.0" → "express"
|
|
1329
|
+
* "@types/node@npm:^20.0.0" → "@types/node"
|
|
1330
|
+
* "pkg@npm:other@npm:^1.0.0" → "pkg" (aliased packages)
|
|
1151
1331
|
*/
|
|
1152
|
-
|
|
1153
|
-
if (
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1332
|
+
extractPackageNameV2Plus(key) {
|
|
1333
|
+
if (key.startsWith("@")) {
|
|
1334
|
+
const match2 = key.match(/^(@[^@]+\/[^@]+)@/);
|
|
1335
|
+
if (match2) {
|
|
1336
|
+
return match2[1];
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const match = key.match(/^([^@]+)@/);
|
|
1340
|
+
if (match) {
|
|
1341
|
+
return match[1];
|
|
1156
1342
|
}
|
|
1157
|
-
return
|
|
1343
|
+
return null;
|
|
1158
1344
|
}
|
|
1159
1345
|
/**
|
|
1160
|
-
*
|
|
1346
|
+
* Parses Yarn v1 (Classic) lockfile format.
|
|
1161
1347
|
*
|
|
1162
|
-
*
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1348
|
+
* Yarn v1 format:
|
|
1349
|
+
* ```
|
|
1350
|
+
* "package-name@^1.0.0":
|
|
1351
|
+
* version "1.2.3"
|
|
1352
|
+
* resolved "https://..."
|
|
1353
|
+
* integrity sha512-...
|
|
1354
|
+
* dependencies:
|
|
1355
|
+
* dep1 "^2.0.0"
|
|
1356
|
+
* ```
|
|
1169
1357
|
*/
|
|
1170
|
-
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1358
|
+
parseLockfileV1(content, lockfilePath, directNames) {
|
|
1359
|
+
const deps = [];
|
|
1360
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1361
|
+
const lines = content.split("\n");
|
|
1362
|
+
let currentPackage = null;
|
|
1363
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1364
|
+
const line = lines[i];
|
|
1365
|
+
if (line.trim().startsWith("#") || line.trim() === "") {
|
|
1366
|
+
continue;
|
|
1176
1367
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1368
|
+
if (line.match(/^["\w@]/) && line.includes(":") && !line.startsWith(" ")) {
|
|
1369
|
+
if (currentPackage?.version) {
|
|
1370
|
+
this.addDependency(currentPackage, directNames, seen, deps);
|
|
1371
|
+
}
|
|
1372
|
+
const pkgLine = line.substring(0, line.lastIndexOf(":"));
|
|
1373
|
+
const names = pkgLine.split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).map((s) => this.extractPackageName(s)).filter((s) => !!s);
|
|
1374
|
+
currentPackage = { names, version: void 0 };
|
|
1375
|
+
} else if (line.trim().startsWith("version ") && currentPackage) {
|
|
1376
|
+
const match = line.match(/version\s+"([^"]+)"/);
|
|
1377
|
+
if (match) {
|
|
1378
|
+
currentPackage.version = match[1];
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (currentPackage?.version) {
|
|
1383
|
+
this.addDependency(currentPackage, directNames, seen, deps);
|
|
1384
|
+
}
|
|
1385
|
+
return deps;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Adds a dependency to the result list (deduplicates by name@version)
|
|
1389
|
+
*/
|
|
1390
|
+
addDependency(pkg, directNames, seen, deps) {
|
|
1391
|
+
if (!pkg.version) return;
|
|
1392
|
+
const name = pkg.names[0];
|
|
1393
|
+
if (!name) return;
|
|
1394
|
+
const key = `${name}@${pkg.version}`;
|
|
1395
|
+
if (seen.has(key)) return;
|
|
1396
|
+
seen.set(key, true);
|
|
1397
|
+
deps.push({
|
|
1398
|
+
name,
|
|
1399
|
+
version: pkg.version,
|
|
1400
|
+
direct: directNames.has(name),
|
|
1401
|
+
ecosystem: "npm",
|
|
1402
|
+
purl: this.buildPurl(name, pkg.version)
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Extracts package name from yarn.lock package declaration.
|
|
1407
|
+
* Examples:
|
|
1408
|
+
* "express@^4.18.0" → "express"
|
|
1409
|
+
* "@types/node@^20.0.0" → "@types/node"
|
|
1410
|
+
* "pkg@npm:other@^1.0.0" → "pkg" (aliased packages)
|
|
1411
|
+
*/
|
|
1412
|
+
extractPackageName(pkgDeclaration) {
|
|
1413
|
+
if (pkgDeclaration.includes("@npm:")) {
|
|
1414
|
+
const beforeAlias = pkgDeclaration.split("@npm:")[0];
|
|
1415
|
+
return beforeAlias || null;
|
|
1416
|
+
}
|
|
1417
|
+
if (pkgDeclaration.startsWith("@")) {
|
|
1418
|
+
const parts = pkgDeclaration.split("@");
|
|
1419
|
+
if (parts.length >= 3) {
|
|
1420
|
+
return `@${parts[1]}`;
|
|
1421
|
+
}
|
|
1422
|
+
} else {
|
|
1423
|
+
const atIndex = pkgDeclaration.indexOf("@");
|
|
1424
|
+
if (atIndex > 0) {
|
|
1425
|
+
return pkgDeclaration.substring(0, atIndex);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Builds a purl (Package URL) for an npm package.
|
|
1432
|
+
*
|
|
1433
|
+
* Per the purl spec:
|
|
1434
|
+
* "The npm scope @ sign prefix is always percent encoded."
|
|
1435
|
+
*
|
|
1436
|
+
* So @types/node@20.11.5 → pkg:npm/%40types/node@20.11.5
|
|
1437
|
+
* And express@4.18.2 → pkg:npm/express@4.18.2
|
|
1438
|
+
*/
|
|
1439
|
+
buildPurl(name, version) {
|
|
1440
|
+
if (name.startsWith("@")) {
|
|
1441
|
+
return `pkg:npm/%40${name.slice(1)}@${version}`;
|
|
1442
|
+
}
|
|
1443
|
+
return `pkg:npm/${name}@${version}`;
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
// src/scanners/pnpm/pnpm-scanner.ts
|
|
1448
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1449
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1450
|
+
import path10 from "path";
|
|
1451
|
+
import { parse as parseYaml2 } from "yaml";
|
|
1452
|
+
var PnpmScanner = class {
|
|
1453
|
+
ecosystem = "npm";
|
|
1454
|
+
lockfileNames = ["pnpm-lock.yaml"];
|
|
1455
|
+
async detect(projectPath) {
|
|
1456
|
+
const lockfilePath = path10.join(projectPath, "pnpm-lock.yaml");
|
|
1457
|
+
return existsSync10(lockfilePath) ? lockfilePath : null;
|
|
1458
|
+
}
|
|
1459
|
+
async scan(projectPath, lockfilePath) {
|
|
1460
|
+
const lockfileRaw = await readFile10(lockfilePath, "utf-8");
|
|
1461
|
+
const dependencies = this.parseLockfile(lockfileRaw, lockfilePath);
|
|
1462
|
+
return {
|
|
1463
|
+
projectPath,
|
|
1464
|
+
ecosystem: "npm",
|
|
1465
|
+
dependencies,
|
|
1466
|
+
lockfilePath,
|
|
1467
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Parses pnpm-lock.yaml file and extracts dependencies.
|
|
1472
|
+
*
|
|
1473
|
+
* pnpm-lock.yaml format (v5.4+):
|
|
1474
|
+
* ```yaml
|
|
1475
|
+
* lockfileVersion: 5.4
|
|
1476
|
+
*
|
|
1477
|
+
* dependencies:
|
|
1478
|
+
* express: 4.18.2
|
|
1479
|
+
*
|
|
1480
|
+
* devDependencies:
|
|
1481
|
+
* typescript: 5.0.0
|
|
1482
|
+
*
|
|
1483
|
+
* packages:
|
|
1484
|
+
* /express/4.18.2:
|
|
1485
|
+
* resolution: {integrity: sha512-...}
|
|
1486
|
+
* dependencies:
|
|
1487
|
+
* accepts: 1.3.8
|
|
1488
|
+
* /@types/node/20.11.5:
|
|
1489
|
+
* resolution: {integrity: sha512-...}
|
|
1490
|
+
* dev: true
|
|
1491
|
+
* ```
|
|
1492
|
+
*
|
|
1493
|
+
* pnpm-lock.yaml format (v6.0+):
|
|
1494
|
+
* ```yaml
|
|
1495
|
+
* lockfileVersion: '6.0'
|
|
1496
|
+
*
|
|
1497
|
+
* dependencies:
|
|
1498
|
+
* express:
|
|
1499
|
+
* specifier: ^4.18.0
|
|
1500
|
+
* version: 4.18.2
|
|
1501
|
+
*
|
|
1502
|
+
* packages:
|
|
1503
|
+
* /express@4.18.2:
|
|
1504
|
+
* resolution: {integrity: sha512-...}
|
|
1505
|
+
* ```
|
|
1506
|
+
*/
|
|
1507
|
+
parseLockfile(content, lockfilePath) {
|
|
1508
|
+
try {
|
|
1509
|
+
const parsed = parseYaml2(content);
|
|
1510
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1511
|
+
throw new Error("Invalid YAML format");
|
|
1512
|
+
}
|
|
1513
|
+
const lockfile = parsed;
|
|
1514
|
+
const lockfileVersion = this.parseLockfileVersion(lockfile.lockfileVersion);
|
|
1515
|
+
const directNames = this.extractDirectDependencies(lockfile);
|
|
1516
|
+
return this.extractDependencies(lockfile, lockfileVersion, directNames);
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
throw new LockfileParseError(
|
|
1519
|
+
lockfilePath,
|
|
1520
|
+
`Failed to parse pnpm-lock.yaml: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Extracts direct dependency names from pnpm lockfile.
|
|
1526
|
+
*
|
|
1527
|
+
* Supports both formats:
|
|
1528
|
+
* - pnpm v5.x: root-level dependencies/devDependencies
|
|
1529
|
+
* - pnpm v6+: importers['.'].dependencies/devDependencies
|
|
1530
|
+
*/
|
|
1531
|
+
extractDirectDependencies(lockfile) {
|
|
1532
|
+
const directNames = /* @__PURE__ */ new Set();
|
|
1533
|
+
if (lockfile.importers && typeof lockfile.importers === "object") {
|
|
1534
|
+
const rootImporter = lockfile.importers["."];
|
|
1535
|
+
if (rootImporter && typeof rootImporter === "object") {
|
|
1536
|
+
if (rootImporter.dependencies && typeof rootImporter.dependencies === "object") {
|
|
1537
|
+
for (const name of Object.keys(rootImporter.dependencies)) {
|
|
1538
|
+
directNames.add(name);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (rootImporter.devDependencies && typeof rootImporter.devDependencies === "object") {
|
|
1542
|
+
for (const name of Object.keys(rootImporter.devDependencies)) {
|
|
1543
|
+
directNames.add(name);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (directNames.size === 0) {
|
|
1549
|
+
if (lockfile.dependencies && typeof lockfile.dependencies === "object") {
|
|
1550
|
+
for (const name of Object.keys(lockfile.dependencies)) {
|
|
1551
|
+
directNames.add(name);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (lockfile.devDependencies && typeof lockfile.devDependencies === "object") {
|
|
1555
|
+
for (const name of Object.keys(lockfile.devDependencies)) {
|
|
1556
|
+
directNames.add(name);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return directNames;
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Parses lockfile version (can be string or number)
|
|
1564
|
+
*/
|
|
1565
|
+
parseLockfileVersion(version) {
|
|
1566
|
+
if (typeof version === "number") {
|
|
1567
|
+
return version;
|
|
1568
|
+
}
|
|
1569
|
+
if (typeof version === "string") {
|
|
1570
|
+
const parsed = parseFloat(version);
|
|
1571
|
+
return isNaN(parsed) ? 5.4 : parsed;
|
|
1572
|
+
}
|
|
1573
|
+
return 5.4;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Extracts dependencies from the lockfile packages section
|
|
1577
|
+
*/
|
|
1578
|
+
extractDependencies(lockfile, lockfileVersion, directNames) {
|
|
1579
|
+
const deps = [];
|
|
1580
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1581
|
+
if (!lockfile.packages || typeof lockfile.packages !== "object") {
|
|
1582
|
+
return deps;
|
|
1583
|
+
}
|
|
1584
|
+
for (const [pkgPath, pkgInfo] of Object.entries(lockfile.packages)) {
|
|
1585
|
+
if (!pkgInfo || typeof pkgInfo !== "object") {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (pkgPath.includes("@workspace:")) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const { name, version } = this.parsePackagePath(pkgPath, lockfileVersion);
|
|
1592
|
+
if (!name || !version) {
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const depKey = `${name}@${version}`;
|
|
1596
|
+
if (seen.has(depKey)) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
seen.set(depKey, true);
|
|
1600
|
+
deps.push({
|
|
1601
|
+
name,
|
|
1602
|
+
version,
|
|
1603
|
+
direct: directNames.has(name),
|
|
1604
|
+
ecosystem: "npm",
|
|
1605
|
+
purl: this.buildPurl(name, version)
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
return deps;
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Parses package path to extract name and version.
|
|
1612
|
+
*
|
|
1613
|
+
* pnpm v5.x format:
|
|
1614
|
+
* "/express/4.18.2" → name: "express", version: "4.18.2"
|
|
1615
|
+
* "/@types/node/20.11.5" → name: "@types/node", version: "20.11.5"
|
|
1616
|
+
* "/accepts/1.3.8" → name: "accepts", version: "1.3.8"
|
|
1617
|
+
*
|
|
1618
|
+
* pnpm v6+ format:
|
|
1619
|
+
* "/express@4.18.2" → name: "express", version: "4.18.2"
|
|
1620
|
+
* "/@types/node@20.11.5" → name: "@types/node", version: "20.11.5"
|
|
1621
|
+
* "/accepts@1.3.8" → name: "accepts", version: "1.3.8"
|
|
1622
|
+
*
|
|
1623
|
+
* Also handles peer dependency suffixes:
|
|
1624
|
+
* "/pkg@1.0.0_dep@2.0.0" → name: "pkg", version: "1.0.0"
|
|
1625
|
+
* "/pkg@1.0.0(dep@2.0.0)" → name: "pkg", version: "1.0.0"
|
|
1626
|
+
*/
|
|
1627
|
+
parsePackagePath(pkgPath, lockfileVersion) {
|
|
1628
|
+
const path14 = pkgPath.startsWith("/") ? pkgPath.slice(1) : pkgPath;
|
|
1629
|
+
const cleanPath = path14.split("_")[0].split("(")[0];
|
|
1630
|
+
if (!cleanPath) {
|
|
1631
|
+
return { name: null, version: null };
|
|
1632
|
+
}
|
|
1633
|
+
if (lockfileVersion >= 6) {
|
|
1634
|
+
return this.parseV6Format(cleanPath);
|
|
1635
|
+
}
|
|
1636
|
+
return this.parseV5Format(cleanPath);
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Parses v6+ format: "express@4.18.2" or "@types/node@20.11.5"
|
|
1640
|
+
*/
|
|
1641
|
+
parseV6Format(path14) {
|
|
1642
|
+
if (path14.startsWith("@")) {
|
|
1643
|
+
const lastAtIndex = path14.lastIndexOf("@");
|
|
1644
|
+
if (lastAtIndex <= 0) {
|
|
1645
|
+
return { name: null, version: null };
|
|
1646
|
+
}
|
|
1647
|
+
const name2 = path14.substring(0, lastAtIndex);
|
|
1648
|
+
const version2 = path14.substring(lastAtIndex + 1);
|
|
1649
|
+
return { name: name2, version: version2 };
|
|
1650
|
+
}
|
|
1651
|
+
const atIndex = path14.indexOf("@");
|
|
1652
|
+
if (atIndex < 0) {
|
|
1653
|
+
return { name: null, version: null };
|
|
1654
|
+
}
|
|
1655
|
+
const name = path14.substring(0, atIndex);
|
|
1656
|
+
const version = path14.substring(atIndex + 1);
|
|
1657
|
+
return { name, version };
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Parses v5.x format: "express/4.18.2" or "@types/node/20.11.5"
|
|
1661
|
+
*/
|
|
1662
|
+
parseV5Format(path14) {
|
|
1663
|
+
if (path14.startsWith("@")) {
|
|
1664
|
+
const parts = path14.split("/");
|
|
1665
|
+
if (parts.length < 3) {
|
|
1666
|
+
return { name: null, version: null };
|
|
1667
|
+
}
|
|
1668
|
+
const name2 = `${parts[0]}/${parts[1]}`;
|
|
1669
|
+
const version2 = parts[2];
|
|
1670
|
+
return { name: name2, version: version2 };
|
|
1671
|
+
}
|
|
1672
|
+
const slashIndex = path14.indexOf("/");
|
|
1673
|
+
if (slashIndex < 0) {
|
|
1674
|
+
return { name: null, version: null };
|
|
1675
|
+
}
|
|
1676
|
+
const name = path14.substring(0, slashIndex);
|
|
1677
|
+
const version = path14.substring(slashIndex + 1);
|
|
1678
|
+
return { name, version };
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Builds a purl (Package URL) for an npm package.
|
|
1682
|
+
*
|
|
1683
|
+
* Per the purl spec:
|
|
1684
|
+
* "The npm scope @ sign prefix is always percent encoded."
|
|
1685
|
+
*
|
|
1686
|
+
* So @types/node@20.11.5 → pkg:npm/%40types/node@20.11.5
|
|
1687
|
+
* And express@4.18.2 → pkg:npm/express@4.18.2
|
|
1688
|
+
*/
|
|
1689
|
+
buildPurl(name, version) {
|
|
1690
|
+
if (name.startsWith("@")) {
|
|
1691
|
+
return `pkg:npm/%40${name.slice(1)}@${version}`;
|
|
1692
|
+
}
|
|
1693
|
+
return `pkg:npm/${name}@${version}`;
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
|
|
1697
|
+
// src/scanners/deno/deno-scanner.ts
|
|
1698
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1699
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1700
|
+
import path11 from "path";
|
|
1701
|
+
var DenoScanner = class {
|
|
1702
|
+
ecosystem = "deno";
|
|
1703
|
+
lockfileNames = ["deno.lock"];
|
|
1704
|
+
async detect(projectPath) {
|
|
1705
|
+
for (const name of this.lockfileNames) {
|
|
1706
|
+
const lockfilePath = path11.join(projectPath, name);
|
|
1707
|
+
if (existsSync11(lockfilePath)) return lockfilePath;
|
|
1708
|
+
}
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
async scan(projectPath, lockfilePath) {
|
|
1712
|
+
const lockfileRaw = await readFile11(lockfilePath, "utf-8");
|
|
1713
|
+
let lockfile;
|
|
1714
|
+
try {
|
|
1715
|
+
lockfile = JSON.parse(lockfileRaw);
|
|
1716
|
+
} catch {
|
|
1717
|
+
throw new LockfileParseError(lockfilePath, "Invalid JSON");
|
|
1718
|
+
}
|
|
1719
|
+
const dependencies = this.parseLockfile(lockfile);
|
|
1720
|
+
return {
|
|
1721
|
+
projectPath,
|
|
1722
|
+
ecosystem: "deno",
|
|
1723
|
+
dependencies,
|
|
1724
|
+
lockfilePath,
|
|
1725
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Parses the lockfile and extracts dependencies from both
|
|
1730
|
+
* npm and jsr package registries.
|
|
1731
|
+
*
|
|
1732
|
+
* Supports v3 (packages nested under `packages`), v4, and v5 (top-level jsr/npm sections).
|
|
1733
|
+
* Uses lockfile specifiers to determine direct vs transitive dependencies.
|
|
1734
|
+
*/
|
|
1735
|
+
parseLockfile(lockfile) {
|
|
1736
|
+
const deps = [];
|
|
1737
|
+
const directNames = this.extractDirectDependencies(lockfile);
|
|
1738
|
+
const jsrPackages = lockfile.jsr ?? lockfile.packages?.jsr ?? {};
|
|
1739
|
+
const npmPackages = lockfile.npm ?? lockfile.packages?.npm ?? {};
|
|
1740
|
+
for (const key of Object.keys(jsrPackages)) {
|
|
1741
|
+
const parsed = this.parsePackageKey(key);
|
|
1742
|
+
if (!parsed) continue;
|
|
1743
|
+
deps.push({
|
|
1744
|
+
name: parsed.name,
|
|
1745
|
+
version: parsed.version,
|
|
1746
|
+
direct: directNames.has(`jsr:${parsed.name}`),
|
|
1747
|
+
ecosystem: "deno",
|
|
1748
|
+
// JSR packages belong to Deno ecosystem
|
|
1749
|
+
purl: this.buildJsrPurl(parsed.name, parsed.version)
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
for (const key of Object.keys(npmPackages)) {
|
|
1753
|
+
const parsed = this.parsePackageKey(key);
|
|
1754
|
+
if (!parsed) continue;
|
|
1755
|
+
deps.push({
|
|
1756
|
+
name: parsed.name,
|
|
1757
|
+
version: parsed.version,
|
|
1758
|
+
direct: directNames.has(`npm:${parsed.name}`),
|
|
1759
|
+
ecosystem: "npm",
|
|
1760
|
+
// npm packages belong to npm ecosystem (for CVE tracking)
|
|
1761
|
+
purl: this.buildNpmPurl(parsed.name, parsed.version)
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
return deps;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Extracts direct dependency names from lockfile specifiers.
|
|
1768
|
+
*
|
|
1769
|
+
* Returns a set of ecosystem-qualified package names like "jsr:@std/assert" or "npm:express".
|
|
1770
|
+
* The ecosystem prefix prevents collisions if the same package name exists in both registries.
|
|
1771
|
+
*/
|
|
1772
|
+
extractDirectDependencies(lockfile) {
|
|
1773
|
+
const directNames = /* @__PURE__ */ new Set();
|
|
1774
|
+
const specifiers = lockfile.specifiers ?? lockfile.packages?.specifiers ?? {};
|
|
1775
|
+
for (const [constraint, resolved] of Object.entries(specifiers)) {
|
|
1776
|
+
if (resolved.startsWith("file:") || resolved.startsWith("https:") || resolved.startsWith("http:") || resolved.startsWith("data:")) {
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
let ecosystem = null;
|
|
1780
|
+
if (constraint.startsWith("jsr:")) {
|
|
1781
|
+
ecosystem = "jsr";
|
|
1782
|
+
} else if (constraint.startsWith("npm:")) {
|
|
1783
|
+
ecosystem = "npm";
|
|
1784
|
+
}
|
|
1785
|
+
if (!ecosystem) continue;
|
|
1786
|
+
let resolvedKey = resolved;
|
|
1787
|
+
if (resolved.startsWith("jsr:") || resolved.startsWith("npm:")) {
|
|
1788
|
+
resolvedKey = resolved.replace(/^(jsr:|npm:)/, "");
|
|
1789
|
+
const parsed = this.parsePackageKey(resolvedKey);
|
|
1790
|
+
if (parsed) {
|
|
1791
|
+
directNames.add(`${ecosystem}:${parsed.name}`);
|
|
1792
|
+
}
|
|
1793
|
+
} else {
|
|
1794
|
+
const name = this.extractNameFromSpecifier(constraint);
|
|
1795
|
+
if (name) {
|
|
1796
|
+
directNames.add(`${ecosystem}:${name}`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return directNames;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Parses a package key like "@std/assert@1.0.10" or "express@4.21.2"
|
|
1804
|
+
* into { name, version }.
|
|
1805
|
+
*
|
|
1806
|
+
* Handles scoped packages where the name starts with @ (e.g., @std/assert).
|
|
1807
|
+
* In that case the version separator is the LAST @ sign.
|
|
1808
|
+
*/
|
|
1809
|
+
parsePackageKey(key) {
|
|
1810
|
+
const lastAtIndex = key.lastIndexOf("@");
|
|
1811
|
+
if (lastAtIndex <= 0) return null;
|
|
1812
|
+
const name = key.slice(0, lastAtIndex);
|
|
1813
|
+
const version = key.slice(lastAtIndex + 1);
|
|
1814
|
+
if (!name || !version) return null;
|
|
1815
|
+
return { name, version };
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Extracts the package name from a Deno import specifier.
|
|
1819
|
+
*
|
|
1820
|
+
* Examples:
|
|
1821
|
+
* "jsr:@std/assert@^1.0.0" → "@std/assert"
|
|
1822
|
+
* "npm:express@^4.18.0" → "express"
|
|
1823
|
+
* "npm:@hono/hono@^4.0.0" → "@hono/hono"
|
|
1824
|
+
* "lodash" (bare) → "lodash"
|
|
1825
|
+
*/
|
|
1826
|
+
extractNameFromSpecifier(specifier) {
|
|
1827
|
+
const withoutPrefix = specifier.replace(/^(jsr:|npm:)/, "");
|
|
1828
|
+
if (!withoutPrefix) return null;
|
|
1829
|
+
if (withoutPrefix.startsWith("@")) {
|
|
1830
|
+
const slashIndex = withoutPrefix.indexOf("/");
|
|
1831
|
+
if (slashIndex === -1) return null;
|
|
1832
|
+
const afterSlash = withoutPrefix.indexOf("@", slashIndex);
|
|
1833
|
+
if (afterSlash === -1) return withoutPrefix;
|
|
1834
|
+
return withoutPrefix.slice(0, afterSlash);
|
|
1835
|
+
}
|
|
1836
|
+
const atIndex = withoutPrefix.indexOf("@");
|
|
1837
|
+
if (atIndex === -1) return withoutPrefix;
|
|
1838
|
+
return withoutPrefix.slice(0, atIndex);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Builds a purl for a JSR package.
|
|
1842
|
+
*
|
|
1843
|
+
* JSR packages use the "jsr" purl type (non-standard but descriptive).
|
|
1844
|
+
* For scoped packages, both @ and all / characters are percent-encoded.
|
|
1845
|
+
* Example: `pkg:jsr/%40std%2Fassert@1.0.10`
|
|
1846
|
+
*/
|
|
1847
|
+
buildJsrPurl(name, version) {
|
|
1848
|
+
if (name.startsWith("@")) {
|
|
1849
|
+
const encoded = "%40" + name.slice(1).replace(/\//g, "%2F");
|
|
1850
|
+
return `pkg:jsr/${encoded}@${version}`;
|
|
1851
|
+
}
|
|
1852
|
+
return `pkg:jsr/${name}@${version}`;
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Builds a purl for an npm package used via Deno.
|
|
1856
|
+
*
|
|
1857
|
+
* Uses the standard npm purl type since these are npm packages.
|
|
1858
|
+
* Per npm purl spec, only @ is encoded, / remains as namespace separator.
|
|
1859
|
+
* Example: `pkg:npm/%40std/assert@1.0.10` or `pkg:npm/express@4.21.2`
|
|
1860
|
+
*/
|
|
1861
|
+
buildNpmPurl(name, version) {
|
|
1862
|
+
if (name.startsWith("@")) {
|
|
1863
|
+
return `pkg:npm/%40${name.slice(1)}@${version}`;
|
|
1864
|
+
}
|
|
1865
|
+
return `pkg:npm/${name}@${version}`;
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
// src/scanners/poetry/poetry-scanner.ts
|
|
1870
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1871
|
+
import { existsSync as existsSync12 } from "fs";
|
|
1872
|
+
import path12 from "path";
|
|
1873
|
+
var PoetryScanner = class {
|
|
1874
|
+
ecosystem = "poetry";
|
|
1875
|
+
lockfileNames = ["poetry.lock"];
|
|
1876
|
+
async detect(projectPath) {
|
|
1877
|
+
const lockfilePath = path12.join(projectPath, "poetry.lock");
|
|
1878
|
+
return existsSync12(lockfilePath) ? lockfilePath : null;
|
|
1879
|
+
}
|
|
1880
|
+
async scan(projectPath, lockfilePath) {
|
|
1881
|
+
const [lockfileRaw, pyprojectRaw] = await Promise.all([
|
|
1882
|
+
readFile12(lockfilePath, "utf-8"),
|
|
1883
|
+
readFile12(path12.join(projectPath, "pyproject.toml"), "utf-8").catch(() => null)
|
|
1884
|
+
]);
|
|
1885
|
+
const packages = this.parseLockfile(lockfileRaw, lockfilePath);
|
|
1886
|
+
const directNames = pyprojectRaw ? this.parsePyprojectToml(pyprojectRaw) : /* @__PURE__ */ new Set();
|
|
1887
|
+
const dependencies = [];
|
|
1888
|
+
for (const pkg of packages) {
|
|
1889
|
+
dependencies.push({
|
|
1890
|
+
name: this.normalizePipName(pkg.name),
|
|
1891
|
+
version: pkg.version,
|
|
1892
|
+
direct: directNames.size > 0 ? directNames.has(this.normalizePipName(pkg.name)) : true,
|
|
1893
|
+
ecosystem: "poetry",
|
|
1894
|
+
purl: this.buildPurl(pkg.name, pkg.version)
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
return {
|
|
1898
|
+
projectPath,
|
|
1899
|
+
ecosystem: "poetry",
|
|
1900
|
+
dependencies,
|
|
1901
|
+
lockfilePath,
|
|
1902
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1906
|
+
* Parses poetry.lock by splitting on [[package]] blocks.
|
|
1907
|
+
* Lightweight parser that handles the regular structure
|
|
1908
|
+
* without needing a full TOML library.
|
|
1909
|
+
*/
|
|
1910
|
+
parseLockfile(content, lockfilePath) {
|
|
1911
|
+
const packages = [];
|
|
1912
|
+
const blocks = content.split(/^\[\[package\]\]$/m);
|
|
1913
|
+
for (const block of blocks) {
|
|
1914
|
+
if (!block.trim()) continue;
|
|
1915
|
+
const name = this.extractField(block, "name");
|
|
1916
|
+
const version = this.extractField(block, "version");
|
|
1917
|
+
if (name && version) {
|
|
1918
|
+
packages.push({ name, version });
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (packages.length === 0 && content.includes("[[package]]")) {
|
|
1922
|
+
throw new LockfileParseError(lockfilePath, "Failed to parse any packages from poetry.lock");
|
|
1923
|
+
}
|
|
1924
|
+
return packages;
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Extracts a string field value from a TOML block.
|
|
1928
|
+
* Handles: `name = "value"` format.
|
|
1929
|
+
*/
|
|
1930
|
+
extractField(block, fieldName) {
|
|
1931
|
+
const regex = new RegExp(`^${fieldName}\\s*=\\s*"([^"]*)"`, "m");
|
|
1932
|
+
const match = block.match(regex);
|
|
1933
|
+
return match ? match[1] : null;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Parses `pyproject.toml` to extract direct dependency names.
|
|
1937
|
+
*
|
|
1938
|
+
* Looks for:
|
|
1939
|
+
* - `[tool.poetry.dependencies]` — main dependencies
|
|
1940
|
+
* - `[tool.poetry.group.dev.dependencies]` — dev dependencies
|
|
1941
|
+
* - `[tool.poetry.group.*.dependencies]` — other groups
|
|
1942
|
+
*
|
|
1943
|
+
* Supports formats:
|
|
1944
|
+
* - `requests = "^2.31.0"`
|
|
1945
|
+
* - `requests = { version = "^2.31.0", optional = true }`
|
|
1946
|
+
* - `python = "^3.12"` — skipped (the Python interpreter itself)
|
|
1947
|
+
*/
|
|
1948
|
+
parsePyprojectToml(content) {
|
|
1949
|
+
const directNames = /* @__PURE__ */ new Set();
|
|
1950
|
+
let inDepsSection = false;
|
|
1951
|
+
for (const rawLine of content.split("\n")) {
|
|
1952
|
+
const line = rawLine.trim();
|
|
1953
|
+
if (line.startsWith("[")) {
|
|
1954
|
+
inDepsSection = line === "[tool.poetry.dependencies]" || /^\[tool\.poetry\.group\.[^\]]+\.dependencies\]$/.test(line);
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
if (inDepsSection && line && !line.startsWith("#")) {
|
|
1958
|
+
const match = line.match(/^([a-zA-Z0-9_][a-zA-Z0-9._-]*)\s*=/);
|
|
1959
|
+
if (match && match[1]) {
|
|
1960
|
+
const name = this.normalizePipName(match[1]);
|
|
1961
|
+
if (name !== "python") {
|
|
1962
|
+
directNames.add(name);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return directNames;
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Normalizes a pip package name per PEP 503.
|
|
1971
|
+
* Converts to lowercase and replaces any run of [-_.] with a single hyphen.
|
|
1972
|
+
*/
|
|
1973
|
+
normalizePipName(name) {
|
|
1974
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Builds a purl for a PyPI package.
|
|
1978
|
+
* Per purl spec, the type is "pypi" (not "poetry").
|
|
1979
|
+
*/
|
|
1980
|
+
buildPurl(name, version) {
|
|
1981
|
+
return `pkg:pypi/${this.normalizePipName(name)}@${version}`;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
// src/scanners/uv/uv-scanner.ts
|
|
1986
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
1987
|
+
import { existsSync as existsSync13 } from "fs";
|
|
1988
|
+
import path13 from "path";
|
|
1989
|
+
var UvScanner = class {
|
|
1990
|
+
ecosystem = "uv";
|
|
1991
|
+
lockfileNames = ["uv.lock"];
|
|
1992
|
+
async detect(projectPath) {
|
|
1993
|
+
const lockfilePath = path13.join(projectPath, "uv.lock");
|
|
1994
|
+
return existsSync13(lockfilePath) ? lockfilePath : null;
|
|
1995
|
+
}
|
|
1996
|
+
async scan(projectPath, lockfilePath) {
|
|
1997
|
+
const [lockfileRaw, pyprojectRaw] = await Promise.all([
|
|
1998
|
+
readFile13(lockfilePath, "utf-8"),
|
|
1999
|
+
readFile13(path13.join(projectPath, "pyproject.toml"), "utf-8").catch(() => null)
|
|
2000
|
+
]);
|
|
2001
|
+
const packages = this.parseLockfile(lockfileRaw, lockfilePath);
|
|
2002
|
+
const projectName = pyprojectRaw ? this.extractProjectName(pyprojectRaw) : null;
|
|
2003
|
+
const directNames = pyprojectRaw ? this.parsePyprojectDeps(pyprojectRaw) : /* @__PURE__ */ new Set();
|
|
2004
|
+
const dependencies = [];
|
|
2005
|
+
for (const pkg of packages) {
|
|
2006
|
+
if (pkg.isEditable) continue;
|
|
2007
|
+
if (projectName && this.normalizePipName(pkg.name) === this.normalizePipName(projectName)) {
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
dependencies.push({
|
|
2011
|
+
name: this.normalizePipName(pkg.name),
|
|
2012
|
+
version: pkg.version,
|
|
2013
|
+
direct: directNames.size > 0 ? directNames.has(this.normalizePipName(pkg.name)) : true,
|
|
2014
|
+
ecosystem: "uv",
|
|
2015
|
+
purl: this.buildPurl(pkg.name, pkg.version)
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
return {
|
|
2019
|
+
projectPath,
|
|
2020
|
+
ecosystem: "uv",
|
|
2021
|
+
dependencies,
|
|
2022
|
+
lockfilePath,
|
|
2023
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Parses uv.lock by splitting on [[package]] blocks.
|
|
2028
|
+
* Lightweight parser that handles the regular structure
|
|
2029
|
+
* without needing a full TOML library.
|
|
2030
|
+
*/
|
|
2031
|
+
parseLockfile(content, lockfilePath) {
|
|
2032
|
+
const packages = [];
|
|
2033
|
+
const blocks = content.split(/^\[\[package\]\]$/m);
|
|
2034
|
+
for (const block of blocks) {
|
|
2035
|
+
if (!block.trim()) continue;
|
|
2036
|
+
const name = this.extractField(block, "name");
|
|
2037
|
+
const version = this.extractField(block, "version");
|
|
2038
|
+
if (name && version) {
|
|
2039
|
+
const isEditable = /source\s*=\s*\{[^}]*editable\s*=/.test(block) || /source\s*=\s*\{[^}]*virtual\s*=/.test(block);
|
|
2040
|
+
packages.push({ name, version, isEditable });
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
if (packages.length === 0 && content.includes("[[package]]")) {
|
|
2044
|
+
throw new LockfileParseError(lockfilePath, "Failed to parse any packages from uv.lock");
|
|
2045
|
+
}
|
|
2046
|
+
return packages;
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Extracts a string field value from a TOML block.
|
|
2050
|
+
* Handles: `name = "value"` format.
|
|
2051
|
+
*/
|
|
2052
|
+
extractField(block, fieldName) {
|
|
2053
|
+
const regex = new RegExp(`^${fieldName}\\s*=\\s*"([^"]*)"`, "m");
|
|
2054
|
+
const match = block.match(regex);
|
|
2055
|
+
return match ? match[1] : null;
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Extracts the project name from `pyproject.toml`.
|
|
2059
|
+
* Looks for `name = "..."` under `[project]`.
|
|
2060
|
+
*/
|
|
2061
|
+
extractProjectName(content) {
|
|
2062
|
+
let inProjectSection = false;
|
|
2063
|
+
for (const rawLine of content.split("\n")) {
|
|
2064
|
+
const line = rawLine.trim();
|
|
2065
|
+
if (line.startsWith("[")) {
|
|
2066
|
+
inProjectSection = line === "[project]";
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (inProjectSection) {
|
|
2070
|
+
const match = line.match(/^name\s*=\s*"([^"]*)"/);
|
|
2071
|
+
if (match) return match[1];
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Parses `pyproject.toml` to extract direct dependency names.
|
|
2078
|
+
*
|
|
2079
|
+
* Looks for:
|
|
2080
|
+
* - `[project]` → `dependencies = [...]` (PEP 621)
|
|
2081
|
+
* - `[project.optional-dependencies]` (extras)
|
|
2082
|
+
* - `[dependency-groups]` (PEP 735, used by uv for dev deps)
|
|
2083
|
+
*
|
|
2084
|
+
* Dependency strings follow PEP 508:
|
|
2085
|
+
* - `"requests>=2.31.0"`
|
|
2086
|
+
* - `"flask[dotenv]>=3.0"`
|
|
2087
|
+
* - `"black"` (bare name)
|
|
2088
|
+
*/
|
|
2089
|
+
parsePyprojectDeps(content) {
|
|
2090
|
+
const directNames = /* @__PURE__ */ new Set();
|
|
2091
|
+
this.extractInlineArray(content, directNames);
|
|
2092
|
+
this.extractDependencyGroups(content, directNames);
|
|
2093
|
+
return directNames;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Extracts dependency names from PEP 621 `dependencies = [...]` arrays
|
|
2097
|
+
* and `[project.optional-dependencies]` sections.
|
|
2098
|
+
*/
|
|
2099
|
+
extractInlineArray(content, directNames) {
|
|
2100
|
+
const arrayRegex = /(?:^dependencies|^[a-zA-Z0-9_-]+)\s*=\s*\[([^\]]*)\]/gm;
|
|
2101
|
+
let match;
|
|
2102
|
+
while ((match = arrayRegex.exec(content)) !== null) {
|
|
2103
|
+
const arrayContent = match[1];
|
|
2104
|
+
this.extractPepNames(arrayContent, directNames);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Extracts dependency names from [dependency-groups] sections.
|
|
2109
|
+
* Format:
|
|
2110
|
+
* ```toml
|
|
2111
|
+
* [dependency-groups]
|
|
2112
|
+
* dev = ["pytest>=7.0", "black"]
|
|
2113
|
+
* ```
|
|
2114
|
+
*/
|
|
2115
|
+
extractDependencyGroups(content, directNames) {
|
|
2116
|
+
let inDepGroups = false;
|
|
2117
|
+
for (const rawLine of content.split("\n")) {
|
|
2118
|
+
const line = rawLine.trim();
|
|
2119
|
+
if (line.startsWith("[")) {
|
|
2120
|
+
inDepGroups = line === "[dependency-groups]";
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
if (inDepGroups && line && !line.startsWith("#")) {
|
|
2124
|
+
const arrayMatch = line.match(/^[a-zA-Z0-9_-]+\s*=\s*\[([^\]]*)\]/);
|
|
2125
|
+
if (arrayMatch) {
|
|
2126
|
+
this.extractPepNames(arrayMatch[1], directNames);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Extracts PEP 508 package names from a comma-separated
|
|
2133
|
+
* list of quoted dependency strings.
|
|
2134
|
+
*/
|
|
2135
|
+
extractPepNames(content, directNames) {
|
|
2136
|
+
const depStrings = content.match(/"([^"]*)"/g);
|
|
2137
|
+
if (!depStrings) return;
|
|
2138
|
+
for (const quoted of depStrings) {
|
|
2139
|
+
const depStr = quoted.replace(/"/g, "").trim();
|
|
2140
|
+
if (!depStr) continue;
|
|
2141
|
+
const nameMatch = depStr.match(/^([a-zA-Z0-9_][a-zA-Z0-9._-]*)/);
|
|
2142
|
+
if (nameMatch && nameMatch[1]) {
|
|
2143
|
+
directNames.add(this.normalizePipName(nameMatch[1]));
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Normalizes a pip package name per PEP 503.
|
|
2149
|
+
* Converts to lowercase and replaces any run of [-_.] with a single hyphen.
|
|
2150
|
+
*/
|
|
2151
|
+
normalizePipName(name) {
|
|
2152
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Builds a purl for a PyPI package.
|
|
2156
|
+
* Per purl spec, the type is "pypi" (not "uv").
|
|
2157
|
+
*/
|
|
2158
|
+
buildPurl(name, version) {
|
|
2159
|
+
return `pkg:pypi/${this.normalizePipName(name)}@${version}`;
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
|
|
2163
|
+
// src/scanners/registry.ts
|
|
2164
|
+
var ScannerRegistry = class {
|
|
2165
|
+
scanners;
|
|
2166
|
+
constructor() {
|
|
2167
|
+
this.scanners = [
|
|
2168
|
+
new NpmScanner(),
|
|
2169
|
+
new NugetScanner(),
|
|
2170
|
+
new CargoScanner(),
|
|
2171
|
+
new PipScanner(),
|
|
2172
|
+
new PoetryScanner(),
|
|
2173
|
+
new UvScanner(),
|
|
2174
|
+
new MavenScanner(),
|
|
2175
|
+
new GoScanner(),
|
|
2176
|
+
new RubyScanner(),
|
|
2177
|
+
new ComposerScanner(),
|
|
2178
|
+
new YarnScanner(),
|
|
2179
|
+
new PnpmScanner(),
|
|
2180
|
+
new DenoScanner()
|
|
2181
|
+
];
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Auto-detects the project's ecosystem and scans dependencies.
|
|
2185
|
+
* Tries each registered scanner in order until one matches.
|
|
2186
|
+
*/
|
|
2187
|
+
async detectAndScan(projectPath) {
|
|
2188
|
+
for (const scanner of this.scanners) {
|
|
2189
|
+
const lockfilePath = await scanner.detect(projectPath);
|
|
2190
|
+
if (lockfilePath) {
|
|
2191
|
+
return scanner.scan(projectPath, lockfilePath);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
throw new NoLockfileError(projectPath);
|
|
2195
|
+
}
|
|
2196
|
+
/** Returns a specific scanner by ecosystem name */
|
|
2197
|
+
getScanner(ecosystem) {
|
|
2198
|
+
return this.scanners.find((s) => s.ecosystem === ecosystem);
|
|
2199
|
+
}
|
|
2200
|
+
/** Lists all registered ecosystems */
|
|
2201
|
+
listEcosystems() {
|
|
2202
|
+
return this.scanners.map((s) => s.ecosystem);
|
|
2203
|
+
}
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
// src/sbom/cyclonedx.ts
|
|
2207
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
2208
|
+
var SCHEMA_URLS = {
|
|
2209
|
+
"1.4": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
|
2210
|
+
"1.5": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
2211
|
+
"1.6": "http://cyclonedx.org/schema/bom-1.6.schema.json",
|
|
2212
|
+
"1.7": "http://cyclonedx.org/schema/bom-1.7.schema.json"
|
|
2213
|
+
};
|
|
2214
|
+
var CycloneDxGenerator = class {
|
|
2215
|
+
constructor(specVersion = "1.7") {
|
|
2216
|
+
this.specVersion = specVersion;
|
|
2217
|
+
}
|
|
2218
|
+
format = "cyclonedx-json";
|
|
2219
|
+
generate(scanResult, toolVersion = DEFAULT_TOOL_VERSION) {
|
|
2220
|
+
const bom = this.buildBom(scanResult, toolVersion);
|
|
2221
|
+
const content = JSON.stringify(bom, null, 2);
|
|
2222
|
+
return {
|
|
2223
|
+
format: "cyclonedx-json",
|
|
2224
|
+
specVersion: this.specVersion,
|
|
2225
|
+
content,
|
|
2226
|
+
componentCount: scanResult.dependencies.length,
|
|
2227
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
buildBom(scanResult, toolVersion) {
|
|
2231
|
+
const projectName = extractProjectName(scanResult.projectPath);
|
|
2232
|
+
return {
|
|
2233
|
+
$schema: SCHEMA_URLS[this.specVersion],
|
|
2234
|
+
bomFormat: "CycloneDX",
|
|
2235
|
+
specVersion: this.specVersion,
|
|
2236
|
+
serialNumber: `urn:uuid:${randomUUID4()}`,
|
|
2237
|
+
version: 1,
|
|
2238
|
+
metadata: {
|
|
2239
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2240
|
+
tools: this.buildTools(toolVersion),
|
|
2241
|
+
// NTIA: metadata.supplier — the org supplying the root software
|
|
2242
|
+
supplier: {
|
|
2243
|
+
name: projectName
|
|
2244
|
+
},
|
|
2245
|
+
component: {
|
|
2246
|
+
type: "application",
|
|
2247
|
+
name: projectName,
|
|
2248
|
+
"bom-ref": "root-component",
|
|
2249
|
+
supplier: { name: projectName }
|
|
2250
|
+
}
|
|
2251
|
+
},
|
|
2252
|
+
components: scanResult.dependencies.map((dep) => this.toComponent(dep)),
|
|
2253
|
+
dependencies: this.buildDependencyGraph(scanResult)
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
/** Converts a Verimu Dependency to a CycloneDX component */
|
|
2257
|
+
toComponent(dep) {
|
|
2258
|
+
return {
|
|
2259
|
+
type: "library",
|
|
2260
|
+
name: dep.name,
|
|
2261
|
+
version: dep.version,
|
|
2262
|
+
purl: dep.purl,
|
|
2263
|
+
"bom-ref": dep.purl,
|
|
2264
|
+
scope: dep.direct ? "required" : "optional",
|
|
2265
|
+
// NTIA: component.supplier — derived from npm scope or package name
|
|
2266
|
+
supplier: {
|
|
2267
|
+
name: deriveSupplierName(dep.name)
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Builds the dependency graph section of the SBOM.
|
|
2273
|
+
*
|
|
2274
|
+
* The root component depends on all dependencies (direct + transitive).
|
|
2275
|
+
* This ensures a single root node in the graph, which NTIA validators expect.
|
|
2276
|
+
*
|
|
2277
|
+
* We include ALL deps under root (not just direct) because from a flat lockfile
|
|
2278
|
+
* we can't reliably reconstruct which transitive dep belongs to which direct dep.
|
|
2279
|
+
* This is still valid per the CycloneDX spec — it represents a complete but flat
|
|
2280
|
+
* dependency relationship.
|
|
2281
|
+
*/
|
|
2282
|
+
buildDependencyGraph(scanResult) {
|
|
2283
|
+
const allDepPurls = scanResult.dependencies.map((d) => d.purl);
|
|
2284
|
+
return [
|
|
2285
|
+
{
|
|
2286
|
+
ref: "root-component",
|
|
2287
|
+
dependsOn: allDepPurls
|
|
2288
|
+
}
|
|
2289
|
+
];
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Builds the tools metadata section.
|
|
2293
|
+
*
|
|
2294
|
+
* CycloneDX 1.4: tools is a flat array of { vendor, name, version, ... }
|
|
2295
|
+
* CycloneDX 1.5+: tools is an object { components: [...] }
|
|
2296
|
+
*/
|
|
2297
|
+
buildTools(toolVersion) {
|
|
2298
|
+
if (this.specVersion === "1.4") {
|
|
2299
|
+
return [
|
|
2300
|
+
{
|
|
2301
|
+
vendor: "Verimu",
|
|
2302
|
+
name: VERIMU_TOOL_NAME,
|
|
2303
|
+
version: toolVersion,
|
|
2304
|
+
externalReferences: [{ type: "website", url: VERIMU_TOOL_WEBSITE }]
|
|
2305
|
+
}
|
|
2306
|
+
];
|
|
2307
|
+
}
|
|
2308
|
+
return {
|
|
2309
|
+
components: [
|
|
2310
|
+
{
|
|
2311
|
+
type: "application",
|
|
2312
|
+
name: VERIMU_TOOL_NAME,
|
|
2313
|
+
version: toolVersion,
|
|
2314
|
+
description: VERIMU_TOOL_DESCRIPTION,
|
|
2315
|
+
supplier: { name: "Verimu" },
|
|
2316
|
+
externalReferences: [{ type: "website", url: VERIMU_TOOL_WEBSITE }]
|
|
2317
|
+
}
|
|
2318
|
+
]
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
// src/sbom/spdx.ts
|
|
2324
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2325
|
+
var SPDX_VERSION2 = "2.3";
|
|
2326
|
+
var SpdxJsonGenerator = class {
|
|
2327
|
+
format = "spdx-json";
|
|
2328
|
+
generate(scanResult, toolVersion = DEFAULT_TOOL_VERSION) {
|
|
2329
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2330
|
+
const projectName = extractProjectName(scanResult.projectPath);
|
|
2331
|
+
const rootPackageId = "SPDXRef-Package-root";
|
|
2332
|
+
const dependencies = normalizeDependencies(scanResult.dependencies);
|
|
2333
|
+
const document = {
|
|
2334
|
+
spdxVersion: `SPDX-${SPDX_VERSION2}`,
|
|
2335
|
+
dataLicense: "CC0-1.0",
|
|
2336
|
+
SPDXID: "SPDXRef-DOCUMENT",
|
|
2337
|
+
name: `${projectName}-sbom`,
|
|
2338
|
+
documentNamespace: `https://verimu.com/spdxdocs/${projectName}-${randomUUID5()}`,
|
|
2339
|
+
creationInfo: {
|
|
2340
|
+
created: timestamp,
|
|
2341
|
+
creators: [`Tool: ${VERIMU_TOOL_NAME}@${toolVersion}`]
|
|
2342
|
+
},
|
|
2343
|
+
documentDescribes: [rootPackageId],
|
|
2344
|
+
packages: [
|
|
2345
|
+
{
|
|
2346
|
+
name: projectName,
|
|
2347
|
+
SPDXID: rootPackageId,
|
|
2348
|
+
versionInfo: "NOASSERTION",
|
|
2349
|
+
supplier: `Organization: ${projectName}`,
|
|
2350
|
+
downloadLocation: "NOASSERTION",
|
|
2351
|
+
filesAnalyzed: false,
|
|
2352
|
+
licenseConcluded: "NOASSERTION",
|
|
2353
|
+
licenseDeclared: "NOASSERTION",
|
|
2354
|
+
primaryPackagePurpose: "APPLICATION"
|
|
2355
|
+
},
|
|
2356
|
+
...dependencies.map((dep, index) => ({
|
|
2357
|
+
name: dep.name,
|
|
2358
|
+
SPDXID: `SPDXRef-Package-${index + 1}`,
|
|
2359
|
+
versionInfo: dep.version,
|
|
2360
|
+
supplier: `Organization: ${dep.supplierName}`,
|
|
2361
|
+
downloadLocation: "NOASSERTION",
|
|
2362
|
+
filesAnalyzed: false,
|
|
2363
|
+
licenseConcluded: "NOASSERTION",
|
|
2364
|
+
licenseDeclared: "NOASSERTION",
|
|
2365
|
+
primaryPackagePurpose: "LIBRARY",
|
|
2366
|
+
externalRefs: [
|
|
2367
|
+
{
|
|
2368
|
+
referenceCategory: "PACKAGE-MANAGER",
|
|
2369
|
+
referenceType: "purl",
|
|
2370
|
+
referenceLocator: dep.purl
|
|
2371
|
+
}
|
|
2372
|
+
]
|
|
2373
|
+
}))
|
|
2374
|
+
],
|
|
2375
|
+
relationships: [
|
|
2376
|
+
{
|
|
2377
|
+
spdxElementId: "SPDXRef-DOCUMENT",
|
|
2378
|
+
relationshipType: "DESCRIBES",
|
|
2379
|
+
relatedSpdxElement: rootPackageId
|
|
2380
|
+
},
|
|
2381
|
+
...dependencies.map((_dep, index) => ({
|
|
2382
|
+
spdxElementId: rootPackageId,
|
|
2383
|
+
relationshipType: "DEPENDS_ON",
|
|
2384
|
+
relatedSpdxElement: `SPDXRef-Package-${index + 1}`
|
|
2385
|
+
}))
|
|
2386
|
+
]
|
|
2387
|
+
};
|
|
2388
|
+
return {
|
|
2389
|
+
format: "spdx-json",
|
|
2390
|
+
specVersion: SPDX_VERSION2,
|
|
2391
|
+
content: JSON.stringify(document, null, 2),
|
|
2392
|
+
componentCount: scanResult.dependencies.length,
|
|
2393
|
+
generatedAt: timestamp
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
|
|
2398
|
+
// src/sbom/swid.ts
|
|
2399
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2400
|
+
var SWID_SPEC_VERSION2 = "ISO/IEC 19770-2:2015";
|
|
2401
|
+
var SwidTagGenerator = class {
|
|
2402
|
+
format = "swid-xml";
|
|
2403
|
+
generate(scanResult, toolVersion = DEFAULT_TOOL_VERSION) {
|
|
2404
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2405
|
+
const projectName = extractProjectName(scanResult.projectPath);
|
|
2406
|
+
const tagId = `com.verimu:${sanitizeTagId2(projectName)}:${DEFAULT_SWID_VERSION}:${randomUUID6()}`;
|
|
2407
|
+
const content = [
|
|
2408
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
2409
|
+
"<SoftwareIdentity",
|
|
2410
|
+
' xmlns="http://standards.iso.org/iso/19770/-2/2015/schema.xsd"',
|
|
2411
|
+
` name="${escapeXml2(projectName)}"`,
|
|
2412
|
+
` tagId="${escapeXml2(tagId)}"`,
|
|
2413
|
+
' tagVersion="1"',
|
|
2414
|
+
` version="${DEFAULT_SWID_VERSION}"`,
|
|
2415
|
+
' versionScheme="semver">',
|
|
2416
|
+
` <Entity name="${escapeXml2(projectName)}" role="softwareCreator" />`,
|
|
2417
|
+
' <Entity name="Verimu" role="tagCreator" />',
|
|
2418
|
+
` <Meta product="${escapeXml2(projectName)}" generator="${VERIMU_TOOL_NAME}" toolVersion="${toolVersion}" generated="${timestamp}" />`,
|
|
2419
|
+
" <!-- TODO: Consider adding dependency/package evidence if we need richer SWID coverage. -->",
|
|
2420
|
+
' <Link rel="describedby" href="https://verimu.com" />',
|
|
2421
|
+
"</SoftwareIdentity>"
|
|
2422
|
+
].join("\n");
|
|
2423
|
+
return {
|
|
2424
|
+
format: "swid-xml",
|
|
2425
|
+
specVersion: SWID_SPEC_VERSION2,
|
|
2426
|
+
content,
|
|
2427
|
+
componentCount: 1,
|
|
2428
|
+
generatedAt: timestamp
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
function escapeXml2(value) {
|
|
2433
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2434
|
+
}
|
|
2435
|
+
function sanitizeTagId2(value) {
|
|
2436
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// src/sbom/artifacts.ts
|
|
2440
|
+
function generateSbomArtifacts(scanResult, toolVersion = DEFAULT_TOOL_VERSION, cyclonedxVersion = "1.7") {
|
|
2441
|
+
return {
|
|
2442
|
+
cyclonedx: new CycloneDxGenerator(cyclonedxVersion).generate(scanResult, toolVersion),
|
|
2443
|
+
spdx: new SpdxJsonGenerator().generate(scanResult, toolVersion),
|
|
2444
|
+
swid: new SwidTagGenerator().generate(scanResult, toolVersion)
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// src/cve/osv.ts
|
|
2449
|
+
var OSV_API_BASE = "https://api.osv.dev/v1";
|
|
1188
2450
|
var BATCH_SIZE = 1e3;
|
|
1189
2451
|
var OsvSource = class {
|
|
1190
2452
|
sourceId = "osv";
|
|
@@ -1450,9 +2712,13 @@ var OsvSource = class {
|
|
|
1450
2712
|
cargo: "crates.io",
|
|
1451
2713
|
maven: "Maven",
|
|
1452
2714
|
pip: "PyPI",
|
|
2715
|
+
poetry: "PyPI",
|
|
2716
|
+
uv: "PyPI",
|
|
1453
2717
|
go: "Go",
|
|
1454
2718
|
ruby: "RubyGems",
|
|
1455
|
-
composer: "Packagist"
|
|
2719
|
+
composer: "Packagist",
|
|
2720
|
+
deno: "JSR"
|
|
2721
|
+
// JSR packages (Deno registry)
|
|
1456
2722
|
};
|
|
1457
2723
|
return map[ecosystem] ?? ecosystem;
|
|
1458
2724
|
}
|
|
@@ -1559,6 +2825,9 @@ var ConsoleReporter = class {
|
|
|
1559
2825
|
lines.push("");
|
|
1560
2826
|
lines.push(` \u2713 SBOM generated (${result.sbom.format}, ${result.sbom.specVersion})`);
|
|
1561
2827
|
lines.push(` Components: ${result.sbom.componentCount}`);
|
|
2828
|
+
if (result.artifacts) {
|
|
2829
|
+
lines.push(` Also wrote: ${result.artifacts.spdx.format}, ${result.artifacts.swid.format}`);
|
|
2830
|
+
}
|
|
1562
2831
|
lines.push("");
|
|
1563
2832
|
const vulns = result.cveCheck.vulnerabilities;
|
|
1564
2833
|
if (vulns.length === 0) {
|
|
@@ -1648,18 +2917,22 @@ var VerimuApiClient = class {
|
|
|
1648
2917
|
return res.json();
|
|
1649
2918
|
}
|
|
1650
2919
|
/**
|
|
1651
|
-
* Upload a
|
|
2920
|
+
* Upload a software inventory artifact payload to a project and trigger CVE scanning.
|
|
2921
|
+
*
|
|
2922
|
+
* Backward-compatible:
|
|
2923
|
+
* - string payloads are treated as legacy raw CycloneDX JSON
|
|
2924
|
+
* - object payloads can include CycloneDX + SPDX + SWID together
|
|
1652
2925
|
*/
|
|
1653
|
-
async uploadSbom(projectId,
|
|
1654
|
-
const
|
|
2926
|
+
async uploadSbom(projectId, payload) {
|
|
2927
|
+
const body = typeof payload === "string" ? JSON.stringify(JSON.parse(payload)) : JSON.stringify(payload);
|
|
1655
2928
|
const res = await fetch(`${this.baseUrl}/api/projects/${projectId}/scan`, {
|
|
1656
2929
|
method: "POST",
|
|
1657
2930
|
headers: this.headers(),
|
|
1658
|
-
body
|
|
2931
|
+
body
|
|
1659
2932
|
});
|
|
1660
2933
|
if (!res.ok) {
|
|
1661
|
-
const
|
|
1662
|
-
throw new Error(`Verimu API: upload SBOM failed (${res.status}): ${
|
|
2934
|
+
const body2 = await res.text();
|
|
2935
|
+
throw new Error(`Verimu API: upload SBOM failed (${res.status}): ${body2}`);
|
|
1663
2936
|
}
|
|
1664
2937
|
return res.json();
|
|
1665
2938
|
}
|
|
@@ -1677,12 +2950,15 @@ var VerimuApiClient = class {
|
|
|
1677
2950
|
const map = {
|
|
1678
2951
|
npm: "npm",
|
|
1679
2952
|
pip: "pip",
|
|
2953
|
+
poetry: "poetry",
|
|
2954
|
+
uv: "uv",
|
|
1680
2955
|
maven: "maven",
|
|
1681
2956
|
nuget: "nuget",
|
|
1682
2957
|
go: "gomod",
|
|
1683
2958
|
cargo: "cargo",
|
|
1684
2959
|
ruby: "bundler",
|
|
1685
|
-
composer: "composer"
|
|
2960
|
+
composer: "composer",
|
|
2961
|
+
deno: "deno"
|
|
1686
2962
|
};
|
|
1687
2963
|
return map[eco] ?? eco;
|
|
1688
2964
|
}
|
|
@@ -1693,13 +2969,19 @@ async function scan(config) {
|
|
|
1693
2969
|
const {
|
|
1694
2970
|
projectPath,
|
|
1695
2971
|
sbomOutput = "./sbom.cdx.json",
|
|
1696
|
-
skipCveCheck = false
|
|
2972
|
+
skipCveCheck = false,
|
|
2973
|
+
cyclonedxVersion = "1.7"
|
|
1697
2974
|
} = config;
|
|
1698
2975
|
const registry = new ScannerRegistry();
|
|
1699
2976
|
const scanResult = await registry.detectAndScan(projectPath);
|
|
1700
|
-
const
|
|
1701
|
-
const sbom =
|
|
1702
|
-
|
|
2977
|
+
const artifacts = generateSbomArtifacts(scanResult, void 0, cyclonedxVersion);
|
|
2978
|
+
const sbom = artifacts.cyclonedx;
|
|
2979
|
+
const outputPaths = deriveArtifactOutputPaths(sbomOutput);
|
|
2980
|
+
await Promise.all([
|
|
2981
|
+
writeFile(outputPaths.cyclonedx, artifacts.cyclonedx.content, "utf-8"),
|
|
2982
|
+
writeFile(outputPaths.spdx, artifacts.spdx.content, "utf-8"),
|
|
2983
|
+
writeFile(outputPaths.swid, artifacts.swid.content, "utf-8")
|
|
2984
|
+
]);
|
|
1703
2985
|
let cveCheck;
|
|
1704
2986
|
if (skipCveCheck) {
|
|
1705
2987
|
cveCheck = {
|
|
@@ -1728,6 +3010,7 @@ async function scan(config) {
|
|
|
1728
3010
|
dependencyCount: scanResult.dependencies.length
|
|
1729
3011
|
},
|
|
1730
3012
|
sbom,
|
|
3013
|
+
artifacts,
|
|
1731
3014
|
cveCheck,
|
|
1732
3015
|
summary,
|
|
1733
3016
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1752,7 +3035,7 @@ async function uploadToVerimu(report, config) {
|
|
|
1752
3035
|
ecosystem: report.project.ecosystem
|
|
1753
3036
|
});
|
|
1754
3037
|
const projectId = upsertRes.project.id;
|
|
1755
|
-
const scanRes = await client.uploadSbom(projectId, report
|
|
3038
|
+
const scanRes = await client.uploadSbom(projectId, buildUploadPayload(report));
|
|
1756
3039
|
return {
|
|
1757
3040
|
projectId,
|
|
1758
3041
|
projectCreated: upsertRes.created,
|
|
@@ -1779,6 +3062,28 @@ function printReport(report) {
|
|
|
1779
3062
|
const reporter = new ConsoleReporter();
|
|
1780
3063
|
console.log(reporter.report(report));
|
|
1781
3064
|
}
|
|
3065
|
+
function deriveArtifactOutputPaths(cycloneDxOutput) {
|
|
3066
|
+
const parsed = parse(cycloneDxOutput);
|
|
3067
|
+
let baseName = parsed.name;
|
|
3068
|
+
if (parsed.ext === ".json" && baseName.endsWith(".cdx")) {
|
|
3069
|
+
baseName = baseName.slice(0, -4);
|
|
3070
|
+
}
|
|
3071
|
+
return {
|
|
3072
|
+
cyclonedx: cycloneDxOutput,
|
|
3073
|
+
spdx: join(parsed.dir, `${baseName}.spdx.json`),
|
|
3074
|
+
swid: join(parsed.dir, `${baseName}.swid.xml`)
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
function buildUploadPayload(report) {
|
|
3078
|
+
if (!report.artifacts) {
|
|
3079
|
+
return report.sbom.content;
|
|
3080
|
+
}
|
|
3081
|
+
return {
|
|
3082
|
+
cyclonedx: JSON.parse(report.artifacts.cyclonedx.content),
|
|
3083
|
+
spdx: JSON.parse(report.artifacts.spdx.content),
|
|
3084
|
+
swid: report.artifacts.swid.content
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
1782
3087
|
export {
|
|
1783
3088
|
ApiKeyRequiredError,
|
|
1784
3089
|
CargoScanner,
|
|
@@ -1787,6 +3092,7 @@ export {
|
|
|
1787
3092
|
CveAggregator,
|
|
1788
3093
|
CveSourceError,
|
|
1789
3094
|
CycloneDxGenerator,
|
|
3095
|
+
DenoScanner,
|
|
1790
3096
|
GoScanner,
|
|
1791
3097
|
LockfileParseError,
|
|
1792
3098
|
MavenScanner,
|
|
@@ -1795,11 +3101,18 @@ export {
|
|
|
1795
3101
|
NugetScanner,
|
|
1796
3102
|
OsvSource,
|
|
1797
3103
|
PipScanner,
|
|
3104
|
+
PnpmScanner,
|
|
1798
3105
|
RubyScanner,
|
|
1799
3106
|
ScannerRegistry,
|
|
3107
|
+
SpdxJsonGenerator,
|
|
3108
|
+
SwidTagGenerator,
|
|
1800
3109
|
VerimuApiClient,
|
|
1801
3110
|
VerimuError,
|
|
3111
|
+
YarnScanner,
|
|
1802
3112
|
generateSbom,
|
|
3113
|
+
generateSbomArtifacts,
|
|
3114
|
+
generateSpdxSbom,
|
|
3115
|
+
generateSwidTag,
|
|
1803
3116
|
printReport,
|
|
1804
3117
|
scan,
|
|
1805
3118
|
shouldFailCi,
|