verimu 0.0.19 → 0.0.21
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/cli.mjs +458 -41
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +10 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.mjs +10 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -14178,9 +14178,10 @@ var NpmScanner = class {
|
|
|
14178
14178
|
if (lockfile.packages) {
|
|
14179
14179
|
for (const [pkgPath, pkgInfo] of Object.entries(lockfile.packages)) {
|
|
14180
14180
|
if (pkgPath === "") continue;
|
|
14181
|
+
if (!pkgPath.startsWith("node_modules/")) continue;
|
|
14182
|
+
if (pkgInfo.link) continue;
|
|
14181
14183
|
const name = this.extractPackageName(pkgPath);
|
|
14182
14184
|
if (!name || !pkgInfo.version) continue;
|
|
14183
|
-
if (pkgInfo.link) continue;
|
|
14184
14185
|
deps.push({
|
|
14185
14186
|
name,
|
|
14186
14187
|
version: pkgInfo.version,
|
|
@@ -15468,8 +15469,8 @@ var PnpmScanner = class {
|
|
|
15468
15469
|
* "/pkg@1.0.0(dep@2.0.0)" → name: "pkg", version: "1.0.0"
|
|
15469
15470
|
*/
|
|
15470
15471
|
parsePackagePath(pkgPath, lockfileVersion) {
|
|
15471
|
-
const
|
|
15472
|
-
const cleanPath =
|
|
15472
|
+
const path15 = pkgPath.startsWith("/") ? pkgPath.slice(1) : pkgPath;
|
|
15473
|
+
const cleanPath = path15.split("_")[0].split("(")[0];
|
|
15473
15474
|
if (!cleanPath) {
|
|
15474
15475
|
return { name: null, version: null };
|
|
15475
15476
|
}
|
|
@@ -15481,30 +15482,30 @@ var PnpmScanner = class {
|
|
|
15481
15482
|
/**
|
|
15482
15483
|
* Parses v6+ format: "express@4.18.2" or "@types/node@20.11.5"
|
|
15483
15484
|
*/
|
|
15484
|
-
parseV6Format(
|
|
15485
|
-
if (
|
|
15486
|
-
const lastAtIndex =
|
|
15485
|
+
parseV6Format(path15) {
|
|
15486
|
+
if (path15.startsWith("@")) {
|
|
15487
|
+
const lastAtIndex = path15.lastIndexOf("@");
|
|
15487
15488
|
if (lastAtIndex <= 0) {
|
|
15488
15489
|
return { name: null, version: null };
|
|
15489
15490
|
}
|
|
15490
|
-
const name2 =
|
|
15491
|
-
const version2 =
|
|
15491
|
+
const name2 = path15.substring(0, lastAtIndex);
|
|
15492
|
+
const version2 = path15.substring(lastAtIndex + 1);
|
|
15492
15493
|
return { name: name2, version: version2 };
|
|
15493
15494
|
}
|
|
15494
|
-
const atIndex =
|
|
15495
|
+
const atIndex = path15.indexOf("@");
|
|
15495
15496
|
if (atIndex < 0) {
|
|
15496
15497
|
return { name: null, version: null };
|
|
15497
15498
|
}
|
|
15498
|
-
const name =
|
|
15499
|
-
const version =
|
|
15499
|
+
const name = path15.substring(0, atIndex);
|
|
15500
|
+
const version = path15.substring(atIndex + 1);
|
|
15500
15501
|
return { name, version };
|
|
15501
15502
|
}
|
|
15502
15503
|
/**
|
|
15503
15504
|
* Parses v5.x format: "express/4.18.2" or "@types/node/20.11.5"
|
|
15504
15505
|
*/
|
|
15505
|
-
parseV5Format(
|
|
15506
|
-
if (
|
|
15507
|
-
const parts =
|
|
15506
|
+
parseV5Format(path15) {
|
|
15507
|
+
if (path15.startsWith("@")) {
|
|
15508
|
+
const parts = path15.split("/");
|
|
15508
15509
|
if (parts.length < 3) {
|
|
15509
15510
|
return { name: null, version: null };
|
|
15510
15511
|
}
|
|
@@ -15512,12 +15513,12 @@ var PnpmScanner = class {
|
|
|
15512
15513
|
const version2 = parts[2];
|
|
15513
15514
|
return { name: name2, version: version2 };
|
|
15514
15515
|
}
|
|
15515
|
-
const slashIndex =
|
|
15516
|
+
const slashIndex = path15.indexOf("/");
|
|
15516
15517
|
if (slashIndex < 0) {
|
|
15517
15518
|
return { name: null, version: null };
|
|
15518
15519
|
}
|
|
15519
|
-
const name =
|
|
15520
|
-
const version =
|
|
15520
|
+
const name = path15.substring(0, slashIndex);
|
|
15521
|
+
const version = path15.substring(slashIndex + 1);
|
|
15521
15522
|
return { name, version };
|
|
15522
15523
|
}
|
|
15523
15524
|
/**
|
|
@@ -16825,7 +16826,8 @@ var VerimuApiClient = class {
|
|
|
16825
16826
|
name: opts.name,
|
|
16826
16827
|
ecosystem: this.mapEcosystem(opts.ecosystem),
|
|
16827
16828
|
repository_url: opts.repositoryUrl ?? null,
|
|
16828
|
-
platform: opts.platform ?? null
|
|
16829
|
+
platform: opts.platform ?? null,
|
|
16830
|
+
group_name: opts.groupName ?? null
|
|
16829
16831
|
})
|
|
16830
16832
|
});
|
|
16831
16833
|
if (!res.ok) {
|
|
@@ -16977,6 +16979,8 @@ function buildSnippet(params) {
|
|
|
16977
16979
|
const startLine = Math.max(1, centerLine - numContextLines);
|
|
16978
16980
|
const endLine = Math.min(lines.length || 1, centerLine + numContextLines);
|
|
16979
16981
|
const code = lines.slice(startLine - 1, endLine).join("\n");
|
|
16982
|
+
const highlightOffset = centerLine - startLine;
|
|
16983
|
+
const highlight = [highlightOffset, highlightOffset];
|
|
16980
16984
|
return {
|
|
16981
16985
|
filePath: relative(projectPath, filePath).split(sep).join("/"),
|
|
16982
16986
|
startLine,
|
|
@@ -16984,7 +16988,8 @@ function buildSnippet(params) {
|
|
|
16984
16988
|
code,
|
|
16985
16989
|
matchKind,
|
|
16986
16990
|
calledSymbol,
|
|
16987
|
-
confidence
|
|
16991
|
+
confidence,
|
|
16992
|
+
highlight
|
|
16988
16993
|
};
|
|
16989
16994
|
}
|
|
16990
16995
|
function dedupeSnippets(snippets) {
|
|
@@ -17128,34 +17133,34 @@ var JsAstAnalyzer = class {
|
|
|
17128
17133
|
matchCandidates.push({ packageKey: packageKey3, line, matchKind, calledSymbol, confidence });
|
|
17129
17134
|
};
|
|
17130
17135
|
traverseFn(ast, {
|
|
17131
|
-
ImportDeclaration: (
|
|
17132
|
-
const source =
|
|
17136
|
+
ImportDeclaration: (path15) => {
|
|
17137
|
+
const source = path15.node.source;
|
|
17133
17138
|
if (!(0, import_types.isStringLiteral)(source)) return;
|
|
17134
17139
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17135
17140
|
if (!pkgKey) return;
|
|
17136
|
-
addMatch(pkgKey,
|
|
17137
|
-
for (const specifier of
|
|
17141
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "import", void 0, 0.95);
|
|
17142
|
+
for (const specifier of path15.node.specifiers) {
|
|
17138
17143
|
if ((0, import_types.isImportDefaultSpecifier)(specifier) || (0, import_types.isImportNamespaceSpecifier)(specifier) || (0, import_types.isImportSpecifier)(specifier)) {
|
|
17139
17144
|
symbolToPackage.set(specifier.local.name, pkgKey);
|
|
17140
17145
|
}
|
|
17141
17146
|
}
|
|
17142
17147
|
},
|
|
17143
|
-
ExportNamedDeclaration: (
|
|
17144
|
-
const source =
|
|
17148
|
+
ExportNamedDeclaration: (path15) => {
|
|
17149
|
+
const source = path15.node.source;
|
|
17145
17150
|
if (!source || !(0, import_types.isStringLiteral)(source)) return;
|
|
17146
17151
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17147
17152
|
if (!pkgKey) return;
|
|
17148
|
-
addMatch(pkgKey,
|
|
17153
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "export_from", void 0, 0.85);
|
|
17149
17154
|
},
|
|
17150
|
-
ExportAllDeclaration: (
|
|
17151
|
-
const source =
|
|
17155
|
+
ExportAllDeclaration: (path15) => {
|
|
17156
|
+
const source = path15.node.source;
|
|
17152
17157
|
if (!(0, import_types.isStringLiteral)(source)) return;
|
|
17153
17158
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17154
17159
|
if (!pkgKey) return;
|
|
17155
|
-
addMatch(pkgKey,
|
|
17160
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "export_from", void 0, 0.85);
|
|
17156
17161
|
},
|
|
17157
|
-
VariableDeclarator: (
|
|
17158
|
-
const node =
|
|
17162
|
+
VariableDeclarator: (path15) => {
|
|
17163
|
+
const node = path15.node;
|
|
17159
17164
|
if (!(0, import_types.isVariableDeclarator)(node)) return;
|
|
17160
17165
|
if (!node.init || !(0, import_types.isCallExpression)(node.init)) return;
|
|
17161
17166
|
if (!(0, import_types.isIdentifier)(node.init.callee, { name: "require" })) return;
|
|
@@ -17168,8 +17173,8 @@ var JsAstAnalyzer = class {
|
|
|
17168
17173
|
symbolToPackage.set(identifier, pkgKey);
|
|
17169
17174
|
}
|
|
17170
17175
|
},
|
|
17171
|
-
CallExpression: (
|
|
17172
|
-
const node =
|
|
17176
|
+
CallExpression: (path15) => {
|
|
17177
|
+
const node = path15.node;
|
|
17173
17178
|
if ((0, import_types.isIdentifier)(node.callee, { name: "require" })) {
|
|
17174
17179
|
const firstArg = node.arguments[0];
|
|
17175
17180
|
if (firstArg && (0, import_types.isStringLiteral)(firstArg)) {
|
|
@@ -17190,12 +17195,12 @@ var JsAstAnalyzer = class {
|
|
|
17190
17195
|
0.75
|
|
17191
17196
|
);
|
|
17192
17197
|
},
|
|
17193
|
-
ImportExpression: (
|
|
17194
|
-
const source =
|
|
17198
|
+
ImportExpression: (path15) => {
|
|
17199
|
+
const source = path15.node.source;
|
|
17195
17200
|
if (!(0, import_types.isStringLiteral)(source)) return;
|
|
17196
17201
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17197
17202
|
if (!pkgKey) return;
|
|
17198
|
-
addMatch(pkgKey,
|
|
17203
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "dynamic_import", void 0, 0.9);
|
|
17199
17204
|
}
|
|
17200
17205
|
});
|
|
17201
17206
|
for (const candidate of matchCandidates) {
|
|
@@ -18743,7 +18748,8 @@ async function uploadToVerimu(report, config) {
|
|
|
18743
18748
|
const projectName = basename(config.projectPath);
|
|
18744
18749
|
const upsertRes = await client.upsertProject({
|
|
18745
18750
|
name: projectName,
|
|
18746
|
-
ecosystem: report.project.ecosystem
|
|
18751
|
+
ecosystem: report.project.ecosystem,
|
|
18752
|
+
groupName: config.groupName
|
|
18747
18753
|
});
|
|
18748
18754
|
const projectId = upsertRes.project.id;
|
|
18749
18755
|
const scanRes = await client.uploadSbom(projectId, buildUploadPayload(report));
|
|
@@ -18945,6 +18951,325 @@ function severityBadge2(severity) {
|
|
|
18945
18951
|
return badges[severity] ?? "[???] ";
|
|
18946
18952
|
}
|
|
18947
18953
|
|
|
18954
|
+
// src/discovery/lockfile-discovery.ts
|
|
18955
|
+
import { readdir as readdir3, stat } from "fs/promises";
|
|
18956
|
+
import { existsSync as existsSync14 } from "fs";
|
|
18957
|
+
import path14 from "path";
|
|
18958
|
+
var LOCKFILE_MAP = {
|
|
18959
|
+
"pnpm-lock.yaml": { ecosystem: "npm", scanner: "pnpm" },
|
|
18960
|
+
"yarn.lock": { ecosystem: "npm", scanner: "yarn" },
|
|
18961
|
+
"package-lock.json": { ecosystem: "npm", scanner: "npm" },
|
|
18962
|
+
"deno.lock": { ecosystem: "npm", scanner: "deno" },
|
|
18963
|
+
"Cargo.lock": { ecosystem: "cargo", scanner: "cargo" },
|
|
18964
|
+
"go.sum": { ecosystem: "go", scanner: "go" },
|
|
18965
|
+
"Gemfile.lock": { ecosystem: "ruby", scanner: "ruby" },
|
|
18966
|
+
"composer.lock": { ecosystem: "composer", scanner: "composer" },
|
|
18967
|
+
"packages.lock.json": { ecosystem: "nuget", scanner: "nuget" },
|
|
18968
|
+
"poetry.lock": { ecosystem: "poetry", scanner: "poetry" },
|
|
18969
|
+
"uv.lock": { ecosystem: "uv", scanner: "uv" },
|
|
18970
|
+
"Pipfile.lock": { ecosystem: "pip", scanner: "pip" },
|
|
18971
|
+
"requirements.txt": { ecosystem: "pip", scanner: "pip" },
|
|
18972
|
+
"pom.xml": { ecosystem: "maven", scanner: "maven" }
|
|
18973
|
+
};
|
|
18974
|
+
var DEFAULT_EXCLUDES = [
|
|
18975
|
+
"node_modules",
|
|
18976
|
+
".git",
|
|
18977
|
+
".hg",
|
|
18978
|
+
".svn",
|
|
18979
|
+
"vendor",
|
|
18980
|
+
"target",
|
|
18981
|
+
"dist",
|
|
18982
|
+
"build",
|
|
18983
|
+
".next",
|
|
18984
|
+
".nuxt",
|
|
18985
|
+
"__pycache__",
|
|
18986
|
+
".venv",
|
|
18987
|
+
"venv",
|
|
18988
|
+
".tox",
|
|
18989
|
+
"coverage",
|
|
18990
|
+
".cache",
|
|
18991
|
+
"out",
|
|
18992
|
+
".output"
|
|
18993
|
+
];
|
|
18994
|
+
var LockfileDiscovery = class {
|
|
18995
|
+
lockfileNames = Object.keys(LOCKFILE_MAP);
|
|
18996
|
+
/**
|
|
18997
|
+
* Discovers all lockfiles recursively starting from rootPath.
|
|
18998
|
+
* Returns a list of projects that can be scanned.
|
|
18999
|
+
*/
|
|
19000
|
+
async discover(options) {
|
|
19001
|
+
const { rootPath, exclude, maxDepth } = options;
|
|
19002
|
+
const absoluteRoot = path14.resolve(rootPath);
|
|
19003
|
+
const discovered = [];
|
|
19004
|
+
const excludePatterns = this.buildExcludePatterns(exclude);
|
|
19005
|
+
await this.walkDirectory(absoluteRoot, absoluteRoot, discovered, {
|
|
19006
|
+
excludePatterns,
|
|
19007
|
+
maxDepth: maxDepth ?? Infinity,
|
|
19008
|
+
//not set, infinite for now, can add as a cli-flag later if needed
|
|
19009
|
+
currentDepth: 0
|
|
19010
|
+
});
|
|
19011
|
+
discovered.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
19012
|
+
return discovered;
|
|
19013
|
+
}
|
|
19014
|
+
/**
|
|
19015
|
+
* Recursively walks directories looking for lockfiles.
|
|
19016
|
+
* Stops descending into a directory once a lockfile is found
|
|
19017
|
+
* (to avoid scanning nested node_modules, etc.)
|
|
19018
|
+
*/
|
|
19019
|
+
async walkDirectory(currentPath, rootPath, results, options) {
|
|
19020
|
+
const { excludePatterns, maxDepth, currentDepth } = options;
|
|
19021
|
+
if (currentDepth > maxDepth) return;
|
|
19022
|
+
const relativePath = path14.relative(rootPath, currentPath) || ".";
|
|
19023
|
+
if (this.matchesAnyPattern(relativePath, excludePatterns)) {
|
|
19024
|
+
return;
|
|
19025
|
+
}
|
|
19026
|
+
const foundLockfile = await this.findLockfileInDir(currentPath);
|
|
19027
|
+
if (foundLockfile) {
|
|
19028
|
+
results.push({
|
|
19029
|
+
projectPath: currentPath,
|
|
19030
|
+
relativePath,
|
|
19031
|
+
lockfile: {
|
|
19032
|
+
name: foundLockfile.name,
|
|
19033
|
+
path: path14.join(currentPath, foundLockfile.name)
|
|
19034
|
+
},
|
|
19035
|
+
ecosystem: foundLockfile.ecosystem,
|
|
19036
|
+
scannerType: foundLockfile.scanner
|
|
19037
|
+
});
|
|
19038
|
+
return;
|
|
19039
|
+
}
|
|
19040
|
+
let entries;
|
|
19041
|
+
try {
|
|
19042
|
+
entries = await readdir3(currentPath);
|
|
19043
|
+
} catch {
|
|
19044
|
+
return;
|
|
19045
|
+
}
|
|
19046
|
+
for (const entry of entries) {
|
|
19047
|
+
const entryPath = path14.join(currentPath, entry);
|
|
19048
|
+
try {
|
|
19049
|
+
const stats = await stat(entryPath);
|
|
19050
|
+
if (stats.isDirectory()) {
|
|
19051
|
+
if (this.isDefaultExclude(entry)) continue;
|
|
19052
|
+
await this.walkDirectory(entryPath, rootPath, results, {
|
|
19053
|
+
...options,
|
|
19054
|
+
currentDepth: currentDepth + 1
|
|
19055
|
+
});
|
|
19056
|
+
}
|
|
19057
|
+
} catch {
|
|
19058
|
+
}
|
|
19059
|
+
}
|
|
19060
|
+
}
|
|
19061
|
+
/**
|
|
19062
|
+
* Looks for a lockfile in the given directory.
|
|
19063
|
+
* Returns the first match in priority order.
|
|
19064
|
+
*/
|
|
19065
|
+
async findLockfileInDir(dirPath) {
|
|
19066
|
+
const priorityOrder = [
|
|
19067
|
+
"pnpm-lock.yaml",
|
|
19068
|
+
"yarn.lock",
|
|
19069
|
+
"package-lock.json",
|
|
19070
|
+
"deno.lock",
|
|
19071
|
+
"Cargo.lock",
|
|
19072
|
+
"go.sum",
|
|
19073
|
+
"poetry.lock",
|
|
19074
|
+
"uv.lock",
|
|
19075
|
+
"Pipfile.lock",
|
|
19076
|
+
"composer.lock",
|
|
19077
|
+
"Gemfile.lock",
|
|
19078
|
+
"packages.lock.json",
|
|
19079
|
+
"pom.xml",
|
|
19080
|
+
"requirements.txt"
|
|
19081
|
+
];
|
|
19082
|
+
for (const lockfileName of priorityOrder) {
|
|
19083
|
+
const lockfilePath = path14.join(dirPath, lockfileName);
|
|
19084
|
+
if (existsSync14(lockfilePath)) {
|
|
19085
|
+
const info = LOCKFILE_MAP[lockfileName];
|
|
19086
|
+
return { name: lockfileName, ...info };
|
|
19087
|
+
}
|
|
19088
|
+
}
|
|
19089
|
+
return null;
|
|
19090
|
+
}
|
|
19091
|
+
/**
|
|
19092
|
+
* Builds exclude patterns from user input + defaults.
|
|
19093
|
+
*/
|
|
19094
|
+
buildExcludePatterns(userExcludes) {
|
|
19095
|
+
const patterns = [];
|
|
19096
|
+
for (const dir of DEFAULT_EXCLUDES) {
|
|
19097
|
+
patterns.push(`**/${dir}`);
|
|
19098
|
+
patterns.push(`**/${dir}/**`);
|
|
19099
|
+
}
|
|
19100
|
+
if (userExcludes) {
|
|
19101
|
+
patterns.push(...userExcludes);
|
|
19102
|
+
}
|
|
19103
|
+
return patterns;
|
|
19104
|
+
}
|
|
19105
|
+
/**
|
|
19106
|
+
* Quick check if a directory name is in the default exclude list.
|
|
19107
|
+
*/
|
|
19108
|
+
isDefaultExclude(dirName) {
|
|
19109
|
+
return DEFAULT_EXCLUDES.includes(dirName);
|
|
19110
|
+
}
|
|
19111
|
+
/**
|
|
19112
|
+
* Checks if a path matches any of the given glob patterns.
|
|
19113
|
+
* Uses simple glob matching (supports *, **, ?).
|
|
19114
|
+
*/
|
|
19115
|
+
matchesAnyPattern(relativePath, patterns) {
|
|
19116
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
19117
|
+
for (const pattern of patterns) {
|
|
19118
|
+
if (this.matchGlob(normalized, pattern)) {
|
|
19119
|
+
return true;
|
|
19120
|
+
}
|
|
19121
|
+
}
|
|
19122
|
+
return false;
|
|
19123
|
+
}
|
|
19124
|
+
/**
|
|
19125
|
+
* Simple glob matcher supporting:
|
|
19126
|
+
* - * (matches any characters except /)
|
|
19127
|
+
* - ** (matches any characters including /)
|
|
19128
|
+
* - ? (matches single character)
|
|
19129
|
+
*/
|
|
19130
|
+
matchGlob(str, pattern) {
|
|
19131
|
+
let regex = pattern.replace(/\\/g, "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*").replace(/\?/g, ".");
|
|
19132
|
+
regex = `^${regex}$`;
|
|
19133
|
+
return new RegExp(regex).test(str);
|
|
19134
|
+
}
|
|
19135
|
+
};
|
|
19136
|
+
|
|
19137
|
+
// src/discovery/orchestrator.ts
|
|
19138
|
+
import { basename as basename2 } from "path";
|
|
19139
|
+
var MultiProjectOrchestrator = class {
|
|
19140
|
+
discovery = new LockfileDiscovery();
|
|
19141
|
+
reporter = new ConsoleReporter();
|
|
19142
|
+
/**
|
|
19143
|
+
* Gets a display name for a project (uses directory name instead of "." for root)
|
|
19144
|
+
*/
|
|
19145
|
+
getDisplayName(project, rootPath) {
|
|
19146
|
+
if (project.relativePath === ".") {
|
|
19147
|
+
return basename2(rootPath);
|
|
19148
|
+
}
|
|
19149
|
+
return project.relativePath;
|
|
19150
|
+
}
|
|
19151
|
+
/**
|
|
19152
|
+
* Discovers and scans all projects in a directory tree.
|
|
19153
|
+
*/
|
|
19154
|
+
async scanAll(config) {
|
|
19155
|
+
const startTime = Date.now();
|
|
19156
|
+
const apiKey = config.apiKey;
|
|
19157
|
+
const apiBaseUrl = config.apiBaseUrl;
|
|
19158
|
+
console.log(`
|
|
19159
|
+
Discovering projects in ${config.projectPath}...`);
|
|
19160
|
+
const projects = await this.discovery.discover({
|
|
19161
|
+
rootPath: config.projectPath,
|
|
19162
|
+
exclude: config.exclude
|
|
19163
|
+
});
|
|
19164
|
+
if (projects.length === 0) {
|
|
19165
|
+
console.log("No projects with lockfiles found.");
|
|
19166
|
+
return {
|
|
19167
|
+
totalDiscovered: 0,
|
|
19168
|
+
successful: [],
|
|
19169
|
+
failed: [],
|
|
19170
|
+
skipped: [],
|
|
19171
|
+
durationMs: Date.now() - startTime
|
|
19172
|
+
};
|
|
19173
|
+
}
|
|
19174
|
+
const isSingleProject = projects.length === 1;
|
|
19175
|
+
const groupName = isSingleProject ? config.groupName : config.groupName || basename2(config.projectPath);
|
|
19176
|
+
console.log(`Found ${projects.length} project(s):
|
|
19177
|
+
`);
|
|
19178
|
+
for (const p of projects) {
|
|
19179
|
+
const displayName = this.getDisplayName(p, config.projectPath);
|
|
19180
|
+
console.log(` \u2022 ${displayName} (${p.scannerType})`);
|
|
19181
|
+
}
|
|
19182
|
+
if (!isSingleProject) {
|
|
19183
|
+
if (!config.groupName) {
|
|
19184
|
+
console.log(`
|
|
19185
|
+
\u2139 Auto-grouping projects as: "${groupName}"`);
|
|
19186
|
+
console.log(" (Use --group to specify a custom group name)\n");
|
|
19187
|
+
} else {
|
|
19188
|
+
console.log(`
|
|
19189
|
+
\u2139 Grouping projects as: "${groupName}"
|
|
19190
|
+
`);
|
|
19191
|
+
}
|
|
19192
|
+
} else {
|
|
19193
|
+
console.log("");
|
|
19194
|
+
}
|
|
19195
|
+
const successful = [];
|
|
19196
|
+
const failed = [];
|
|
19197
|
+
for (let i = 0; i < projects.length; i++) {
|
|
19198
|
+
const project = projects[i];
|
|
19199
|
+
const displayName = this.getDisplayName(project, config.projectPath);
|
|
19200
|
+
console.log("\u2500".repeat(60));
|
|
19201
|
+
console.log(`[${i + 1}/${projects.length}] Scanning: ${displayName}`);
|
|
19202
|
+
console.log("\u2500".repeat(60));
|
|
19203
|
+
console.log("");
|
|
19204
|
+
try {
|
|
19205
|
+
const sbomOutput = this.deriveSbomPath(project, config.sbomOutput);
|
|
19206
|
+
const report = await scan({
|
|
19207
|
+
...config,
|
|
19208
|
+
projectPath: project.projectPath,
|
|
19209
|
+
sbomOutput,
|
|
19210
|
+
groupName,
|
|
19211
|
+
// Use auto-derived or user-provided group name
|
|
19212
|
+
apiKey: void 0
|
|
19213
|
+
// Don't upload in scan, do it separately
|
|
19214
|
+
});
|
|
19215
|
+
console.log(this.reporter.report(report));
|
|
19216
|
+
if (apiKey) {
|
|
19217
|
+
console.log("");
|
|
19218
|
+
console.log(` Syncing ${displayName} to Verimu platform...`);
|
|
19219
|
+
try {
|
|
19220
|
+
const uploadConfig = {
|
|
19221
|
+
...config,
|
|
19222
|
+
projectPath: project.projectPath,
|
|
19223
|
+
groupName,
|
|
19224
|
+
// Use auto-derived or user-provided group name
|
|
19225
|
+
apiKey,
|
|
19226
|
+
apiBaseUrl
|
|
19227
|
+
};
|
|
19228
|
+
const uploadResult = await uploadToVerimu(report, uploadConfig);
|
|
19229
|
+
if (uploadResult.projectCreated) {
|
|
19230
|
+
console.log(` \u2713 Project created: ${displayName}`);
|
|
19231
|
+
}
|
|
19232
|
+
console.log(` \u2713 ${uploadResult.totalDependencies} dependencies tracked`);
|
|
19233
|
+
console.log(renderPlatformScan(displayName, uploadResult));
|
|
19234
|
+
console.log(` \u2713 Dashboard: ${uploadResult.dashboardUrl}`);
|
|
19235
|
+
} catch (err) {
|
|
19236
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19237
|
+
console.log(` \u26A0 Platform sync failed: ${msg}`);
|
|
19238
|
+
console.log(" Your SBOM was still generated locally. You can upload it manually.");
|
|
19239
|
+
}
|
|
19240
|
+
}
|
|
19241
|
+
console.log("");
|
|
19242
|
+
successful.push({ project, report });
|
|
19243
|
+
} catch (error) {
|
|
19244
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
19245
|
+
console.log(` \u2717 Failed: ${errorMsg}`);
|
|
19246
|
+
console.log("");
|
|
19247
|
+
failed.push({ project, error: errorMsg });
|
|
19248
|
+
throw error;
|
|
19249
|
+
}
|
|
19250
|
+
}
|
|
19251
|
+
return {
|
|
19252
|
+
totalDiscovered: projects.length,
|
|
19253
|
+
successful,
|
|
19254
|
+
failed,
|
|
19255
|
+
skipped: [],
|
|
19256
|
+
durationMs: Date.now() - startTime
|
|
19257
|
+
};
|
|
19258
|
+
}
|
|
19259
|
+
/**
|
|
19260
|
+
* Derives SBOM output path for a project.
|
|
19261
|
+
* Places SBOMs in project directories by default.
|
|
19262
|
+
*/
|
|
19263
|
+
deriveSbomPath(project, configOutput) {
|
|
19264
|
+
if (configOutput) {
|
|
19265
|
+
const base = configOutput.replace(/\.cdx\.json$/, "");
|
|
19266
|
+
const sanitized = project.relativePath.replace(/[/\\]/g, "-");
|
|
19267
|
+
return `${base}.${sanitized}.cdx.json`;
|
|
19268
|
+
}
|
|
19269
|
+
return `${project.projectPath}/sbom.cdx.json`;
|
|
19270
|
+
}
|
|
19271
|
+
};
|
|
19272
|
+
|
|
18948
19273
|
// src/cli.ts
|
|
18949
19274
|
var require2 = createRequire(import.meta.url);
|
|
18950
19275
|
var pkg = require2("../package.json");
|
|
@@ -18977,7 +19302,11 @@ function parseArgs(argv) {
|
|
|
18977
19302
|
skipCveCheck: false,
|
|
18978
19303
|
skipUpload: false,
|
|
18979
19304
|
cyclonedxVersion: "1.7",
|
|
18980
|
-
contextLines: void 0
|
|
19305
|
+
contextLines: void 0,
|
|
19306
|
+
groupName: void 0,
|
|
19307
|
+
recursive: true,
|
|
19308
|
+
// Recursive by default
|
|
19309
|
+
exclude: void 0
|
|
18981
19310
|
};
|
|
18982
19311
|
let i = 0;
|
|
18983
19312
|
while (i < args.length) {
|
|
@@ -19023,6 +19352,20 @@ function parseArgs(argv) {
|
|
|
19023
19352
|
throw new Error(`Invalid CycloneDX version: ${val}`);
|
|
19024
19353
|
}
|
|
19025
19354
|
result.cyclonedxVersion = val;
|
|
19355
|
+
} else if (arg === "--group-name" || arg.startsWith("--group-name=")) {
|
|
19356
|
+
const val = arg.startsWith("--group-name=") ? arg.split("=")[1] : args[++i];
|
|
19357
|
+
if (!val || val.startsWith("--")) {
|
|
19358
|
+
throw new Error("--group-name requires a value");
|
|
19359
|
+
}
|
|
19360
|
+
result.groupName = val;
|
|
19361
|
+
} else if (arg === "--no-recursive" || arg === "--not-recursive") {
|
|
19362
|
+
result.recursive = false;
|
|
19363
|
+
} else if (arg === "--exclude") {
|
|
19364
|
+
const val = args[++i];
|
|
19365
|
+
if (!val || val.startsWith("--")) {
|
|
19366
|
+
throw new Error("--exclude requires a comma-separated list of patterns");
|
|
19367
|
+
}
|
|
19368
|
+
result.exclude = val.split(",").map((p) => p.trim());
|
|
19026
19369
|
}
|
|
19027
19370
|
i++;
|
|
19028
19371
|
}
|
|
@@ -19065,8 +19408,32 @@ async function main() {
|
|
|
19065
19408
|
// Don't pass apiKey to scan() if --skip-upload — we'll handle upload separately for better logging
|
|
19066
19409
|
apiKey: apiKey && !args.skipUpload ? void 0 : void 0,
|
|
19067
19410
|
apiBaseUrl,
|
|
19068
|
-
numContextLines: args.contextLines
|
|
19411
|
+
numContextLines: args.contextLines,
|
|
19412
|
+
groupName: args.groupName
|
|
19069
19413
|
};
|
|
19414
|
+
if (args.recursive) {
|
|
19415
|
+
const orchestrator = new MultiProjectOrchestrator();
|
|
19416
|
+
let result;
|
|
19417
|
+
try {
|
|
19418
|
+
result = await orchestrator.scanAll({
|
|
19419
|
+
...config,
|
|
19420
|
+
recursive: true,
|
|
19421
|
+
exclude: args.exclude,
|
|
19422
|
+
// Pass API key for platform uploads
|
|
19423
|
+
apiKey: apiKey && !args.skipUpload ? apiKey : void 0,
|
|
19424
|
+
apiBaseUrl
|
|
19425
|
+
});
|
|
19426
|
+
} catch (err) {
|
|
19427
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19428
|
+
logError(msg);
|
|
19429
|
+
process.exit(2);
|
|
19430
|
+
}
|
|
19431
|
+
printMultiProjectSummary(result);
|
|
19432
|
+
if (result.failed.length > 0) {
|
|
19433
|
+
process.exit(1);
|
|
19434
|
+
}
|
|
19435
|
+
return;
|
|
19436
|
+
}
|
|
19070
19437
|
let report;
|
|
19071
19438
|
try {
|
|
19072
19439
|
report = await scan(config);
|
|
@@ -19107,12 +19474,50 @@ async function main() {
|
|
|
19107
19474
|
process.exit(1);
|
|
19108
19475
|
}
|
|
19109
19476
|
}
|
|
19477
|
+
function printMultiProjectSummary(result) {
|
|
19478
|
+
console.log("\n" + "\u2500".repeat(60));
|
|
19479
|
+
console.log("Multi-Project Scan Summary");
|
|
19480
|
+
console.log("\u2500".repeat(60));
|
|
19481
|
+
console.log(`
|
|
19482
|
+
Projects discovered: ${result.totalDiscovered}`);
|
|
19483
|
+
console.log(` \u2713 Successful: ${result.successful.length}`);
|
|
19484
|
+
console.log(` \u2717 Failed: ${result.failed.length}`);
|
|
19485
|
+
if (result.successful.length > 0) {
|
|
19486
|
+
const totalDeps = result.successful.reduce(
|
|
19487
|
+
(sum, r) => sum + r.report.summary.totalDependencies,
|
|
19488
|
+
0
|
|
19489
|
+
);
|
|
19490
|
+
const totalVulns = result.successful.reduce(
|
|
19491
|
+
(sum, r) => sum + r.report.summary.totalVulnerabilities,
|
|
19492
|
+
0
|
|
19493
|
+
);
|
|
19494
|
+
console.log(`
|
|
19495
|
+
Total dependencies: ${totalDeps}`);
|
|
19496
|
+
console.log(`Total vulnerabilities: ${totalVulns}`);
|
|
19497
|
+
const critical = result.successful.reduce((sum, r) => sum + r.report.summary.critical, 0);
|
|
19498
|
+
const high = result.successful.reduce((sum, r) => sum + r.report.summary.high, 0);
|
|
19499
|
+
const medium = result.successful.reduce((sum, r) => sum + r.report.summary.medium, 0);
|
|
19500
|
+
const low = result.successful.reduce((sum, r) => sum + r.report.summary.low, 0);
|
|
19501
|
+
if (totalVulns > 0) {
|
|
19502
|
+
console.log(` Critical: ${critical}, High: ${high}, Medium: ${medium}, Low: ${low}`);
|
|
19503
|
+
}
|
|
19504
|
+
}
|
|
19505
|
+
if (result.failed.length > 0) {
|
|
19506
|
+
console.log("\nFailed projects:");
|
|
19507
|
+
for (const f of result.failed) {
|
|
19508
|
+
console.log(` \u2022 ${f.project.relativePath}: ${f.error}`);
|
|
19509
|
+
}
|
|
19510
|
+
}
|
|
19511
|
+
console.log(`
|
|
19512
|
+
Completed in ${(result.durationMs / 1e3).toFixed(2)}s`);
|
|
19513
|
+
console.log("");
|
|
19514
|
+
}
|
|
19110
19515
|
function printHelp() {
|
|
19111
19516
|
console.log(`
|
|
19112
19517
|
Verimu \u2014 CRA Compliance Scanner
|
|
19113
19518
|
|
|
19114
19519
|
Usage:
|
|
19115
|
-
verimu Scan current directory
|
|
19520
|
+
verimu Scan current directory (recursively)
|
|
19116
19521
|
verimu scan [options] Full scan (SBOM + CVE check)
|
|
19117
19522
|
verimu generate-sbom [options] Generate SBOM only (no CVE check)
|
|
19118
19523
|
verimu help Show this help
|
|
@@ -19121,23 +19526,35 @@ function printHelp() {
|
|
|
19121
19526
|
Options:
|
|
19122
19527
|
--path, -p <dir> Project directory to scan (default: .)
|
|
19123
19528
|
--output, -o <file> CycloneDX output path (SPDX/SWID are written alongside it)
|
|
19529
|
+
--group-name <name> Group name for organizing related projects in dashboard
|
|
19124
19530
|
--fail-on <severity> Exit 1 if vulns at or above: CRITICAL, HIGH, MEDIUM, LOW
|
|
19125
19531
|
--skip-cve Skip CVE vulnerability checking
|
|
19126
19532
|
--skip-upload Don't sync to Verimu platform (even if API key is set)
|
|
19127
19533
|
--context-lines <n> Snippet context lines around matches (default: 4, clamped to 0..20)
|
|
19128
19534
|
--cdx-version <ver> CycloneDX spec: 1.4, 1.5, 1.6, 1.7 (default: 1.7)
|
|
19129
19535
|
|
|
19536
|
+
Project Discovery:
|
|
19537
|
+
--no-recursive Disable recursive discovery (scan only root directory)
|
|
19538
|
+
--exclude <patterns> Exclude paths matching patterns (comma-separated globs)
|
|
19539
|
+
|
|
19540
|
+
Note: Verimu automatically discovers all projects recursively by default.
|
|
19541
|
+
For monorepos with multiple lockfiles, projects are auto-grouped by directory name.
|
|
19542
|
+
Single lockfile projects are treated normally without grouping.
|
|
19543
|
+
|
|
19130
19544
|
Environment:
|
|
19131
19545
|
VERIMU_API_KEY API key for Verimu platform (from app.verimu.com)
|
|
19132
19546
|
VERIMU_API_URL Custom API URL (default: https://api.verimu.com)
|
|
19133
19547
|
|
|
19134
19548
|
Examples:
|
|
19135
|
-
npx verimu #
|
|
19549
|
+
npx verimu # Scan all projects recursively
|
|
19136
19550
|
VERIMU_API_KEY=vmu_xxx npx verimu # Scan + sync to platform
|
|
19137
19551
|
npx verimu scan --fail-on HIGH # Fail CI on HIGH+ vulns
|
|
19552
|
+
npx verimu scan --group-name my-app # Group projects with custom name
|
|
19138
19553
|
npx verimu scan --context-lines 8 # Wider context around usage snippets
|
|
19139
19554
|
npx verimu scan --cdx-version 1.5 # Specify CycloneDX version
|
|
19140
19555
|
npx verimu scan --path ./backend --output ./reports/sbom.json
|
|
19556
|
+
npx verimu scan --no-recursive # Scan only root directory
|
|
19557
|
+
npx verimu scan --exclude "legacy/*" # Exclude legacy projects
|
|
19141
19558
|
|
|
19142
19559
|
Supported ecosystems:
|
|
19143
19560
|
npm (package-lock.json) pip (requirements.txt)
|