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/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 = "0.0.0",
56
+ projectVersion = DEFAULT_PROJECT_VERSION,
7
57
  dependencies
8
58
  } = input;
9
59
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
10
- const resolvedDeps = dependencies.map((dep) => ({
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: "verimu",
74
+ name: VERIMU_TOOL_NAME,
29
75
  version: "0.0.1",
30
- description: "Verimu CRA Compliance Scanner",
76
+ description: VERIMU_TOOL_DESCRIPTION,
31
77
  supplier: { name: "Verimu" },
32
78
  externalReferences: [
33
- { type: "website", url: "https://verimu.com" }
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: deriveSupplierName(dep.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
- var PURL_TYPE_MAP = {
73
- npm: "npm",
74
- nuget: "nuget",
75
- cargo: "cargo",
76
- maven: "maven",
77
- pip: "pypi",
78
- go: "golang",
79
- ruby: "gem",
80
- composer: "composer"
81
- };
82
- function buildPurl(name, version, ecosystem) {
83
- const type = PURL_TYPE_MAP[ecosystem] || ecosystem;
84
- if (ecosystem === "npm" && name.startsWith("@")) {
85
- return `pkg:${type}/%40${name.slice(1)}@${version}`;
86
- }
87
- return `pkg:${type}/${name}@${version}`;
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
- function deriveSupplierName(packageName) {
90
- if (packageName.startsWith("@")) {
91
- return packageName.split("/")[0];
92
- }
93
- return packageName;
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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 (Python), pom.xml (Maven), go.sum (Go), Gemfile.lock (Ruby), composer.lock (Composer)`,
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/registry.ts
1034
- var ScannerRegistry = class {
1035
- scanners;
1036
- constructor() {
1037
- this.scanners = [
1038
- new NpmScanner(),
1039
- new NugetScanner(),
1040
- new CargoScanner(),
1041
- new PipScanner(),
1042
- new MavenScanner(),
1043
- new GoScanner(),
1044
- new RubyScanner(),
1045
- new ComposerScanner()
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
- * Auto-detects the project's ecosystem and scans dependencies.
1050
- * Tries each registered scanner in order until one matches.
1216
+ * Parses yarn.lock file and extracts dependencies.
1217
+ * Automatically detects and handles both v1 (Classic) and v2+ (Berry) formats.
1051
1218
  */
1052
- async detectAndScan(projectPath) {
1053
- for (const scanner of this.scanners) {
1054
- const lockfilePath = await scanner.detect(projectPath);
1055
- if (lockfilePath) {
1056
- return scanner.scan(projectPath, lockfilePath);
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
- // src/sbom/cyclonedx.ts
1072
- import { randomUUID as randomUUID2 } from "crypto";
1073
- var CycloneDxGenerator = class {
1074
- format = "cyclonedx-json";
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
- buildBom(scanResult, toolVersion) {
1087
- const projectName = this.extractProjectName(scanResult.projectPath);
1088
- return {
1089
- $schema: "http://cyclonedx.org/schema/bom-1.7.schema.json",
1090
- bomFormat: "CycloneDX",
1091
- specVersion: "1.7",
1092
- serialNumber: `urn:uuid:${randomUUID2()}`,
1093
- version: 1,
1094
- metadata: {
1095
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1096
- tools: {
1097
- components: [
1098
- {
1099
- type: "application",
1100
- name: "verimu",
1101
- version: toolVersion,
1102
- description: "Verimu CRA Compliance Scanner",
1103
- supplier: { name: "Verimu" },
1104
- externalReferences: [
1105
- {
1106
- type: "website",
1107
- url: "https://verimu.com"
1108
- }
1109
- ]
1110
- }
1111
- ]
1112
- },
1113
- // NTIA: metadata.supplier the org supplying the root software
1114
- supplier: {
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
- components: scanResult.dependencies.map((dep) => this.toComponent(dep)),
1125
- dependencies: this.buildDependencyGraph(scanResult)
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
- /** Converts a Verimu Dependency to a CycloneDX component */
1129
- toComponent(dep) {
1130
- return {
1131
- type: "library",
1132
- name: dep.name,
1133
- version: dep.version,
1134
- purl: dep.purl,
1135
- "bom-ref": dep.purl,
1136
- scope: dep.direct ? "required" : "optional",
1137
- // NTIA: component.supplier — derived from npm scope or package name
1138
- supplier: {
1139
- name: this.deriveSupplierName(dep.name)
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
- * Derives a supplier name from a package name.
1145
- *
1146
- * For scoped packages like "@vue/reactivity" → "@vue"
1147
- * For unscoped packages like "express" → "express"
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
- deriveSupplierName(packageName) {
1153
- if (packageName.startsWith("@")) {
1154
- const scope = packageName.split("/")[0];
1155
- return scope;
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 packageName;
1343
+ return null;
1158
1344
  }
1159
1345
  /**
1160
- * Builds the dependency graph section of the SBOM.
1346
+ * Parses Yarn v1 (Classic) lockfile format.
1161
1347
  *
1162
- * The root component depends on all dependencies (direct + transitive).
1163
- * This ensures a single root node in the graph, which NTIA validators expect.
1164
- *
1165
- * We include ALL deps under root (not just direct) because from a flat lockfile
1166
- * we can't reliably reconstruct which transitive dep belongs to which direct dep.
1167
- * This is still valid per the CycloneDX spec — it represents a complete but flat
1168
- * dependency relationship.
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
- buildDependencyGraph(scanResult) {
1171
- const allDepPurls = scanResult.dependencies.map((d) => d.purl);
1172
- return [
1173
- {
1174
- ref: "root-component",
1175
- dependsOn: allDepPurls
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
- /** Extracts project name from path */
1180
- extractProjectName(projectPath) {
1181
- const parts = projectPath.replace(/\\/g, "/").split("/");
1182
- return parts[parts.length - 1] || "unknown-project";
1183
- }
1184
- };
1185
-
1186
- // src/cve/osv.ts
1187
- var OSV_API_BASE = "https://api.osv.dev/v1";
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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 CycloneDX SBOM to a project and trigger CVE scanning.
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, sbomContent) {
1654
- const sbomJson = JSON.parse(sbomContent);
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: JSON.stringify(sbomJson)
2931
+ body
1659
2932
  });
1660
2933
  if (!res.ok) {
1661
- const body = await res.text();
1662
- throw new Error(`Verimu API: upload SBOM failed (${res.status}): ${body}`);
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 sbomGenerator = new CycloneDxGenerator();
1701
- const sbom = sbomGenerator.generate(scanResult);
1702
- await writeFile(sbomOutput, sbom.content, "utf-8");
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.sbom.content);
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,