verimu 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -13
- package/dist/cli.mjs +1570 -127
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1087 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.mjs +1081 -80
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -13052,9 +13052,9 @@ var require_traverse = __commonJS({
|
|
|
13052
13052
|
Object.defineProperty(exports, "__esModule", {
|
|
13053
13053
|
value: true
|
|
13054
13054
|
});
|
|
13055
|
-
exports.default =
|
|
13055
|
+
exports.default = traverse;
|
|
13056
13056
|
var _index = require_definitions();
|
|
13057
|
-
function
|
|
13057
|
+
function traverse(node, handlers, state) {
|
|
13058
13058
|
if (typeof handlers === "function") {
|
|
13059
13059
|
handlers = {
|
|
13060
13060
|
enter: handlers
|
|
@@ -14096,7 +14096,7 @@ import { createRequire } from "module";
|
|
|
14096
14096
|
|
|
14097
14097
|
// src/scan.ts
|
|
14098
14098
|
import { writeFile } from "fs/promises";
|
|
14099
|
-
import { basename, join as
|
|
14099
|
+
import { basename, join as join3, parse as parse2 } from "path";
|
|
14100
14100
|
|
|
14101
14101
|
// src/scanners/npm/npm-scanner.ts
|
|
14102
14102
|
import { readFile } from "fs/promises";
|
|
@@ -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
|
/**
|
|
@@ -16750,6 +16751,15 @@ var ConsoleReporter = class {
|
|
|
16750
16751
|
lines.push(
|
|
16751
16752
|
` Findings: direct_evidence=${directEvidence}, indirect_no_evidence=${indirectNoEvidence}, unsupported=${unsupported}, analysis_error=${analysisErrors}`
|
|
16752
16753
|
);
|
|
16754
|
+
if (result.usageContext.ecosystemStatus.length > 0) {
|
|
16755
|
+
lines.push(" Analyzer status:");
|
|
16756
|
+
for (const status of result.usageContext.ecosystemStatus) {
|
|
16757
|
+
const note = status.note ? ` (${status.note})` : "";
|
|
16758
|
+
lines.push(
|
|
16759
|
+
` ${status.ecosystem}: ${status.status} via ${status.analyzer}${note}`
|
|
16760
|
+
);
|
|
16761
|
+
}
|
|
16762
|
+
}
|
|
16753
16763
|
if (result.usageContext.artifactPath) {
|
|
16754
16764
|
lines.push(` Artifact: ${result.usageContext.artifactPath}`);
|
|
16755
16765
|
}
|
|
@@ -16816,7 +16826,8 @@ var VerimuApiClient = class {
|
|
|
16816
16826
|
name: opts.name,
|
|
16817
16827
|
ecosystem: this.mapEcosystem(opts.ecosystem),
|
|
16818
16828
|
repository_url: opts.repositoryUrl ?? null,
|
|
16819
|
-
platform: opts.platform ?? null
|
|
16829
|
+
platform: opts.platform ?? null,
|
|
16830
|
+
group_name: opts.groupName ?? null
|
|
16820
16831
|
})
|
|
16821
16832
|
});
|
|
16822
16833
|
if (!res.ok) {
|
|
@@ -17001,7 +17012,7 @@ var import_types = __toESM(require_lib3(), 1);
|
|
|
17001
17012
|
import { readdir, readFile as readFile14 } from "fs/promises";
|
|
17002
17013
|
import { join } from "path";
|
|
17003
17014
|
import { parse } from "@babel/parser";
|
|
17004
|
-
import
|
|
17015
|
+
import traverseModule from "@babel/traverse";
|
|
17005
17016
|
var JS_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
17006
17017
|
".js",
|
|
17007
17018
|
".jsx",
|
|
@@ -17031,6 +17042,24 @@ var JsAstAnalyzer = class {
|
|
|
17031
17042
|
return this.ecosystems.has(ecosystem);
|
|
17032
17043
|
}
|
|
17033
17044
|
async analyze(context) {
|
|
17045
|
+
const traverseFn = resolveTraverseFunction(traverseModule);
|
|
17046
|
+
if (!traverseFn) {
|
|
17047
|
+
return {
|
|
17048
|
+
packages: context.packages.map((pkg2) => ({
|
|
17049
|
+
packageName: pkg2.packageName,
|
|
17050
|
+
ecosystem: pkg2.ecosystem,
|
|
17051
|
+
status: "analysis_error",
|
|
17052
|
+
snippets: [],
|
|
17053
|
+
notes: "Failed to resolve @babel/traverse runtime export"
|
|
17054
|
+
})),
|
|
17055
|
+
errors: [{
|
|
17056
|
+
analyzer: this.name,
|
|
17057
|
+
ecosystem: context.ecosystem,
|
|
17058
|
+
error: "Failed to resolve @babel/traverse runtime export"
|
|
17059
|
+
}],
|
|
17060
|
+
snippetsProduced: 0
|
|
17061
|
+
};
|
|
17062
|
+
}
|
|
17034
17063
|
const packageMap = this.buildPackageMaps(context.packages);
|
|
17035
17064
|
const resultMap = /* @__PURE__ */ new Map();
|
|
17036
17065
|
const snippetKeyMap = /* @__PURE__ */ new Map();
|
|
@@ -17094,41 +17123,41 @@ var JsAstAnalyzer = class {
|
|
|
17094
17123
|
const matchCandidates = [];
|
|
17095
17124
|
const matchSeen = /* @__PURE__ */ new Set();
|
|
17096
17125
|
const symbolToPackage = /* @__PURE__ */ new Map();
|
|
17097
|
-
const addMatch = (
|
|
17098
|
-
const candidateKey = `${
|
|
17126
|
+
const addMatch = (packageKey3, line, matchKind, calledSymbol, confidence = 0.8) => {
|
|
17127
|
+
const candidateKey = `${packageKey3}:${line}:${matchKind}:${calledSymbol ?? ""}`;
|
|
17099
17128
|
if (matchSeen.has(candidateKey)) return;
|
|
17100
17129
|
matchSeen.add(candidateKey);
|
|
17101
|
-
matchCandidates.push({ packageKey:
|
|
17130
|
+
matchCandidates.push({ packageKey: packageKey3, line, matchKind, calledSymbol, confidence });
|
|
17102
17131
|
};
|
|
17103
|
-
|
|
17104
|
-
ImportDeclaration: (
|
|
17105
|
-
const source =
|
|
17132
|
+
traverseFn(ast, {
|
|
17133
|
+
ImportDeclaration: (path15) => {
|
|
17134
|
+
const source = path15.node.source;
|
|
17106
17135
|
if (!(0, import_types.isStringLiteral)(source)) return;
|
|
17107
17136
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17108
17137
|
if (!pkgKey) return;
|
|
17109
|
-
addMatch(pkgKey,
|
|
17110
|
-
for (const specifier of
|
|
17138
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "import", void 0, 0.95);
|
|
17139
|
+
for (const specifier of path15.node.specifiers) {
|
|
17111
17140
|
if ((0, import_types.isImportDefaultSpecifier)(specifier) || (0, import_types.isImportNamespaceSpecifier)(specifier) || (0, import_types.isImportSpecifier)(specifier)) {
|
|
17112
17141
|
symbolToPackage.set(specifier.local.name, pkgKey);
|
|
17113
17142
|
}
|
|
17114
17143
|
}
|
|
17115
17144
|
},
|
|
17116
|
-
ExportNamedDeclaration: (
|
|
17117
|
-
const source =
|
|
17145
|
+
ExportNamedDeclaration: (path15) => {
|
|
17146
|
+
const source = path15.node.source;
|
|
17118
17147
|
if (!source || !(0, import_types.isStringLiteral)(source)) return;
|
|
17119
17148
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17120
17149
|
if (!pkgKey) return;
|
|
17121
|
-
addMatch(pkgKey,
|
|
17150
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "export_from", void 0, 0.85);
|
|
17122
17151
|
},
|
|
17123
|
-
ExportAllDeclaration: (
|
|
17124
|
-
const source =
|
|
17152
|
+
ExportAllDeclaration: (path15) => {
|
|
17153
|
+
const source = path15.node.source;
|
|
17125
17154
|
if (!(0, import_types.isStringLiteral)(source)) return;
|
|
17126
17155
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17127
17156
|
if (!pkgKey) return;
|
|
17128
|
-
addMatch(pkgKey,
|
|
17157
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "export_from", void 0, 0.85);
|
|
17129
17158
|
},
|
|
17130
|
-
VariableDeclarator: (
|
|
17131
|
-
const node =
|
|
17159
|
+
VariableDeclarator: (path15) => {
|
|
17160
|
+
const node = path15.node;
|
|
17132
17161
|
if (!(0, import_types.isVariableDeclarator)(node)) return;
|
|
17133
17162
|
if (!node.init || !(0, import_types.isCallExpression)(node.init)) return;
|
|
17134
17163
|
if (!(0, import_types.isIdentifier)(node.init.callee, { name: "require" })) return;
|
|
@@ -17141,8 +17170,8 @@ var JsAstAnalyzer = class {
|
|
|
17141
17170
|
symbolToPackage.set(identifier, pkgKey);
|
|
17142
17171
|
}
|
|
17143
17172
|
},
|
|
17144
|
-
CallExpression: (
|
|
17145
|
-
const node =
|
|
17173
|
+
CallExpression: (path15) => {
|
|
17174
|
+
const node = path15.node;
|
|
17146
17175
|
if ((0, import_types.isIdentifier)(node.callee, { name: "require" })) {
|
|
17147
17176
|
const firstArg = node.arguments[0];
|
|
17148
17177
|
if (firstArg && (0, import_types.isStringLiteral)(firstArg)) {
|
|
@@ -17163,12 +17192,12 @@ var JsAstAnalyzer = class {
|
|
|
17163
17192
|
0.75
|
|
17164
17193
|
);
|
|
17165
17194
|
},
|
|
17166
|
-
ImportExpression: (
|
|
17167
|
-
const source =
|
|
17195
|
+
ImportExpression: (path15) => {
|
|
17196
|
+
const source = path15.node.source;
|
|
17168
17197
|
if (!(0, import_types.isStringLiteral)(source)) return;
|
|
17169
17198
|
const pkgKey = findPackageKey(resolveImportTarget(source.value), packageMap.byName);
|
|
17170
17199
|
if (!pkgKey) return;
|
|
17171
|
-
addMatch(pkgKey,
|
|
17200
|
+
addMatch(pkgKey, path15.node.loc?.start.line ?? 1, "dynamic_import", void 0, 0.9);
|
|
17172
17201
|
}
|
|
17173
17202
|
});
|
|
17174
17203
|
for (const candidate of matchCandidates) {
|
|
@@ -17222,6 +17251,18 @@ var JsAstAnalyzer = class {
|
|
|
17222
17251
|
return `${ecosystem}::${packageName}`;
|
|
17223
17252
|
}
|
|
17224
17253
|
};
|
|
17254
|
+
function resolveTraverseFunction(moduleValue) {
|
|
17255
|
+
if (typeof moduleValue === "function") {
|
|
17256
|
+
return moduleValue;
|
|
17257
|
+
}
|
|
17258
|
+
if (typeof moduleValue === "object" && moduleValue !== null && "default" in moduleValue) {
|
|
17259
|
+
const candidate = moduleValue.default;
|
|
17260
|
+
if (typeof candidate === "function") {
|
|
17261
|
+
return candidate;
|
|
17262
|
+
}
|
|
17263
|
+
}
|
|
17264
|
+
return null;
|
|
17265
|
+
}
|
|
17225
17266
|
async function collectSourceFiles(rootPath) {
|
|
17226
17267
|
const files = [];
|
|
17227
17268
|
async function walk(dirPath) {
|
|
@@ -17317,9 +17358,9 @@ function collectIdentifiers(pattern) {
|
|
|
17317
17358
|
function resolveCallMatch(callee, symbolToPackage) {
|
|
17318
17359
|
const normalized = unwrapExpression(callee);
|
|
17319
17360
|
if ((0, import_types.isIdentifier)(normalized)) {
|
|
17320
|
-
const
|
|
17321
|
-
if (!
|
|
17322
|
-
return { packageKey:
|
|
17361
|
+
const packageKey3 = symbolToPackage.get(normalized.name);
|
|
17362
|
+
if (!packageKey3) return null;
|
|
17363
|
+
return { packageKey: packageKey3, calledSymbol: normalized.name };
|
|
17323
17364
|
}
|
|
17324
17365
|
if ((0, import_types.isMemberExpression)(normalized) || (0, import_types.isOptionalMemberExpression)(normalized)) {
|
|
17325
17366
|
return resolveMemberCallMatch(normalized, symbolToPackage);
|
|
@@ -17336,13 +17377,13 @@ function unwrapExpression(expression) {
|
|
|
17336
17377
|
function resolveMemberCallMatch(memberExpression, symbolToPackage) {
|
|
17337
17378
|
const objectExpr = unwrapExpression(memberExpression.object);
|
|
17338
17379
|
if (!(0, import_types.isIdentifier)(objectExpr)) return null;
|
|
17339
|
-
const
|
|
17340
|
-
if (!
|
|
17380
|
+
const packageKey3 = symbolToPackage.get(objectExpr.name);
|
|
17381
|
+
if (!packageKey3) return null;
|
|
17341
17382
|
const propertyName = propertyNameOf(memberExpression);
|
|
17342
17383
|
if (!propertyName) {
|
|
17343
|
-
return { packageKey:
|
|
17384
|
+
return { packageKey: packageKey3, calledSymbol: objectExpr.name };
|
|
17344
17385
|
}
|
|
17345
|
-
return { packageKey:
|
|
17386
|
+
return { packageKey: packageKey3, calledSymbol: `${objectExpr.name}.${propertyName}` };
|
|
17346
17387
|
}
|
|
17347
17388
|
function propertyNameOf(memberExpression) {
|
|
17348
17389
|
if (memberExpression.computed) {
|
|
@@ -17359,34 +17400,1021 @@ function propertyNameOf(memberExpression) {
|
|
|
17359
17400
|
return null;
|
|
17360
17401
|
}
|
|
17361
17402
|
|
|
17362
|
-
// src/context/analyzers/
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17403
|
+
// src/context/analyzers/shared.ts
|
|
17404
|
+
import { readdir as readdir2, readFile as readFile15 } from "fs/promises";
|
|
17405
|
+
import { join as join2 } from "path";
|
|
17406
|
+
var DEFAULT_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
17407
|
+
".git",
|
|
17408
|
+
".hg",
|
|
17409
|
+
".svn",
|
|
17410
|
+
"node_modules",
|
|
17411
|
+
"dist",
|
|
17412
|
+
"build",
|
|
17413
|
+
"coverage",
|
|
17414
|
+
".next",
|
|
17415
|
+
".nuxt",
|
|
17416
|
+
".turbo",
|
|
17417
|
+
"vendor",
|
|
17418
|
+
".venv",
|
|
17419
|
+
"venv",
|
|
17420
|
+
"target",
|
|
17421
|
+
"bin",
|
|
17422
|
+
"obj"
|
|
17423
|
+
]);
|
|
17424
|
+
function packageKey(ecosystem, packageName) {
|
|
17425
|
+
return `${ecosystem}::${packageName}`;
|
|
17426
|
+
}
|
|
17427
|
+
function initState(packages) {
|
|
17428
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
17429
|
+
const snippetKeyMap = /* @__PURE__ */ new Map();
|
|
17430
|
+
for (const pkg2 of packages) {
|
|
17431
|
+
const key = packageKey(pkg2.ecosystem, pkg2.packageName);
|
|
17432
|
+
resultMap.set(key, {
|
|
17433
|
+
packageName: pkg2.packageName,
|
|
17434
|
+
ecosystem: pkg2.ecosystem,
|
|
17435
|
+
status: "indirect_no_evidence",
|
|
17436
|
+
snippets: []
|
|
17437
|
+
});
|
|
17438
|
+
snippetKeyMap.set(key, /* @__PURE__ */ new Set());
|
|
17439
|
+
}
|
|
17440
|
+
return {
|
|
17441
|
+
resultMap,
|
|
17442
|
+
snippetKeyMap,
|
|
17443
|
+
errors: [],
|
|
17444
|
+
snippetsProduced: 0
|
|
17445
|
+
};
|
|
17446
|
+
}
|
|
17447
|
+
function errorResultFromMessage(context, analyzerName, message, notes) {
|
|
17448
|
+
return {
|
|
17449
|
+
packages: context.packages.map((pkg2) => ({
|
|
17450
|
+
packageName: pkg2.packageName,
|
|
17451
|
+
ecosystem: pkg2.ecosystem,
|
|
17452
|
+
status: "analysis_error",
|
|
17453
|
+
snippets: [],
|
|
17454
|
+
notes
|
|
17455
|
+
})),
|
|
17456
|
+
errors: [{ analyzer: analyzerName, ecosystem: context.ecosystem, error: message }],
|
|
17457
|
+
snippetsProduced: 0
|
|
17458
|
+
};
|
|
17459
|
+
}
|
|
17460
|
+
function toAnalyzerResult(state) {
|
|
17461
|
+
for (const result of state.resultMap.values()) {
|
|
17462
|
+
result.snippets = dedupeSnippets(result.snippets);
|
|
17463
|
+
if (result.snippets.length > 0) {
|
|
17464
|
+
result.status = "direct_evidence";
|
|
17465
|
+
}
|
|
17371
17466
|
}
|
|
17467
|
+
return {
|
|
17468
|
+
packages: Array.from(state.resultMap.values()),
|
|
17469
|
+
errors: state.errors,
|
|
17470
|
+
snippetsProduced: state.snippetsProduced
|
|
17471
|
+
};
|
|
17472
|
+
}
|
|
17473
|
+
async function collectSourceFiles2(rootPath, extensions, ignoredDirs = DEFAULT_IGNORED_DIRS) {
|
|
17474
|
+
const files = [];
|
|
17475
|
+
async function walk(dirPath) {
|
|
17476
|
+
const entries = await readdir2(dirPath, { withFileTypes: true });
|
|
17477
|
+
for (const entry of entries) {
|
|
17478
|
+
const fullPath = join2(dirPath, entry.name);
|
|
17479
|
+
if (entry.isDirectory()) {
|
|
17480
|
+
if (ignoredDirs.has(entry.name)) continue;
|
|
17481
|
+
await walk(fullPath);
|
|
17482
|
+
continue;
|
|
17483
|
+
}
|
|
17484
|
+
if (!entry.isFile()) continue;
|
|
17485
|
+
if (!extensions.has(extensionOf2(entry.name))) continue;
|
|
17486
|
+
files.push(fullPath);
|
|
17487
|
+
}
|
|
17488
|
+
}
|
|
17489
|
+
await walk(rootPath);
|
|
17490
|
+
return files;
|
|
17491
|
+
}
|
|
17492
|
+
async function readSourceFile(analyzerName, ecosystem, filePath, errors) {
|
|
17493
|
+
try {
|
|
17494
|
+
return await readFile15(filePath, "utf-8");
|
|
17495
|
+
} catch (err) {
|
|
17496
|
+
errors.push({
|
|
17497
|
+
analyzer: analyzerName,
|
|
17498
|
+
ecosystem,
|
|
17499
|
+
error: `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
17500
|
+
});
|
|
17501
|
+
return null;
|
|
17502
|
+
}
|
|
17503
|
+
}
|
|
17504
|
+
function addCandidate(context, state, filePath, sourceText, candidate) {
|
|
17505
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) return;
|
|
17506
|
+
const packageResult = state.resultMap.get(candidate.packageKey);
|
|
17507
|
+
if (!packageResult) return;
|
|
17508
|
+
if (packageResult.snippets.length >= context.maxSnippetsPerPackage) return;
|
|
17509
|
+
const snippet = buildSnippet({
|
|
17510
|
+
projectPath: context.projectPath,
|
|
17511
|
+
filePath,
|
|
17512
|
+
sourceText,
|
|
17513
|
+
line: candidate.line,
|
|
17514
|
+
numContextLines: context.numContextLines,
|
|
17515
|
+
matchKind: candidate.matchKind,
|
|
17516
|
+
calledSymbol: candidate.calledSymbol,
|
|
17517
|
+
confidence: candidate.confidence ?? 0.8
|
|
17518
|
+
});
|
|
17519
|
+
const dedupeKey = `${snippet.filePath}:${snippet.startLine}:${snippet.endLine}:${snippet.matchKind}:${snippet.calledSymbol ?? ""}`;
|
|
17520
|
+
const packageSnippetKeys = state.snippetKeyMap.get(candidate.packageKey);
|
|
17521
|
+
if (!packageSnippetKeys || packageSnippetKeys.has(dedupeKey)) return;
|
|
17522
|
+
packageSnippetKeys.add(dedupeKey);
|
|
17523
|
+
packageResult.snippets.push(snippet);
|
|
17524
|
+
state.snippetsProduced += 1;
|
|
17525
|
+
}
|
|
17526
|
+
function extensionOf2(fileName) {
|
|
17527
|
+
const index = fileName.lastIndexOf(".");
|
|
17528
|
+
return index === -1 ? "" : fileName.slice(index).toLowerCase();
|
|
17529
|
+
}
|
|
17530
|
+
function basePackageName(name) {
|
|
17531
|
+
const slash = name.includes("/") ? name.split("/").at(-1) ?? name : name;
|
|
17532
|
+
const colon = slash.includes(":") ? slash.split(":").at(-1) ?? slash : slash;
|
|
17533
|
+
return colon;
|
|
17534
|
+
}
|
|
17535
|
+
function toIdentifierToken(value) {
|
|
17536
|
+
return value.replace(/[^A-Za-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
17537
|
+
}
|
|
17538
|
+
function uniqueTokens(values) {
|
|
17539
|
+
const result = [];
|
|
17540
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17541
|
+
for (const value of values) {
|
|
17542
|
+
const trimmed = value.trim();
|
|
17543
|
+
if (!trimmed) continue;
|
|
17544
|
+
const key = trimmed.toLowerCase();
|
|
17545
|
+
if (seen.has(key)) continue;
|
|
17546
|
+
seen.add(key);
|
|
17547
|
+
result.push(trimmed);
|
|
17548
|
+
}
|
|
17549
|
+
return result;
|
|
17550
|
+
}
|
|
17551
|
+
|
|
17552
|
+
// src/context/analyzers/python-ast-analyzer.ts
|
|
17553
|
+
var PYTHON_EXTENSIONS = /* @__PURE__ */ new Set([".py"]);
|
|
17554
|
+
var PythonAstAnalyzer = class {
|
|
17555
|
+
name = "python-ast-analyzer";
|
|
17556
|
+
ecosystems = /* @__PURE__ */ new Set(["pip", "poetry", "uv"]);
|
|
17372
17557
|
supports(ecosystem) {
|
|
17373
17558
|
return this.ecosystems.has(ecosystem);
|
|
17374
17559
|
}
|
|
17375
17560
|
async analyze(context) {
|
|
17376
|
-
const
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17561
|
+
const state = initState(context.packages);
|
|
17562
|
+
const packagePatterns = context.packages.map(
|
|
17563
|
+
(pkg2) => this.patternForPackage(pkg2.packageName, pkg2.ecosystem)
|
|
17564
|
+
);
|
|
17565
|
+
let files;
|
|
17566
|
+
try {
|
|
17567
|
+
files = await collectSourceFiles2(context.projectPath, PYTHON_EXTENSIONS);
|
|
17568
|
+
} catch (err) {
|
|
17569
|
+
return errorResultFromMessage(
|
|
17570
|
+
context,
|
|
17571
|
+
this.name,
|
|
17572
|
+
err instanceof Error ? err.message : String(err),
|
|
17573
|
+
"Failed to enumerate Python source files"
|
|
17574
|
+
);
|
|
17575
|
+
}
|
|
17576
|
+
for (const filePath of files) {
|
|
17577
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) break;
|
|
17578
|
+
const sourceText = await readSourceFile(this.name, context.ecosystem, filePath, state.errors);
|
|
17579
|
+
if (sourceText === null) continue;
|
|
17580
|
+
const aliasToPackage = /* @__PURE__ */ new Map();
|
|
17581
|
+
const lines = sourceText.split(/\r?\n/);
|
|
17582
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
17583
|
+
const line = lines[idx];
|
|
17584
|
+
const trimmed = stripInlineComment(line).trim();
|
|
17585
|
+
const lineNumber = idx + 1;
|
|
17586
|
+
if (!trimmed) continue;
|
|
17587
|
+
const importMatch = trimmed.match(/^import\s+(.+)$/);
|
|
17588
|
+
if (importMatch) {
|
|
17589
|
+
const segments = importMatch[1].split(",").map((part) => part.trim()).filter(Boolean);
|
|
17590
|
+
for (const segment of segments) {
|
|
17591
|
+
const parsed = segment.match(/^([A-Za-z_][A-Za-z0-9_\.]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?$/);
|
|
17592
|
+
if (!parsed) continue;
|
|
17593
|
+
const moduleName = parsed[1];
|
|
17594
|
+
const alias = parsed[2] ?? moduleName.split(".").at(0) ?? moduleName;
|
|
17595
|
+
this.addImportForModule(
|
|
17596
|
+
context,
|
|
17597
|
+
state,
|
|
17598
|
+
filePath,
|
|
17599
|
+
sourceText,
|
|
17600
|
+
packagePatterns,
|
|
17601
|
+
aliasToPackage,
|
|
17602
|
+
moduleName,
|
|
17603
|
+
alias,
|
|
17604
|
+
lineNumber
|
|
17605
|
+
);
|
|
17606
|
+
}
|
|
17607
|
+
continue;
|
|
17608
|
+
}
|
|
17609
|
+
const fromMatch = trimmed.match(
|
|
17610
|
+
/^from\s+([A-Za-z_][A-Za-z0-9_\.]*)\s+import\s+(.+)$/
|
|
17611
|
+
);
|
|
17612
|
+
if (fromMatch) {
|
|
17613
|
+
const moduleName = fromMatch[1];
|
|
17614
|
+
const imported = fromMatch[2].split(",").map((part) => part.trim()).filter(Boolean);
|
|
17615
|
+
for (const part of imported) {
|
|
17616
|
+
const parsed = part.match(/^([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?$/);
|
|
17617
|
+
if (!parsed) continue;
|
|
17618
|
+
const name = parsed[1];
|
|
17619
|
+
const alias = parsed[2] ?? name;
|
|
17620
|
+
this.addImportForModule(
|
|
17621
|
+
context,
|
|
17622
|
+
state,
|
|
17623
|
+
filePath,
|
|
17624
|
+
sourceText,
|
|
17625
|
+
packagePatterns,
|
|
17626
|
+
aliasToPackage,
|
|
17627
|
+
moduleName,
|
|
17628
|
+
alias,
|
|
17629
|
+
lineNumber
|
|
17630
|
+
);
|
|
17631
|
+
}
|
|
17632
|
+
continue;
|
|
17633
|
+
}
|
|
17634
|
+
for (const [alias, pkgKey] of aliasToPackage.entries()) {
|
|
17635
|
+
const escapedAlias = escapeRegex(alias);
|
|
17636
|
+
const directCall = new RegExp(`\\b${escapedAlias}\\s*\\(`);
|
|
17637
|
+
if (directCall.test(trimmed)) {
|
|
17638
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17639
|
+
packageKey: pkgKey,
|
|
17640
|
+
line: lineNumber,
|
|
17641
|
+
matchKind: "call",
|
|
17642
|
+
calledSymbol: alias,
|
|
17643
|
+
confidence: 0.78
|
|
17644
|
+
});
|
|
17645
|
+
}
|
|
17646
|
+
const memberCall = new RegExp(`\\b${escapedAlias}\\s*\\.\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`);
|
|
17647
|
+
const match = trimmed.match(memberCall);
|
|
17648
|
+
if (match) {
|
|
17649
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17650
|
+
packageKey: pkgKey,
|
|
17651
|
+
line: lineNumber,
|
|
17652
|
+
matchKind: "call",
|
|
17653
|
+
calledSymbol: `${alias}.${match[1]}`,
|
|
17654
|
+
confidence: 0.8
|
|
17655
|
+
});
|
|
17656
|
+
}
|
|
17657
|
+
}
|
|
17658
|
+
}
|
|
17659
|
+
}
|
|
17660
|
+
return toAnalyzerResult(state);
|
|
17661
|
+
}
|
|
17662
|
+
addImportForModule(context, state, filePath, sourceText, packagePatterns, aliasToPackage, moduleName, alias, lineNumber) {
|
|
17663
|
+
const normalizedModule = moduleName.toLowerCase();
|
|
17664
|
+
for (const pkg2 of packagePatterns) {
|
|
17665
|
+
const matched = pkg2.modules.some(
|
|
17666
|
+
(candidate) => normalizedModule === candidate || normalizedModule.startsWith(`${candidate}.`)
|
|
17667
|
+
);
|
|
17668
|
+
if (!matched) continue;
|
|
17669
|
+
aliasToPackage.set(alias, pkg2.key);
|
|
17670
|
+
aliasToPackage.set(moduleName.split(".").at(0) ?? moduleName, pkg2.key);
|
|
17671
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17672
|
+
packageKey: pkg2.key,
|
|
17673
|
+
line: lineNumber,
|
|
17674
|
+
matchKind: "import",
|
|
17675
|
+
confidence: 0.95
|
|
17676
|
+
});
|
|
17677
|
+
}
|
|
17678
|
+
}
|
|
17679
|
+
patternForPackage(packageName, ecosystem) {
|
|
17680
|
+
const normalized = toIdentifierToken(packageName).replace(/_/g, "-");
|
|
17681
|
+
const base = toIdentifierToken(basePackageName(packageName));
|
|
17682
|
+
const packageModules = [
|
|
17683
|
+
normalized.replace(/-/g, "_"),
|
|
17684
|
+
base.replace(/-/g, "_"),
|
|
17685
|
+
base.replace(/_/g, "")
|
|
17686
|
+
];
|
|
17687
|
+
if (normalized === "pyyaml" || base === "pyyaml") {
|
|
17688
|
+
packageModules.push("yaml");
|
|
17689
|
+
}
|
|
17690
|
+
return {
|
|
17691
|
+
key: packageKey(ecosystem, packageName),
|
|
17692
|
+
modules: uniqueTokens(packageModules.map((v) => v.toLowerCase()))
|
|
17693
|
+
};
|
|
17694
|
+
}
|
|
17695
|
+
};
|
|
17696
|
+
function stripInlineComment(line) {
|
|
17697
|
+
const hashIndex = line.indexOf("#");
|
|
17698
|
+
return hashIndex === -1 ? line : line.slice(0, hashIndex);
|
|
17699
|
+
}
|
|
17700
|
+
function escapeRegex(value) {
|
|
17701
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17702
|
+
}
|
|
17703
|
+
|
|
17704
|
+
// src/context/analyzers/java-ast-analyzer.ts
|
|
17705
|
+
var JAVA_EXTENSIONS = /* @__PURE__ */ new Set([".java"]);
|
|
17706
|
+
var JavaAstAnalyzer = class {
|
|
17707
|
+
name = "java-ast-analyzer";
|
|
17708
|
+
ecosystems = /* @__PURE__ */ new Set(["maven"]);
|
|
17709
|
+
supports(ecosystem) {
|
|
17710
|
+
return this.ecosystems.has(ecosystem);
|
|
17711
|
+
}
|
|
17712
|
+
async analyze(context) {
|
|
17713
|
+
const state = initState(context.packages);
|
|
17714
|
+
const packagePatterns = context.packages.map(
|
|
17715
|
+
(pkg2) => this.patternForPackage(pkg2.packageName, pkg2.ecosystem)
|
|
17716
|
+
);
|
|
17717
|
+
let files;
|
|
17718
|
+
try {
|
|
17719
|
+
files = await collectSourceFiles2(context.projectPath, JAVA_EXTENSIONS);
|
|
17720
|
+
} catch (err) {
|
|
17721
|
+
return errorResultFromMessage(
|
|
17722
|
+
context,
|
|
17723
|
+
this.name,
|
|
17724
|
+
err instanceof Error ? err.message : String(err),
|
|
17725
|
+
"Failed to enumerate Java source files"
|
|
17726
|
+
);
|
|
17727
|
+
}
|
|
17728
|
+
for (const filePath of files) {
|
|
17729
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) break;
|
|
17730
|
+
const sourceText = await readSourceFile(this.name, context.ecosystem, filePath, state.errors);
|
|
17731
|
+
if (sourceText === null) continue;
|
|
17732
|
+
const lines = sourceText.split(/\r?\n/);
|
|
17733
|
+
const symbolToPackage = /* @__PURE__ */ new Map();
|
|
17734
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
17735
|
+
const line = stripLineComment(lines[idx]).trim();
|
|
17736
|
+
const lineNumber = idx + 1;
|
|
17737
|
+
if (!line) continue;
|
|
17738
|
+
const importMatch = line.match(/^import\s+(?:static\s+)?([A-Za-z0-9_.*]+)\s*;/);
|
|
17739
|
+
if (importMatch) {
|
|
17740
|
+
const importPath = importMatch[1].replace(/\.\*$/, "");
|
|
17741
|
+
const simpleName = importPath.split(".").at(-1) ?? importPath;
|
|
17742
|
+
for (const pkg2 of packagePatterns) {
|
|
17743
|
+
if (!pkg2.candidates.some((candidate) => importPath.startsWith(candidate))) continue;
|
|
17744
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17745
|
+
packageKey: pkg2.key,
|
|
17746
|
+
line: lineNumber,
|
|
17747
|
+
matchKind: "import",
|
|
17748
|
+
confidence: 0.94
|
|
17749
|
+
});
|
|
17750
|
+
if (simpleName && /^[A-Za-z_][A-Za-z0-9_]*$/.test(simpleName)) {
|
|
17751
|
+
symbolToPackage.set(simpleName, pkg2.key);
|
|
17752
|
+
}
|
|
17753
|
+
}
|
|
17754
|
+
continue;
|
|
17755
|
+
}
|
|
17756
|
+
const varDecl = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:=|;)/);
|
|
17757
|
+
if (varDecl) {
|
|
17758
|
+
const typeName = varDecl[1];
|
|
17759
|
+
const variableName = varDecl[2];
|
|
17760
|
+
const pkgKey2 = symbolToPackage.get(typeName);
|
|
17761
|
+
if (pkgKey2) {
|
|
17762
|
+
symbolToPackage.set(variableName, pkgKey2);
|
|
17763
|
+
}
|
|
17764
|
+
}
|
|
17765
|
+
const callMatch = line.match(/\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
17766
|
+
if (!callMatch) continue;
|
|
17767
|
+
const lhs = callMatch[1];
|
|
17768
|
+
const member = callMatch[2];
|
|
17769
|
+
const pkgKey = symbolToPackage.get(lhs);
|
|
17770
|
+
if (!pkgKey) continue;
|
|
17771
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17772
|
+
packageKey: pkgKey,
|
|
17773
|
+
line: lineNumber,
|
|
17774
|
+
matchKind: "call",
|
|
17775
|
+
calledSymbol: `${lhs}.${member}`,
|
|
17776
|
+
confidence: 0.8
|
|
17777
|
+
});
|
|
17778
|
+
}
|
|
17779
|
+
}
|
|
17780
|
+
return toAnalyzerResult(state);
|
|
17781
|
+
}
|
|
17782
|
+
patternForPackage(packageName, ecosystem) {
|
|
17783
|
+
const [groupIdRaw, artifactIdRaw] = packageName.split(":");
|
|
17784
|
+
const groupId = (groupIdRaw ?? "").trim();
|
|
17785
|
+
const artifactId = (artifactIdRaw ?? "").trim();
|
|
17786
|
+
const candidates = uniqueTokens([
|
|
17787
|
+
groupId,
|
|
17788
|
+
groupId && artifactId ? `${groupId}.${artifactId.replace(/-/g, ".")}` : "",
|
|
17789
|
+
artifactId ? artifactId.replace(/-/g, ".") : "",
|
|
17790
|
+
artifactId ? toIdentifierToken(artifactId).replace(/_/g, ".") : ""
|
|
17791
|
+
]);
|
|
17792
|
+
return {
|
|
17793
|
+
key: packageKey(ecosystem, packageName),
|
|
17794
|
+
candidates
|
|
17795
|
+
};
|
|
17796
|
+
}
|
|
17797
|
+
};
|
|
17798
|
+
function stripLineComment(line) {
|
|
17799
|
+
const idx = line.indexOf("//");
|
|
17800
|
+
return idx === -1 ? line : line.slice(0, idx);
|
|
17801
|
+
}
|
|
17802
|
+
|
|
17803
|
+
// src/context/analyzers/dotnet-ast-analyzer.ts
|
|
17804
|
+
var CSHARP_EXTENSIONS = /* @__PURE__ */ new Set([".cs"]);
|
|
17805
|
+
var DotnetAstAnalyzer = class {
|
|
17806
|
+
name = "dotnet-ast-analyzer";
|
|
17807
|
+
ecosystems = /* @__PURE__ */ new Set(["nuget"]);
|
|
17808
|
+
supports(ecosystem) {
|
|
17809
|
+
return this.ecosystems.has(ecosystem);
|
|
17810
|
+
}
|
|
17811
|
+
async analyze(context) {
|
|
17812
|
+
const state = initState(context.packages);
|
|
17813
|
+
const packagePatterns = context.packages.map(
|
|
17814
|
+
(pkg2) => this.patternForPackage(pkg2.packageName, pkg2.ecosystem)
|
|
17815
|
+
);
|
|
17816
|
+
let files;
|
|
17817
|
+
try {
|
|
17818
|
+
files = await collectSourceFiles2(context.projectPath, CSHARP_EXTENSIONS);
|
|
17819
|
+
} catch (err) {
|
|
17820
|
+
return errorResultFromMessage(
|
|
17821
|
+
context,
|
|
17822
|
+
this.name,
|
|
17823
|
+
err instanceof Error ? err.message : String(err),
|
|
17824
|
+
"Failed to enumerate C# source files"
|
|
17825
|
+
);
|
|
17826
|
+
}
|
|
17827
|
+
for (const filePath of files) {
|
|
17828
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) break;
|
|
17829
|
+
const sourceText = await readSourceFile(this.name, context.ecosystem, filePath, state.errors);
|
|
17830
|
+
if (sourceText === null) continue;
|
|
17831
|
+
const lines = sourceText.split(/\r?\n/);
|
|
17832
|
+
const symbolToPackage = /* @__PURE__ */ new Map();
|
|
17833
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
17834
|
+
const line = stripLineComment2(lines[idx]).trim();
|
|
17835
|
+
const lineNumber = idx + 1;
|
|
17836
|
+
if (!line) continue;
|
|
17837
|
+
const aliasUsing = line.match(/^using\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([A-Za-z0-9_.]+)\s*;/);
|
|
17838
|
+
if (aliasUsing) {
|
|
17839
|
+
const alias = aliasUsing[1];
|
|
17840
|
+
const targetNamespace = aliasUsing[2];
|
|
17841
|
+
for (const pkg2 of packagePatterns) {
|
|
17842
|
+
if (!pkg2.namespaceCandidates.some((candidate) => targetNamespace.startsWith(candidate))) continue;
|
|
17843
|
+
symbolToPackage.set(alias, pkg2.key);
|
|
17844
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17845
|
+
packageKey: pkg2.key,
|
|
17846
|
+
line: lineNumber,
|
|
17847
|
+
matchKind: "import",
|
|
17848
|
+
confidence: 0.93
|
|
17849
|
+
});
|
|
17850
|
+
}
|
|
17851
|
+
continue;
|
|
17852
|
+
}
|
|
17853
|
+
const usingMatch = line.match(/^using\s+([A-Za-z0-9_.]+)\s*;/);
|
|
17854
|
+
if (usingMatch) {
|
|
17855
|
+
const namespaceName = usingMatch[1];
|
|
17856
|
+
const tailSymbol = namespaceName.split(".").at(-1) ?? namespaceName;
|
|
17857
|
+
for (const pkg2 of packagePatterns) {
|
|
17858
|
+
if (!pkg2.namespaceCandidates.some((candidate) => namespaceName.startsWith(candidate))) continue;
|
|
17859
|
+
symbolToPackage.set(tailSymbol, pkg2.key);
|
|
17860
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17861
|
+
packageKey: pkg2.key,
|
|
17862
|
+
line: lineNumber,
|
|
17863
|
+
matchKind: "import",
|
|
17864
|
+
confidence: 0.93
|
|
17865
|
+
});
|
|
17866
|
+
}
|
|
17867
|
+
continue;
|
|
17868
|
+
}
|
|
17869
|
+
const varDecl = line.match(/^([A-Za-z_][A-Za-z0-9_.]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:=|;)/);
|
|
17870
|
+
if (varDecl) {
|
|
17871
|
+
const typeName = varDecl[1].split(".").at(-1) ?? varDecl[1];
|
|
17872
|
+
const variableName = varDecl[2];
|
|
17873
|
+
const pkgKey = symbolToPackage.get(typeName);
|
|
17874
|
+
if (pkgKey) {
|
|
17875
|
+
symbolToPackage.set(variableName, pkgKey);
|
|
17876
|
+
}
|
|
17877
|
+
}
|
|
17878
|
+
const memberCall = line.match(/\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
17879
|
+
if (memberCall) {
|
|
17880
|
+
const lhs = memberCall[1];
|
|
17881
|
+
const method = memberCall[2];
|
|
17882
|
+
const pkgKey = symbolToPackage.get(lhs) ?? this.findSymbolCandidatePackage(lhs, packagePatterns);
|
|
17883
|
+
if (pkgKey) {
|
|
17884
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17885
|
+
packageKey: pkgKey,
|
|
17886
|
+
line: lineNumber,
|
|
17887
|
+
matchKind: "call",
|
|
17888
|
+
calledSymbol: `${lhs}.${method}`,
|
|
17889
|
+
confidence: 0.79
|
|
17890
|
+
});
|
|
17891
|
+
}
|
|
17892
|
+
}
|
|
17893
|
+
}
|
|
17894
|
+
}
|
|
17895
|
+
return toAnalyzerResult(state);
|
|
17896
|
+
}
|
|
17897
|
+
patternForPackage(packageName, ecosystem) {
|
|
17898
|
+
const dotted = packageName.replace(/-/g, ".");
|
|
17899
|
+
const segments = dotted.split(".");
|
|
17900
|
+
const symbolCandidates = uniqueTokens([
|
|
17901
|
+
segments.at(-1) ?? "",
|
|
17902
|
+
segments.at(-2) ?? "",
|
|
17903
|
+
packageName.split(".").at(-1) ?? ""
|
|
17904
|
+
]);
|
|
17905
|
+
const namespaceCandidates = uniqueTokens([
|
|
17906
|
+
dotted,
|
|
17907
|
+
segments.slice(0, -1).join("."),
|
|
17908
|
+
segments.slice(0, Math.min(2, segments.length)).join(".")
|
|
17909
|
+
]);
|
|
17383
17910
|
return {
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17911
|
+
key: packageKey(ecosystem, packageName),
|
|
17912
|
+
namespaceCandidates,
|
|
17913
|
+
symbolCandidates
|
|
17387
17914
|
};
|
|
17388
17915
|
}
|
|
17916
|
+
findSymbolCandidatePackage(symbol, packagePatterns) {
|
|
17917
|
+
for (const pkg2 of packagePatterns) {
|
|
17918
|
+
if (pkg2.symbolCandidates.includes(symbol)) {
|
|
17919
|
+
return pkg2.key;
|
|
17920
|
+
}
|
|
17921
|
+
}
|
|
17922
|
+
return null;
|
|
17923
|
+
}
|
|
17389
17924
|
};
|
|
17925
|
+
function stripLineComment2(line) {
|
|
17926
|
+
const idx = line.indexOf("//");
|
|
17927
|
+
return idx === -1 ? line : line.slice(0, idx);
|
|
17928
|
+
}
|
|
17929
|
+
|
|
17930
|
+
// src/context/analyzers/rust-ast-analyzer.ts
|
|
17931
|
+
var RUST_EXTENSIONS = /* @__PURE__ */ new Set([".rs"]);
|
|
17932
|
+
var RustAstAnalyzer = class {
|
|
17933
|
+
name = "rust-ast-analyzer";
|
|
17934
|
+
ecosystems = /* @__PURE__ */ new Set(["cargo"]);
|
|
17935
|
+
supports(ecosystem) {
|
|
17936
|
+
return this.ecosystems.has(ecosystem);
|
|
17937
|
+
}
|
|
17938
|
+
async analyze(context) {
|
|
17939
|
+
const state = initState(context.packages);
|
|
17940
|
+
const packagePatterns = context.packages.map(
|
|
17941
|
+
(pkg2) => this.patternForPackage(pkg2.packageName, pkg2.ecosystem)
|
|
17942
|
+
);
|
|
17943
|
+
let files;
|
|
17944
|
+
try {
|
|
17945
|
+
files = await collectSourceFiles2(context.projectPath, RUST_EXTENSIONS);
|
|
17946
|
+
} catch (err) {
|
|
17947
|
+
return errorResultFromMessage(
|
|
17948
|
+
context,
|
|
17949
|
+
this.name,
|
|
17950
|
+
err instanceof Error ? err.message : String(err),
|
|
17951
|
+
"Failed to enumerate Rust source files"
|
|
17952
|
+
);
|
|
17953
|
+
}
|
|
17954
|
+
for (const filePath of files) {
|
|
17955
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) break;
|
|
17956
|
+
const sourceText = await readSourceFile(this.name, context.ecosystem, filePath, state.errors);
|
|
17957
|
+
if (sourceText === null) continue;
|
|
17958
|
+
const lines = sourceText.split(/\r?\n/);
|
|
17959
|
+
const symbolToPackage = /* @__PURE__ */ new Map();
|
|
17960
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
17961
|
+
const line = stripLineComment3(lines[idx]).trim();
|
|
17962
|
+
const lineNumber = idx + 1;
|
|
17963
|
+
if (!line) continue;
|
|
17964
|
+
const useMatch = line.match(/^use\s+([A-Za-z_][A-Za-z0-9_:]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?\s*;/);
|
|
17965
|
+
if (useMatch) {
|
|
17966
|
+
const pathExpr = useMatch[1];
|
|
17967
|
+
const alias = useMatch[2] ?? pathExpr.split("::").at(0) ?? pathExpr;
|
|
17968
|
+
const crate = pathExpr.split("::").at(0) ?? pathExpr;
|
|
17969
|
+
for (const pkg2 of packagePatterns) {
|
|
17970
|
+
if (!pkg2.crateNames.includes(crate)) continue;
|
|
17971
|
+
symbolToPackage.set(alias, pkg2.key);
|
|
17972
|
+
symbolToPackage.set(crate, pkg2.key);
|
|
17973
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17974
|
+
packageKey: pkg2.key,
|
|
17975
|
+
line: lineNumber,
|
|
17976
|
+
matchKind: "import",
|
|
17977
|
+
confidence: 0.94
|
|
17978
|
+
});
|
|
17979
|
+
}
|
|
17980
|
+
continue;
|
|
17981
|
+
}
|
|
17982
|
+
const externMatch = line.match(/^extern\s+crate\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?\s*;/);
|
|
17983
|
+
if (externMatch) {
|
|
17984
|
+
const crate = externMatch[1];
|
|
17985
|
+
const alias = externMatch[2] ?? crate;
|
|
17986
|
+
for (const pkg2 of packagePatterns) {
|
|
17987
|
+
if (!pkg2.crateNames.includes(crate)) continue;
|
|
17988
|
+
symbolToPackage.set(alias, pkg2.key);
|
|
17989
|
+
symbolToPackage.set(crate, pkg2.key);
|
|
17990
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
17991
|
+
packageKey: pkg2.key,
|
|
17992
|
+
line: lineNumber,
|
|
17993
|
+
matchKind: "import",
|
|
17994
|
+
confidence: 0.94
|
|
17995
|
+
});
|
|
17996
|
+
}
|
|
17997
|
+
continue;
|
|
17998
|
+
}
|
|
17999
|
+
const scopedCall = line.match(/\b([A-Za-z_][A-Za-z0-9_]*)::([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
18000
|
+
if (scopedCall) {
|
|
18001
|
+
const lhs = scopedCall[1];
|
|
18002
|
+
const func = scopedCall[2];
|
|
18003
|
+
const pkgKey = symbolToPackage.get(lhs);
|
|
18004
|
+
if (pkgKey) {
|
|
18005
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18006
|
+
packageKey: pkgKey,
|
|
18007
|
+
line: lineNumber,
|
|
18008
|
+
matchKind: "call",
|
|
18009
|
+
calledSymbol: `${lhs}::${func}`,
|
|
18010
|
+
confidence: 0.8
|
|
18011
|
+
});
|
|
18012
|
+
}
|
|
18013
|
+
}
|
|
18014
|
+
const methodCall = line.match(/\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
18015
|
+
if (methodCall) {
|
|
18016
|
+
const lhs = methodCall[1];
|
|
18017
|
+
const method = methodCall[2];
|
|
18018
|
+
const pkgKey = symbolToPackage.get(lhs);
|
|
18019
|
+
if (pkgKey) {
|
|
18020
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18021
|
+
packageKey: pkgKey,
|
|
18022
|
+
line: lineNumber,
|
|
18023
|
+
matchKind: "call",
|
|
18024
|
+
calledSymbol: `${lhs}.${method}`,
|
|
18025
|
+
confidence: 0.78
|
|
18026
|
+
});
|
|
18027
|
+
}
|
|
18028
|
+
}
|
|
18029
|
+
}
|
|
18030
|
+
}
|
|
18031
|
+
return toAnalyzerResult(state);
|
|
18032
|
+
}
|
|
18033
|
+
patternForPackage(packageName, ecosystem) {
|
|
18034
|
+
const crateName = packageName.replace(/-/g, "_");
|
|
18035
|
+
return {
|
|
18036
|
+
key: packageKey(ecosystem, packageName),
|
|
18037
|
+
crateNames: uniqueTokens([crateName, packageName])
|
|
18038
|
+
};
|
|
18039
|
+
}
|
|
18040
|
+
};
|
|
18041
|
+
function stripLineComment3(line) {
|
|
18042
|
+
const idx = line.indexOf("//");
|
|
18043
|
+
return idx === -1 ? line : line.slice(0, idx);
|
|
18044
|
+
}
|
|
18045
|
+
|
|
18046
|
+
// src/context/analyzers/go-ast-analyzer.ts
|
|
18047
|
+
var GO_EXTENSIONS = /* @__PURE__ */ new Set([".go"]);
|
|
18048
|
+
var GoAstAnalyzer = class {
|
|
18049
|
+
name = "go-ast-analyzer";
|
|
18050
|
+
ecosystems = /* @__PURE__ */ new Set(["go"]);
|
|
18051
|
+
supports(ecosystem) {
|
|
18052
|
+
return this.ecosystems.has(ecosystem);
|
|
18053
|
+
}
|
|
18054
|
+
async analyze(context) {
|
|
18055
|
+
const state = initState(context.packages);
|
|
18056
|
+
const packagePatterns = context.packages.map((pkg2) => this.patternForPackage(pkg2.packageName, pkg2.ecosystem));
|
|
18057
|
+
let files;
|
|
18058
|
+
try {
|
|
18059
|
+
files = await collectSourceFiles2(context.projectPath, GO_EXTENSIONS);
|
|
18060
|
+
} catch (err) {
|
|
18061
|
+
return errorResultFromMessage(
|
|
18062
|
+
context,
|
|
18063
|
+
this.name,
|
|
18064
|
+
err instanceof Error ? err.message : String(err),
|
|
18065
|
+
"Failed to enumerate Go source files"
|
|
18066
|
+
);
|
|
18067
|
+
}
|
|
18068
|
+
for (const filePath of files) {
|
|
18069
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) break;
|
|
18070
|
+
const sourceText = await readSourceFile(this.name, context.ecosystem, filePath, state.errors);
|
|
18071
|
+
if (sourceText === null) continue;
|
|
18072
|
+
const aliasesByPackage = /* @__PURE__ */ new Map();
|
|
18073
|
+
for (const pkg2 of packagePatterns) {
|
|
18074
|
+
aliasesByPackage.set(pkg2.key, new Set(pkg2.aliases));
|
|
18075
|
+
}
|
|
18076
|
+
const lines = sourceText.split(/\r?\n/);
|
|
18077
|
+
let inImportBlock = false;
|
|
18078
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
18079
|
+
const lineNumber = idx + 1;
|
|
18080
|
+
const line = lines[idx];
|
|
18081
|
+
const trimmed = line.trim();
|
|
18082
|
+
if (trimmed.startsWith("import (")) {
|
|
18083
|
+
inImportBlock = true;
|
|
18084
|
+
continue;
|
|
18085
|
+
}
|
|
18086
|
+
if (inImportBlock && trimmed === ")") {
|
|
18087
|
+
inImportBlock = false;
|
|
18088
|
+
continue;
|
|
18089
|
+
}
|
|
18090
|
+
const match = this.extractImport(trimmed, inImportBlock);
|
|
18091
|
+
if (match) {
|
|
18092
|
+
for (const pkg2 of packagePatterns) {
|
|
18093
|
+
if (match.importPath !== pkg2.packageName) continue;
|
|
18094
|
+
const alias = match.alias && match.alias !== "_" && match.alias !== "." ? toIdentifierToken(match.alias) : pkg2.aliases[0];
|
|
18095
|
+
if (alias) {
|
|
18096
|
+
aliasesByPackage.get(pkg2.key)?.add(alias);
|
|
18097
|
+
}
|
|
18098
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18099
|
+
packageKey: pkg2.key,
|
|
18100
|
+
line: lineNumber,
|
|
18101
|
+
matchKind: "import",
|
|
18102
|
+
confidence: 0.95
|
|
18103
|
+
});
|
|
18104
|
+
}
|
|
18105
|
+
}
|
|
18106
|
+
for (const pkg2 of packagePatterns) {
|
|
18107
|
+
const aliases = aliasesByPackage.get(pkg2.key);
|
|
18108
|
+
if (!aliases) continue;
|
|
18109
|
+
for (const alias of aliases) {
|
|
18110
|
+
if (!alias) continue;
|
|
18111
|
+
const escapedAlias = escapeRegex2(alias);
|
|
18112
|
+
const callRegex = new RegExp(`\\b${escapedAlias}\\s*\\.\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`);
|
|
18113
|
+
const callMatch = line.match(callRegex);
|
|
18114
|
+
if (!callMatch) continue;
|
|
18115
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18116
|
+
packageKey: pkg2.key,
|
|
18117
|
+
line: lineNumber,
|
|
18118
|
+
matchKind: "call",
|
|
18119
|
+
calledSymbol: `${alias}.${callMatch[1]}`,
|
|
18120
|
+
confidence: 0.8
|
|
18121
|
+
});
|
|
18122
|
+
}
|
|
18123
|
+
}
|
|
18124
|
+
}
|
|
18125
|
+
}
|
|
18126
|
+
return toAnalyzerResult(state);
|
|
18127
|
+
}
|
|
18128
|
+
patternForPackage(packageName, ecosystem) {
|
|
18129
|
+
const baseName = basePackageName(packageName);
|
|
18130
|
+
const identifierBase = toIdentifierToken(baseName.replace(/^v[0-9]+$/, ""));
|
|
18131
|
+
const shortened = identifierBase.replace(/go$/i, "") || identifierBase;
|
|
18132
|
+
return {
|
|
18133
|
+
key: packageKey(ecosystem, packageName),
|
|
18134
|
+
packageName,
|
|
18135
|
+
aliases: uniqueTokens([identifierBase, shortened])
|
|
18136
|
+
};
|
|
18137
|
+
}
|
|
18138
|
+
extractImport(trimmedLine, inImportBlock) {
|
|
18139
|
+
if (!inImportBlock && !trimmedLine.startsWith("import ")) return null;
|
|
18140
|
+
if (inImportBlock) {
|
|
18141
|
+
const blockMatch = trimmedLine.match(/^(?:(\.|_|[A-Za-z_][A-Za-z0-9_]*)\s+)?\"([^\"]+)\"/);
|
|
18142
|
+
if (!blockMatch) return null;
|
|
18143
|
+
return {
|
|
18144
|
+
alias: blockMatch[1] ?? null,
|
|
18145
|
+
importPath: blockMatch[2]
|
|
18146
|
+
};
|
|
18147
|
+
}
|
|
18148
|
+
const singleMatch = trimmedLine.match(/^import\s+(?:(\.|_|[A-Za-z_][A-Za-z0-9_]*)\s+)?\"([^\"]+)\"/);
|
|
18149
|
+
if (!singleMatch) return null;
|
|
18150
|
+
return {
|
|
18151
|
+
alias: singleMatch[1] ?? null,
|
|
18152
|
+
importPath: singleMatch[2]
|
|
18153
|
+
};
|
|
18154
|
+
}
|
|
18155
|
+
};
|
|
18156
|
+
function escapeRegex2(value) {
|
|
18157
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18158
|
+
}
|
|
18159
|
+
|
|
18160
|
+
// src/context/analyzers/ruby-ast-analyzer.ts
|
|
18161
|
+
var RUBY_EXTENSIONS = /* @__PURE__ */ new Set([".rb"]);
|
|
18162
|
+
var RubyAstAnalyzer = class {
|
|
18163
|
+
name = "ruby-ast-analyzer";
|
|
18164
|
+
ecosystems = /* @__PURE__ */ new Set(["ruby"]);
|
|
18165
|
+
supports(ecosystem) {
|
|
18166
|
+
return this.ecosystems.has(ecosystem);
|
|
18167
|
+
}
|
|
18168
|
+
async analyze(context) {
|
|
18169
|
+
const state = initState(context.packages);
|
|
18170
|
+
const packagePatterns = context.packages.map(
|
|
18171
|
+
(pkg2) => this.patternForPackage(pkg2.packageName, pkg2.ecosystem)
|
|
18172
|
+
);
|
|
18173
|
+
let files;
|
|
18174
|
+
try {
|
|
18175
|
+
files = await collectSourceFiles2(context.projectPath, RUBY_EXTENSIONS);
|
|
18176
|
+
} catch (err) {
|
|
18177
|
+
return errorResultFromMessage(
|
|
18178
|
+
context,
|
|
18179
|
+
this.name,
|
|
18180
|
+
err instanceof Error ? err.message : String(err),
|
|
18181
|
+
"Failed to enumerate Ruby source files"
|
|
18182
|
+
);
|
|
18183
|
+
}
|
|
18184
|
+
for (const filePath of files) {
|
|
18185
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) break;
|
|
18186
|
+
const sourceText = await readSourceFile(this.name, context.ecosystem, filePath, state.errors);
|
|
18187
|
+
if (sourceText === null) continue;
|
|
18188
|
+
const lines = sourceText.split(/\r?\n/);
|
|
18189
|
+
const symbolToPackage = /* @__PURE__ */ new Map();
|
|
18190
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
18191
|
+
const line = stripInlineComment2(lines[idx]).trim();
|
|
18192
|
+
const lineNumber = idx + 1;
|
|
18193
|
+
if (!line) continue;
|
|
18194
|
+
const requireMatch = line.match(/^require(?:_relative)?\s+['"]([^'"]+)['"]/);
|
|
18195
|
+
if (requireMatch) {
|
|
18196
|
+
const requiredPath = requireMatch[1];
|
|
18197
|
+
for (const pkg2 of packagePatterns) {
|
|
18198
|
+
if (!pkg2.requireCandidates.some((candidate) => requiredPath.includes(candidate))) continue;
|
|
18199
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18200
|
+
packageKey: pkg2.key,
|
|
18201
|
+
line: lineNumber,
|
|
18202
|
+
matchKind: "require",
|
|
18203
|
+
confidence: 0.93
|
|
18204
|
+
});
|
|
18205
|
+
for (const constant of pkg2.constantCandidates) {
|
|
18206
|
+
symbolToPackage.set(constant, pkg2.key);
|
|
18207
|
+
}
|
|
18208
|
+
}
|
|
18209
|
+
continue;
|
|
18210
|
+
}
|
|
18211
|
+
const includeMatch = line.match(/^include\s+([A-Za-z_][A-Za-z0-9_:]*)/);
|
|
18212
|
+
if (includeMatch) {
|
|
18213
|
+
const moduleName = includeMatch[1];
|
|
18214
|
+
for (const pkg2 of packagePatterns) {
|
|
18215
|
+
if (!pkg2.constantCandidates.some((candidate) => moduleName.startsWith(candidate))) continue;
|
|
18216
|
+
symbolToPackage.set(moduleName.split("::").at(0) ?? moduleName, pkg2.key);
|
|
18217
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18218
|
+
packageKey: pkg2.key,
|
|
18219
|
+
line: lineNumber,
|
|
18220
|
+
matchKind: "import",
|
|
18221
|
+
confidence: 0.9
|
|
18222
|
+
});
|
|
18223
|
+
}
|
|
18224
|
+
continue;
|
|
18225
|
+
}
|
|
18226
|
+
const namespacedCall = line.match(/\b([A-Z][A-Za-z0-9_:]*)\s*(?:\.|::)\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
18227
|
+
if (namespacedCall) {
|
|
18228
|
+
const lhs = namespacedCall[1].split("::").at(0) ?? namespacedCall[1];
|
|
18229
|
+
const method = namespacedCall[2];
|
|
18230
|
+
const pkgKey = symbolToPackage.get(lhs) ?? this.findConstantCandidatePackage(lhs, packagePatterns);
|
|
18231
|
+
if (pkgKey) {
|
|
18232
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18233
|
+
packageKey: pkgKey,
|
|
18234
|
+
line: lineNumber,
|
|
18235
|
+
matchKind: "call",
|
|
18236
|
+
calledSymbol: `${lhs}.${method}`,
|
|
18237
|
+
confidence: 0.78
|
|
18238
|
+
});
|
|
18239
|
+
}
|
|
18240
|
+
}
|
|
18241
|
+
}
|
|
18242
|
+
}
|
|
18243
|
+
return toAnalyzerResult(state);
|
|
18244
|
+
}
|
|
18245
|
+
patternForPackage(packageName, ecosystem) {
|
|
18246
|
+
const base = basePackageName(packageName);
|
|
18247
|
+
const normalized = base.toLowerCase();
|
|
18248
|
+
const requireCandidates = uniqueTokens([
|
|
18249
|
+
normalized,
|
|
18250
|
+
normalized.replace(/-/g, "/"),
|
|
18251
|
+
normalized.replace(/-/g, "_")
|
|
18252
|
+
]);
|
|
18253
|
+
const constantParts = normalized.replace(/[^a-z0-9_/-]/g, "").split(/[\/_-]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1));
|
|
18254
|
+
const constant = constantParts.join("::");
|
|
18255
|
+
const collapsedConstant = constantParts.join("");
|
|
18256
|
+
return {
|
|
18257
|
+
key: packageKey(ecosystem, packageName),
|
|
18258
|
+
requireCandidates,
|
|
18259
|
+
constantCandidates: uniqueTokens([constant, collapsedConstant, constantParts.at(0) ?? ""])
|
|
18260
|
+
};
|
|
18261
|
+
}
|
|
18262
|
+
findConstantCandidatePackage(constant, packagePatterns) {
|
|
18263
|
+
for (const pkg2 of packagePatterns) {
|
|
18264
|
+
if (pkg2.constantCandidates.includes(constant)) {
|
|
18265
|
+
return pkg2.key;
|
|
18266
|
+
}
|
|
18267
|
+
}
|
|
18268
|
+
return null;
|
|
18269
|
+
}
|
|
18270
|
+
};
|
|
18271
|
+
function stripInlineComment2(line) {
|
|
18272
|
+
const idx = line.indexOf("#");
|
|
18273
|
+
return idx === -1 ? line : line.slice(0, idx);
|
|
18274
|
+
}
|
|
18275
|
+
|
|
18276
|
+
// src/context/analyzers/php-ast-analyzer.ts
|
|
18277
|
+
var PHP_EXTENSIONS = /* @__PURE__ */ new Set([".php"]);
|
|
18278
|
+
var PhpAstAnalyzer = class {
|
|
18279
|
+
name = "php-ast-analyzer";
|
|
18280
|
+
ecosystems = /* @__PURE__ */ new Set(["composer"]);
|
|
18281
|
+
supports(ecosystem) {
|
|
18282
|
+
return this.ecosystems.has(ecosystem);
|
|
18283
|
+
}
|
|
18284
|
+
async analyze(context) {
|
|
18285
|
+
const state = initState(context.packages);
|
|
18286
|
+
const packagePatterns = context.packages.map(
|
|
18287
|
+
(pkg2) => this.patternForPackage(pkg2.packageName, pkg2.ecosystem)
|
|
18288
|
+
);
|
|
18289
|
+
let files;
|
|
18290
|
+
try {
|
|
18291
|
+
files = await collectSourceFiles2(context.projectPath, PHP_EXTENSIONS);
|
|
18292
|
+
} catch (err) {
|
|
18293
|
+
return errorResultFromMessage(
|
|
18294
|
+
context,
|
|
18295
|
+
this.name,
|
|
18296
|
+
err instanceof Error ? err.message : String(err),
|
|
18297
|
+
"Failed to enumerate PHP source files"
|
|
18298
|
+
);
|
|
18299
|
+
}
|
|
18300
|
+
for (const filePath of files) {
|
|
18301
|
+
if (state.snippetsProduced >= context.maxSnippetsTotal) break;
|
|
18302
|
+
const sourceText = await readSourceFile(this.name, context.ecosystem, filePath, state.errors);
|
|
18303
|
+
if (sourceText === null) continue;
|
|
18304
|
+
const lines = sourceText.split(/\r?\n/);
|
|
18305
|
+
const symbolToPackage = /* @__PURE__ */ new Map();
|
|
18306
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
18307
|
+
const line = stripLineComment4(lines[idx]).trim();
|
|
18308
|
+
const lineNumber = idx + 1;
|
|
18309
|
+
if (!line) continue;
|
|
18310
|
+
const useMatch = line.match(/^use\s+([A-Za-z0-9_\\]+)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?\s*;/i);
|
|
18311
|
+
if (useMatch) {
|
|
18312
|
+
const namespacePath = useMatch[1];
|
|
18313
|
+
const alias = useMatch[2] ?? namespacePath.split("\\").at(-1) ?? namespacePath;
|
|
18314
|
+
for (const pkg2 of packagePatterns) {
|
|
18315
|
+
const normalizedNamespace = namespacePath.toLowerCase();
|
|
18316
|
+
if (!pkg2.namespaceCandidates.some((candidate) => normalizedNamespace.startsWith(candidate.toLowerCase()))) continue;
|
|
18317
|
+
symbolToPackage.set(alias, pkg2.key);
|
|
18318
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18319
|
+
packageKey: pkg2.key,
|
|
18320
|
+
line: lineNumber,
|
|
18321
|
+
matchKind: "import",
|
|
18322
|
+
confidence: 0.94
|
|
18323
|
+
});
|
|
18324
|
+
}
|
|
18325
|
+
continue;
|
|
18326
|
+
}
|
|
18327
|
+
const requireMatch = line.match(/^require(?:_once)?\s*\(?\s*['"]([^'"]+)['"]\s*\)?\s*;/i);
|
|
18328
|
+
if (requireMatch) {
|
|
18329
|
+
const pathExpr = requireMatch[1].toLowerCase();
|
|
18330
|
+
for (const pkg2 of packagePatterns) {
|
|
18331
|
+
const hasVendor = pathExpr.includes(pkg2.vendor.toLowerCase());
|
|
18332
|
+
const hasPackage = pathExpr.includes(pkg2.package.toLowerCase());
|
|
18333
|
+
if (!hasVendor && !hasPackage) continue;
|
|
18334
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18335
|
+
packageKey: pkg2.key,
|
|
18336
|
+
line: lineNumber,
|
|
18337
|
+
matchKind: "require",
|
|
18338
|
+
confidence: 0.9
|
|
18339
|
+
});
|
|
18340
|
+
}
|
|
18341
|
+
continue;
|
|
18342
|
+
}
|
|
18343
|
+
const staticCall = line.match(/\b([A-Za-z_][A-Za-z0-9_]*)::([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
18344
|
+
if (staticCall) {
|
|
18345
|
+
const lhs = staticCall[1];
|
|
18346
|
+
const method = staticCall[2];
|
|
18347
|
+
const pkgKey = symbolToPackage.get(lhs) ?? this.findSymbolCandidatePackage(lhs, packagePatterns);
|
|
18348
|
+
if (pkgKey) {
|
|
18349
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18350
|
+
packageKey: pkgKey,
|
|
18351
|
+
line: lineNumber,
|
|
18352
|
+
matchKind: "call",
|
|
18353
|
+
calledSymbol: `${lhs}::${method}`,
|
|
18354
|
+
confidence: 0.8
|
|
18355
|
+
});
|
|
18356
|
+
}
|
|
18357
|
+
}
|
|
18358
|
+
const constructorMatch = line.match(/new\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
18359
|
+
if (constructorMatch) {
|
|
18360
|
+
const className = constructorMatch[1];
|
|
18361
|
+
const pkgKey = symbolToPackage.get(className) ?? this.findSymbolCandidatePackage(className, packagePatterns);
|
|
18362
|
+
if (pkgKey) {
|
|
18363
|
+
addCandidate(context, state, filePath, sourceText, {
|
|
18364
|
+
packageKey: pkgKey,
|
|
18365
|
+
line: lineNumber,
|
|
18366
|
+
matchKind: "call",
|
|
18367
|
+
calledSymbol: `new ${className}`,
|
|
18368
|
+
confidence: 0.76
|
|
18369
|
+
});
|
|
18370
|
+
}
|
|
18371
|
+
}
|
|
18372
|
+
}
|
|
18373
|
+
}
|
|
18374
|
+
return toAnalyzerResult(state);
|
|
18375
|
+
}
|
|
18376
|
+
patternForPackage(packageName, ecosystem) {
|
|
18377
|
+
const [vendorRaw, packageRaw] = packageName.split("/");
|
|
18378
|
+
const vendor = vendorRaw ?? packageName;
|
|
18379
|
+
const packagePart = packageRaw ?? packageName;
|
|
18380
|
+
const namespaceCandidates = uniqueTokens([
|
|
18381
|
+
pascalize(vendor),
|
|
18382
|
+
pascalize(packagePart),
|
|
18383
|
+
`${pascalize(vendor)}\\${pascalize(packagePart)}`,
|
|
18384
|
+
`${pascalize(vendor)}\\${pascalize(packagePart.replace(/-/g, "_"))}`
|
|
18385
|
+
]);
|
|
18386
|
+
const symbolCandidates = uniqueTokens([
|
|
18387
|
+
pascalize(packagePart),
|
|
18388
|
+
pascalize(vendor),
|
|
18389
|
+
pascalize(packagePart).split("\\").at(-1) ?? ""
|
|
18390
|
+
]);
|
|
18391
|
+
return {
|
|
18392
|
+
key: packageKey(ecosystem, packageName),
|
|
18393
|
+
vendor,
|
|
18394
|
+
package: packagePart,
|
|
18395
|
+
namespaceCandidates,
|
|
18396
|
+
symbolCandidates
|
|
18397
|
+
};
|
|
18398
|
+
}
|
|
18399
|
+
findSymbolCandidatePackage(symbol, packagePatterns) {
|
|
18400
|
+
const normalized = symbol.toLowerCase();
|
|
18401
|
+
for (const pkg2 of packagePatterns) {
|
|
18402
|
+
if (pkg2.symbolCandidates.some((candidate) => candidate.toLowerCase() === normalized)) {
|
|
18403
|
+
return pkg2.key;
|
|
18404
|
+
}
|
|
18405
|
+
}
|
|
18406
|
+
return null;
|
|
18407
|
+
}
|
|
18408
|
+
};
|
|
18409
|
+
function pascalize(input) {
|
|
18410
|
+
return input.split(/[\/_.-]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join("\\");
|
|
18411
|
+
}
|
|
18412
|
+
function stripLineComment4(line) {
|
|
18413
|
+
const slashIdx = line.indexOf("//");
|
|
18414
|
+
if (slashIdx !== -1) return line.slice(0, slashIdx);
|
|
18415
|
+
const hashIdx = line.indexOf("#");
|
|
18416
|
+
return hashIdx === -1 ? line : line.slice(0, hashIdx);
|
|
18417
|
+
}
|
|
17390
18418
|
|
|
17391
18419
|
// src/context/usage-context-engine.ts
|
|
17392
18420
|
var DEFAULT_MAX_SNIPPETS_PER_PACKAGE = 20;
|
|
@@ -17396,41 +18424,13 @@ var UsageContextEngine = class {
|
|
|
17396
18424
|
constructor(analyzers) {
|
|
17397
18425
|
this.analyzers = analyzers ?? [
|
|
17398
18426
|
new JsAstAnalyzer(),
|
|
17399
|
-
new
|
|
17400
|
-
|
|
17401
|
-
|
|
17402
|
-
|
|
17403
|
-
),
|
|
17404
|
-
new
|
|
17405
|
-
|
|
17406
|
-
["maven"],
|
|
17407
|
-
"Java AST analyzer is not yet implemented in this release"
|
|
17408
|
-
),
|
|
17409
|
-
new UnsupportedAnalyzer(
|
|
17410
|
-
"dotnet-ast-analyzer",
|
|
17411
|
-
["nuget"],
|
|
17412
|
-
"NuGet/C# AST analyzer is not yet implemented in this release"
|
|
17413
|
-
),
|
|
17414
|
-
new UnsupportedAnalyzer(
|
|
17415
|
-
"rust-ast-analyzer",
|
|
17416
|
-
["cargo"],
|
|
17417
|
-
"Rust AST analyzer is not yet implemented in this release"
|
|
17418
|
-
),
|
|
17419
|
-
new UnsupportedAnalyzer(
|
|
17420
|
-
"go-ast-analyzer",
|
|
17421
|
-
["go"],
|
|
17422
|
-
"Go AST analyzer is not yet implemented in this release"
|
|
17423
|
-
),
|
|
17424
|
-
new UnsupportedAnalyzer(
|
|
17425
|
-
"ruby-ast-analyzer",
|
|
17426
|
-
["ruby"],
|
|
17427
|
-
"Ruby AST analyzer is not yet implemented in this release"
|
|
17428
|
-
),
|
|
17429
|
-
new UnsupportedAnalyzer(
|
|
17430
|
-
"php-ast-analyzer",
|
|
17431
|
-
["composer"],
|
|
17432
|
-
"PHP AST analyzer is not yet implemented in this release"
|
|
17433
|
-
)
|
|
18427
|
+
new PythonAstAnalyzer(),
|
|
18428
|
+
new JavaAstAnalyzer(),
|
|
18429
|
+
new DotnetAstAnalyzer(),
|
|
18430
|
+
new RustAstAnalyzer(),
|
|
18431
|
+
new GoAstAnalyzer(),
|
|
18432
|
+
new RubyAstAnalyzer(),
|
|
18433
|
+
new PhpAstAnalyzer()
|
|
17434
18434
|
];
|
|
17435
18435
|
}
|
|
17436
18436
|
async analyze(input) {
|
|
@@ -17490,7 +18490,7 @@ var UsageContextEngine = class {
|
|
|
17490
18490
|
try {
|
|
17491
18491
|
const result = await analyzer.analyze(runContext);
|
|
17492
18492
|
const resultByKey = new Map(
|
|
17493
|
-
result.packages.map((pkg2) => [
|
|
18493
|
+
result.packages.map((pkg2) => [packageKey2(pkg2.ecosystem, pkg2.packageName), pkg2])
|
|
17494
18494
|
);
|
|
17495
18495
|
errors.push(...result.errors);
|
|
17496
18496
|
remainingSnippets = Math.max(0, remainingSnippets - result.snippetsProduced);
|
|
@@ -17498,7 +18498,7 @@ var UsageContextEngine = class {
|
|
|
17498
18498
|
let unsupportedCount = 0;
|
|
17499
18499
|
let analysisErrorCount = 0;
|
|
17500
18500
|
for (const pkg2 of packages) {
|
|
17501
|
-
const analyzed = resultByKey.get(
|
|
18501
|
+
const analyzed = resultByKey.get(packageKey2(pkg2.ecosystem, pkg2.packageName)) ?? {
|
|
17502
18502
|
packageName: pkg2.packageName,
|
|
17503
18503
|
ecosystem: pkg2.ecosystem,
|
|
17504
18504
|
status: "analysis_error",
|
|
@@ -17603,13 +18603,13 @@ var UsageContextEngine = class {
|
|
|
17603
18603
|
buildVulnerablePackages(vulnerabilities, dependencies) {
|
|
17604
18604
|
const directMap = /* @__PURE__ */ new Map();
|
|
17605
18605
|
for (const dependency of dependencies) {
|
|
17606
|
-
const key =
|
|
18606
|
+
const key = packageKey2(dependency.ecosystem, dependency.name);
|
|
17607
18607
|
const existing = directMap.get(key) ?? false;
|
|
17608
18608
|
directMap.set(key, existing || dependency.direct);
|
|
17609
18609
|
}
|
|
17610
18610
|
const grouped = /* @__PURE__ */ new Map();
|
|
17611
18611
|
for (const vulnerability of vulnerabilities) {
|
|
17612
|
-
const key =
|
|
18612
|
+
const key = packageKey2(vulnerability.ecosystem, vulnerability.packageName);
|
|
17613
18613
|
const existing = grouped.get(key);
|
|
17614
18614
|
if (existing) {
|
|
17615
18615
|
existing.vulnerabilities.push(vulnerability);
|
|
@@ -17633,7 +18633,7 @@ var UsageContextEngine = class {
|
|
|
17633
18633
|
return n > 0 ? n : fallback;
|
|
17634
18634
|
}
|
|
17635
18635
|
};
|
|
17636
|
-
function
|
|
18636
|
+
function packageKey2(ecosystem, packageName) {
|
|
17637
18637
|
return `${ecosystem}::${packageName}`;
|
|
17638
18638
|
}
|
|
17639
18639
|
function groupByEcosystem(packages) {
|
|
@@ -17745,7 +18745,8 @@ async function uploadToVerimu(report, config) {
|
|
|
17745
18745
|
const projectName = basename(config.projectPath);
|
|
17746
18746
|
const upsertRes = await client.upsertProject({
|
|
17747
18747
|
name: projectName,
|
|
17748
|
-
ecosystem: report.project.ecosystem
|
|
18748
|
+
ecosystem: report.project.ecosystem,
|
|
18749
|
+
groupName: config.groupName
|
|
17749
18750
|
});
|
|
17750
18751
|
const projectId = upsertRes.project.id;
|
|
17751
18752
|
const scanRes = await client.uploadSbom(projectId, buildUploadPayload(report));
|
|
@@ -17779,9 +18780,9 @@ function deriveArtifactOutputPaths(cycloneDxOutput) {
|
|
|
17779
18780
|
}
|
|
17780
18781
|
return {
|
|
17781
18782
|
cyclonedx: cycloneDxOutput,
|
|
17782
|
-
spdx:
|
|
17783
|
-
swid:
|
|
17784
|
-
usageContext:
|
|
18783
|
+
spdx: join3(parsed.dir, `${baseName}.spdx.json`),
|
|
18784
|
+
swid: join3(parsed.dir, `${baseName}.swid.xml`),
|
|
18785
|
+
usageContext: join3(parsed.dir, `${baseName}.usage-context.json`)
|
|
17785
18786
|
};
|
|
17786
18787
|
}
|
|
17787
18788
|
function buildUploadPayload(report) {
|
|
@@ -17840,16 +18841,27 @@ function renderPlatformScan(projectPath, result) {
|
|
|
17840
18841
|
return lines.join("\n");
|
|
17841
18842
|
}
|
|
17842
18843
|
function collectVulnerabilities(result) {
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
18844
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
18845
|
+
for (const scanResult of result.scanResponse.scan_results ?? []) {
|
|
18846
|
+
for (const vuln of scanResult.vulnerabilities ?? []) {
|
|
18847
|
+
const next = {
|
|
18848
|
+
dependencyName: scanResult.dependency_name,
|
|
18849
|
+
version: scanResult.version,
|
|
18850
|
+
cveId: vuln.cve_id,
|
|
18851
|
+
severity: normalizeSeverity(vuln.severity ?? "UNKNOWN"),
|
|
18852
|
+
summary: pickSummary(vuln),
|
|
18853
|
+
fixedVersion: pickFixedVersion(vuln)
|
|
18854
|
+
};
|
|
18855
|
+
const key = `${next.dependencyName}::${next.version}::${next.cveId}`;
|
|
18856
|
+
const existing = deduped.get(key);
|
|
18857
|
+
if (!existing) {
|
|
18858
|
+
deduped.set(key, next);
|
|
18859
|
+
continue;
|
|
18860
|
+
}
|
|
18861
|
+
deduped.set(key, mergeVulnerability(existing, next));
|
|
18862
|
+
}
|
|
18863
|
+
}
|
|
18864
|
+
return Array.from(deduped.values());
|
|
17853
18865
|
}
|
|
17854
18866
|
function pickSummary(vuln) {
|
|
17855
18867
|
const value = vuln.summary ?? vuln.description;
|
|
@@ -17882,6 +18894,26 @@ function normalizeSeverity(severity) {
|
|
|
17882
18894
|
return "UNKNOWN";
|
|
17883
18895
|
}
|
|
17884
18896
|
}
|
|
18897
|
+
function mergeVulnerability(current, incoming) {
|
|
18898
|
+
const severity = severityOrder2(incoming.severity) < severityOrder2(current.severity) ? incoming.severity : current.severity;
|
|
18899
|
+
const summary = pickPreferredSummary(current.summary, incoming.summary);
|
|
18900
|
+
const fixedVersion = current.fixedVersion ?? incoming.fixedVersion ?? null;
|
|
18901
|
+
return {
|
|
18902
|
+
...current,
|
|
18903
|
+
severity,
|
|
18904
|
+
summary,
|
|
18905
|
+
fixedVersion
|
|
18906
|
+
};
|
|
18907
|
+
}
|
|
18908
|
+
function pickPreferredSummary(a, b) {
|
|
18909
|
+
const normalizedA = (a ?? "").trim();
|
|
18910
|
+
const normalizedB = (b ?? "").trim();
|
|
18911
|
+
if (!normalizedA) return normalizedB || "No description available";
|
|
18912
|
+
if (!normalizedB) return normalizedA;
|
|
18913
|
+
if (normalizedA === "No description available") return normalizedB;
|
|
18914
|
+
if (normalizedB === "No description available") return normalizedA;
|
|
18915
|
+
return normalizedB.length > normalizedA.length ? normalizedB : normalizedA;
|
|
18916
|
+
}
|
|
17885
18917
|
function summarizeBySeverity(severities) {
|
|
17886
18918
|
const summary = {
|
|
17887
18919
|
CRITICAL: 0,
|
|
@@ -17916,6 +18948,325 @@ function severityBadge2(severity) {
|
|
|
17916
18948
|
return badges[severity] ?? "[???] ";
|
|
17917
18949
|
}
|
|
17918
18950
|
|
|
18951
|
+
// src/discovery/lockfile-discovery.ts
|
|
18952
|
+
import { readdir as readdir3, stat } from "fs/promises";
|
|
18953
|
+
import { existsSync as existsSync14 } from "fs";
|
|
18954
|
+
import path14 from "path";
|
|
18955
|
+
var LOCKFILE_MAP = {
|
|
18956
|
+
"pnpm-lock.yaml": { ecosystem: "npm", scanner: "pnpm" },
|
|
18957
|
+
"yarn.lock": { ecosystem: "npm", scanner: "yarn" },
|
|
18958
|
+
"package-lock.json": { ecosystem: "npm", scanner: "npm" },
|
|
18959
|
+
"deno.lock": { ecosystem: "npm", scanner: "deno" },
|
|
18960
|
+
"Cargo.lock": { ecosystem: "cargo", scanner: "cargo" },
|
|
18961
|
+
"go.sum": { ecosystem: "go", scanner: "go" },
|
|
18962
|
+
"Gemfile.lock": { ecosystem: "ruby", scanner: "ruby" },
|
|
18963
|
+
"composer.lock": { ecosystem: "composer", scanner: "composer" },
|
|
18964
|
+
"packages.lock.json": { ecosystem: "nuget", scanner: "nuget" },
|
|
18965
|
+
"poetry.lock": { ecosystem: "poetry", scanner: "poetry" },
|
|
18966
|
+
"uv.lock": { ecosystem: "uv", scanner: "uv" },
|
|
18967
|
+
"Pipfile.lock": { ecosystem: "pip", scanner: "pip" },
|
|
18968
|
+
"requirements.txt": { ecosystem: "pip", scanner: "pip" },
|
|
18969
|
+
"pom.xml": { ecosystem: "maven", scanner: "maven" }
|
|
18970
|
+
};
|
|
18971
|
+
var DEFAULT_EXCLUDES = [
|
|
18972
|
+
"node_modules",
|
|
18973
|
+
".git",
|
|
18974
|
+
".hg",
|
|
18975
|
+
".svn",
|
|
18976
|
+
"vendor",
|
|
18977
|
+
"target",
|
|
18978
|
+
"dist",
|
|
18979
|
+
"build",
|
|
18980
|
+
".next",
|
|
18981
|
+
".nuxt",
|
|
18982
|
+
"__pycache__",
|
|
18983
|
+
".venv",
|
|
18984
|
+
"venv",
|
|
18985
|
+
".tox",
|
|
18986
|
+
"coverage",
|
|
18987
|
+
".cache",
|
|
18988
|
+
"out",
|
|
18989
|
+
".output"
|
|
18990
|
+
];
|
|
18991
|
+
var LockfileDiscovery = class {
|
|
18992
|
+
lockfileNames = Object.keys(LOCKFILE_MAP);
|
|
18993
|
+
/**
|
|
18994
|
+
* Discovers all lockfiles recursively starting from rootPath.
|
|
18995
|
+
* Returns a list of projects that can be scanned.
|
|
18996
|
+
*/
|
|
18997
|
+
async discover(options) {
|
|
18998
|
+
const { rootPath, exclude, maxDepth } = options;
|
|
18999
|
+
const absoluteRoot = path14.resolve(rootPath);
|
|
19000
|
+
const discovered = [];
|
|
19001
|
+
const excludePatterns = this.buildExcludePatterns(exclude);
|
|
19002
|
+
await this.walkDirectory(absoluteRoot, absoluteRoot, discovered, {
|
|
19003
|
+
excludePatterns,
|
|
19004
|
+
maxDepth: maxDepth ?? Infinity,
|
|
19005
|
+
//not set, infinite for now, can add as a cli-flag later if needed
|
|
19006
|
+
currentDepth: 0
|
|
19007
|
+
});
|
|
19008
|
+
discovered.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
19009
|
+
return discovered;
|
|
19010
|
+
}
|
|
19011
|
+
/**
|
|
19012
|
+
* Recursively walks directories looking for lockfiles.
|
|
19013
|
+
* Stops descending into a directory once a lockfile is found
|
|
19014
|
+
* (to avoid scanning nested node_modules, etc.)
|
|
19015
|
+
*/
|
|
19016
|
+
async walkDirectory(currentPath, rootPath, results, options) {
|
|
19017
|
+
const { excludePatterns, maxDepth, currentDepth } = options;
|
|
19018
|
+
if (currentDepth > maxDepth) return;
|
|
19019
|
+
const relativePath = path14.relative(rootPath, currentPath) || ".";
|
|
19020
|
+
if (this.matchesAnyPattern(relativePath, excludePatterns)) {
|
|
19021
|
+
return;
|
|
19022
|
+
}
|
|
19023
|
+
const foundLockfile = await this.findLockfileInDir(currentPath);
|
|
19024
|
+
if (foundLockfile) {
|
|
19025
|
+
results.push({
|
|
19026
|
+
projectPath: currentPath,
|
|
19027
|
+
relativePath,
|
|
19028
|
+
lockfile: {
|
|
19029
|
+
name: foundLockfile.name,
|
|
19030
|
+
path: path14.join(currentPath, foundLockfile.name)
|
|
19031
|
+
},
|
|
19032
|
+
ecosystem: foundLockfile.ecosystem,
|
|
19033
|
+
scannerType: foundLockfile.scanner
|
|
19034
|
+
});
|
|
19035
|
+
return;
|
|
19036
|
+
}
|
|
19037
|
+
let entries;
|
|
19038
|
+
try {
|
|
19039
|
+
entries = await readdir3(currentPath);
|
|
19040
|
+
} catch {
|
|
19041
|
+
return;
|
|
19042
|
+
}
|
|
19043
|
+
for (const entry of entries) {
|
|
19044
|
+
const entryPath = path14.join(currentPath, entry);
|
|
19045
|
+
try {
|
|
19046
|
+
const stats = await stat(entryPath);
|
|
19047
|
+
if (stats.isDirectory()) {
|
|
19048
|
+
if (this.isDefaultExclude(entry)) continue;
|
|
19049
|
+
await this.walkDirectory(entryPath, rootPath, results, {
|
|
19050
|
+
...options,
|
|
19051
|
+
currentDepth: currentDepth + 1
|
|
19052
|
+
});
|
|
19053
|
+
}
|
|
19054
|
+
} catch {
|
|
19055
|
+
}
|
|
19056
|
+
}
|
|
19057
|
+
}
|
|
19058
|
+
/**
|
|
19059
|
+
* Looks for a lockfile in the given directory.
|
|
19060
|
+
* Returns the first match in priority order.
|
|
19061
|
+
*/
|
|
19062
|
+
async findLockfileInDir(dirPath) {
|
|
19063
|
+
const priorityOrder = [
|
|
19064
|
+
"pnpm-lock.yaml",
|
|
19065
|
+
"yarn.lock",
|
|
19066
|
+
"package-lock.json",
|
|
19067
|
+
"deno.lock",
|
|
19068
|
+
"Cargo.lock",
|
|
19069
|
+
"go.sum",
|
|
19070
|
+
"poetry.lock",
|
|
19071
|
+
"uv.lock",
|
|
19072
|
+
"Pipfile.lock",
|
|
19073
|
+
"composer.lock",
|
|
19074
|
+
"Gemfile.lock",
|
|
19075
|
+
"packages.lock.json",
|
|
19076
|
+
"pom.xml",
|
|
19077
|
+
"requirements.txt"
|
|
19078
|
+
];
|
|
19079
|
+
for (const lockfileName of priorityOrder) {
|
|
19080
|
+
const lockfilePath = path14.join(dirPath, lockfileName);
|
|
19081
|
+
if (existsSync14(lockfilePath)) {
|
|
19082
|
+
const info = LOCKFILE_MAP[lockfileName];
|
|
19083
|
+
return { name: lockfileName, ...info };
|
|
19084
|
+
}
|
|
19085
|
+
}
|
|
19086
|
+
return null;
|
|
19087
|
+
}
|
|
19088
|
+
/**
|
|
19089
|
+
* Builds exclude patterns from user input + defaults.
|
|
19090
|
+
*/
|
|
19091
|
+
buildExcludePatterns(userExcludes) {
|
|
19092
|
+
const patterns = [];
|
|
19093
|
+
for (const dir of DEFAULT_EXCLUDES) {
|
|
19094
|
+
patterns.push(`**/${dir}`);
|
|
19095
|
+
patterns.push(`**/${dir}/**`);
|
|
19096
|
+
}
|
|
19097
|
+
if (userExcludes) {
|
|
19098
|
+
patterns.push(...userExcludes);
|
|
19099
|
+
}
|
|
19100
|
+
return patterns;
|
|
19101
|
+
}
|
|
19102
|
+
/**
|
|
19103
|
+
* Quick check if a directory name is in the default exclude list.
|
|
19104
|
+
*/
|
|
19105
|
+
isDefaultExclude(dirName) {
|
|
19106
|
+
return DEFAULT_EXCLUDES.includes(dirName);
|
|
19107
|
+
}
|
|
19108
|
+
/**
|
|
19109
|
+
* Checks if a path matches any of the given glob patterns.
|
|
19110
|
+
* Uses simple glob matching (supports *, **, ?).
|
|
19111
|
+
*/
|
|
19112
|
+
matchesAnyPattern(relativePath, patterns) {
|
|
19113
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
19114
|
+
for (const pattern of patterns) {
|
|
19115
|
+
if (this.matchGlob(normalized, pattern)) {
|
|
19116
|
+
return true;
|
|
19117
|
+
}
|
|
19118
|
+
}
|
|
19119
|
+
return false;
|
|
19120
|
+
}
|
|
19121
|
+
/**
|
|
19122
|
+
* Simple glob matcher supporting:
|
|
19123
|
+
* - * (matches any characters except /)
|
|
19124
|
+
* - ** (matches any characters including /)
|
|
19125
|
+
* - ? (matches single character)
|
|
19126
|
+
*/
|
|
19127
|
+
matchGlob(str, pattern) {
|
|
19128
|
+
let regex = pattern.replace(/\\/g, "/").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*").replace(/\?/g, ".");
|
|
19129
|
+
regex = `^${regex}$`;
|
|
19130
|
+
return new RegExp(regex).test(str);
|
|
19131
|
+
}
|
|
19132
|
+
};
|
|
19133
|
+
|
|
19134
|
+
// src/discovery/orchestrator.ts
|
|
19135
|
+
import { basename as basename2 } from "path";
|
|
19136
|
+
var MultiProjectOrchestrator = class {
|
|
19137
|
+
discovery = new LockfileDiscovery();
|
|
19138
|
+
reporter = new ConsoleReporter();
|
|
19139
|
+
/**
|
|
19140
|
+
* Gets a display name for a project (uses directory name instead of "." for root)
|
|
19141
|
+
*/
|
|
19142
|
+
getDisplayName(project, rootPath) {
|
|
19143
|
+
if (project.relativePath === ".") {
|
|
19144
|
+
return basename2(rootPath);
|
|
19145
|
+
}
|
|
19146
|
+
return project.relativePath;
|
|
19147
|
+
}
|
|
19148
|
+
/**
|
|
19149
|
+
* Discovers and scans all projects in a directory tree.
|
|
19150
|
+
*/
|
|
19151
|
+
async scanAll(config) {
|
|
19152
|
+
const startTime = Date.now();
|
|
19153
|
+
const apiKey = config.apiKey;
|
|
19154
|
+
const apiBaseUrl = config.apiBaseUrl;
|
|
19155
|
+
console.log(`
|
|
19156
|
+
Discovering projects in ${config.projectPath}...`);
|
|
19157
|
+
const projects = await this.discovery.discover({
|
|
19158
|
+
rootPath: config.projectPath,
|
|
19159
|
+
exclude: config.exclude
|
|
19160
|
+
});
|
|
19161
|
+
if (projects.length === 0) {
|
|
19162
|
+
console.log("No projects with lockfiles found.");
|
|
19163
|
+
return {
|
|
19164
|
+
totalDiscovered: 0,
|
|
19165
|
+
successful: [],
|
|
19166
|
+
failed: [],
|
|
19167
|
+
skipped: [],
|
|
19168
|
+
durationMs: Date.now() - startTime
|
|
19169
|
+
};
|
|
19170
|
+
}
|
|
19171
|
+
const isSingleProject = projects.length === 1;
|
|
19172
|
+
const groupName = isSingleProject ? config.groupName : config.groupName || basename2(config.projectPath);
|
|
19173
|
+
console.log(`Found ${projects.length} project(s):
|
|
19174
|
+
`);
|
|
19175
|
+
for (const p of projects) {
|
|
19176
|
+
const displayName = this.getDisplayName(p, config.projectPath);
|
|
19177
|
+
console.log(` \u2022 ${displayName} (${p.scannerType})`);
|
|
19178
|
+
}
|
|
19179
|
+
if (!isSingleProject) {
|
|
19180
|
+
if (!config.groupName) {
|
|
19181
|
+
console.log(`
|
|
19182
|
+
\u2139 Auto-grouping projects as: "${groupName}"`);
|
|
19183
|
+
console.log(" (Use --group to specify a custom group name)\n");
|
|
19184
|
+
} else {
|
|
19185
|
+
console.log(`
|
|
19186
|
+
\u2139 Grouping projects as: "${groupName}"
|
|
19187
|
+
`);
|
|
19188
|
+
}
|
|
19189
|
+
} else {
|
|
19190
|
+
console.log("");
|
|
19191
|
+
}
|
|
19192
|
+
const successful = [];
|
|
19193
|
+
const failed = [];
|
|
19194
|
+
for (let i = 0; i < projects.length; i++) {
|
|
19195
|
+
const project = projects[i];
|
|
19196
|
+
const displayName = this.getDisplayName(project, config.projectPath);
|
|
19197
|
+
console.log("\u2500".repeat(60));
|
|
19198
|
+
console.log(`[${i + 1}/${projects.length}] Scanning: ${displayName}`);
|
|
19199
|
+
console.log("\u2500".repeat(60));
|
|
19200
|
+
console.log("");
|
|
19201
|
+
try {
|
|
19202
|
+
const sbomOutput = this.deriveSbomPath(project, config.sbomOutput);
|
|
19203
|
+
const report = await scan({
|
|
19204
|
+
...config,
|
|
19205
|
+
projectPath: project.projectPath,
|
|
19206
|
+
sbomOutput,
|
|
19207
|
+
groupName,
|
|
19208
|
+
// Use auto-derived or user-provided group name
|
|
19209
|
+
apiKey: void 0
|
|
19210
|
+
// Don't upload in scan, do it separately
|
|
19211
|
+
});
|
|
19212
|
+
console.log(this.reporter.report(report));
|
|
19213
|
+
if (apiKey) {
|
|
19214
|
+
console.log("");
|
|
19215
|
+
console.log(` Syncing ${displayName} to Verimu platform...`);
|
|
19216
|
+
try {
|
|
19217
|
+
const uploadConfig = {
|
|
19218
|
+
...config,
|
|
19219
|
+
projectPath: project.projectPath,
|
|
19220
|
+
groupName,
|
|
19221
|
+
// Use auto-derived or user-provided group name
|
|
19222
|
+
apiKey,
|
|
19223
|
+
apiBaseUrl
|
|
19224
|
+
};
|
|
19225
|
+
const uploadResult = await uploadToVerimu(report, uploadConfig);
|
|
19226
|
+
if (uploadResult.projectCreated) {
|
|
19227
|
+
console.log(` \u2713 Project created: ${displayName}`);
|
|
19228
|
+
}
|
|
19229
|
+
console.log(` \u2713 ${uploadResult.totalDependencies} dependencies tracked`);
|
|
19230
|
+
console.log(renderPlatformScan(displayName, uploadResult));
|
|
19231
|
+
console.log(` \u2713 Dashboard: ${uploadResult.dashboardUrl}`);
|
|
19232
|
+
} catch (err) {
|
|
19233
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19234
|
+
console.log(` \u26A0 Platform sync failed: ${msg}`);
|
|
19235
|
+
console.log(" Your SBOM was still generated locally. You can upload it manually.");
|
|
19236
|
+
}
|
|
19237
|
+
}
|
|
19238
|
+
console.log("");
|
|
19239
|
+
successful.push({ project, report });
|
|
19240
|
+
} catch (error) {
|
|
19241
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
19242
|
+
console.log(` \u2717 Failed: ${errorMsg}`);
|
|
19243
|
+
console.log("");
|
|
19244
|
+
failed.push({ project, error: errorMsg });
|
|
19245
|
+
throw error;
|
|
19246
|
+
}
|
|
19247
|
+
}
|
|
19248
|
+
return {
|
|
19249
|
+
totalDiscovered: projects.length,
|
|
19250
|
+
successful,
|
|
19251
|
+
failed,
|
|
19252
|
+
skipped: [],
|
|
19253
|
+
durationMs: Date.now() - startTime
|
|
19254
|
+
};
|
|
19255
|
+
}
|
|
19256
|
+
/**
|
|
19257
|
+
* Derives SBOM output path for a project.
|
|
19258
|
+
* Places SBOMs in project directories by default.
|
|
19259
|
+
*/
|
|
19260
|
+
deriveSbomPath(project, configOutput) {
|
|
19261
|
+
if (configOutput) {
|
|
19262
|
+
const base = configOutput.replace(/\.cdx\.json$/, "");
|
|
19263
|
+
const sanitized = project.relativePath.replace(/[/\\]/g, "-");
|
|
19264
|
+
return `${base}.${sanitized}.cdx.json`;
|
|
19265
|
+
}
|
|
19266
|
+
return `${project.projectPath}/sbom.cdx.json`;
|
|
19267
|
+
}
|
|
19268
|
+
};
|
|
19269
|
+
|
|
17919
19270
|
// src/cli.ts
|
|
17920
19271
|
var require2 = createRequire(import.meta.url);
|
|
17921
19272
|
var pkg = require2("../package.json");
|
|
@@ -17948,7 +19299,11 @@ function parseArgs(argv) {
|
|
|
17948
19299
|
skipCveCheck: false,
|
|
17949
19300
|
skipUpload: false,
|
|
17950
19301
|
cyclonedxVersion: "1.7",
|
|
17951
|
-
contextLines: void 0
|
|
19302
|
+
contextLines: void 0,
|
|
19303
|
+
groupName: void 0,
|
|
19304
|
+
recursive: true,
|
|
19305
|
+
// Recursive by default
|
|
19306
|
+
exclude: void 0
|
|
17952
19307
|
};
|
|
17953
19308
|
let i = 0;
|
|
17954
19309
|
while (i < args.length) {
|
|
@@ -17994,6 +19349,20 @@ function parseArgs(argv) {
|
|
|
17994
19349
|
throw new Error(`Invalid CycloneDX version: ${val}`);
|
|
17995
19350
|
}
|
|
17996
19351
|
result.cyclonedxVersion = val;
|
|
19352
|
+
} else if (arg === "--group-name" || arg.startsWith("--group-name=")) {
|
|
19353
|
+
const val = arg.startsWith("--group-name=") ? arg.split("=")[1] : args[++i];
|
|
19354
|
+
if (!val || val.startsWith("--")) {
|
|
19355
|
+
throw new Error("--group-name requires a value");
|
|
19356
|
+
}
|
|
19357
|
+
result.groupName = val;
|
|
19358
|
+
} else if (arg === "--no-recursive" || arg === "--not-recursive") {
|
|
19359
|
+
result.recursive = false;
|
|
19360
|
+
} else if (arg === "--exclude") {
|
|
19361
|
+
const val = args[++i];
|
|
19362
|
+
if (!val || val.startsWith("--")) {
|
|
19363
|
+
throw new Error("--exclude requires a comma-separated list of patterns");
|
|
19364
|
+
}
|
|
19365
|
+
result.exclude = val.split(",").map((p) => p.trim());
|
|
17997
19366
|
}
|
|
17998
19367
|
i++;
|
|
17999
19368
|
}
|
|
@@ -18036,8 +19405,32 @@ async function main() {
|
|
|
18036
19405
|
// Don't pass apiKey to scan() if --skip-upload — we'll handle upload separately for better logging
|
|
18037
19406
|
apiKey: apiKey && !args.skipUpload ? void 0 : void 0,
|
|
18038
19407
|
apiBaseUrl,
|
|
18039
|
-
numContextLines: args.contextLines
|
|
19408
|
+
numContextLines: args.contextLines,
|
|
19409
|
+
groupName: args.groupName
|
|
18040
19410
|
};
|
|
19411
|
+
if (args.recursive) {
|
|
19412
|
+
const orchestrator = new MultiProjectOrchestrator();
|
|
19413
|
+
let result;
|
|
19414
|
+
try {
|
|
19415
|
+
result = await orchestrator.scanAll({
|
|
19416
|
+
...config,
|
|
19417
|
+
recursive: true,
|
|
19418
|
+
exclude: args.exclude,
|
|
19419
|
+
// Pass API key for platform uploads
|
|
19420
|
+
apiKey: apiKey && !args.skipUpload ? apiKey : void 0,
|
|
19421
|
+
apiBaseUrl
|
|
19422
|
+
});
|
|
19423
|
+
} catch (err) {
|
|
19424
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19425
|
+
logError(msg);
|
|
19426
|
+
process.exit(2);
|
|
19427
|
+
}
|
|
19428
|
+
printMultiProjectSummary(result);
|
|
19429
|
+
if (result.failed.length > 0) {
|
|
19430
|
+
process.exit(1);
|
|
19431
|
+
}
|
|
19432
|
+
return;
|
|
19433
|
+
}
|
|
18041
19434
|
let report;
|
|
18042
19435
|
try {
|
|
18043
19436
|
report = await scan(config);
|
|
@@ -18078,12 +19471,50 @@ async function main() {
|
|
|
18078
19471
|
process.exit(1);
|
|
18079
19472
|
}
|
|
18080
19473
|
}
|
|
19474
|
+
function printMultiProjectSummary(result) {
|
|
19475
|
+
console.log("\n" + "\u2500".repeat(60));
|
|
19476
|
+
console.log("Multi-Project Scan Summary");
|
|
19477
|
+
console.log("\u2500".repeat(60));
|
|
19478
|
+
console.log(`
|
|
19479
|
+
Projects discovered: ${result.totalDiscovered}`);
|
|
19480
|
+
console.log(` \u2713 Successful: ${result.successful.length}`);
|
|
19481
|
+
console.log(` \u2717 Failed: ${result.failed.length}`);
|
|
19482
|
+
if (result.successful.length > 0) {
|
|
19483
|
+
const totalDeps = result.successful.reduce(
|
|
19484
|
+
(sum, r) => sum + r.report.summary.totalDependencies,
|
|
19485
|
+
0
|
|
19486
|
+
);
|
|
19487
|
+
const totalVulns = result.successful.reduce(
|
|
19488
|
+
(sum, r) => sum + r.report.summary.totalVulnerabilities,
|
|
19489
|
+
0
|
|
19490
|
+
);
|
|
19491
|
+
console.log(`
|
|
19492
|
+
Total dependencies: ${totalDeps}`);
|
|
19493
|
+
console.log(`Total vulnerabilities: ${totalVulns}`);
|
|
19494
|
+
const critical = result.successful.reduce((sum, r) => sum + r.report.summary.critical, 0);
|
|
19495
|
+
const high = result.successful.reduce((sum, r) => sum + r.report.summary.high, 0);
|
|
19496
|
+
const medium = result.successful.reduce((sum, r) => sum + r.report.summary.medium, 0);
|
|
19497
|
+
const low = result.successful.reduce((sum, r) => sum + r.report.summary.low, 0);
|
|
19498
|
+
if (totalVulns > 0) {
|
|
19499
|
+
console.log(` Critical: ${critical}, High: ${high}, Medium: ${medium}, Low: ${low}`);
|
|
19500
|
+
}
|
|
19501
|
+
}
|
|
19502
|
+
if (result.failed.length > 0) {
|
|
19503
|
+
console.log("\nFailed projects:");
|
|
19504
|
+
for (const f of result.failed) {
|
|
19505
|
+
console.log(` \u2022 ${f.project.relativePath}: ${f.error}`);
|
|
19506
|
+
}
|
|
19507
|
+
}
|
|
19508
|
+
console.log(`
|
|
19509
|
+
Completed in ${(result.durationMs / 1e3).toFixed(2)}s`);
|
|
19510
|
+
console.log("");
|
|
19511
|
+
}
|
|
18081
19512
|
function printHelp() {
|
|
18082
19513
|
console.log(`
|
|
18083
19514
|
Verimu \u2014 CRA Compliance Scanner
|
|
18084
19515
|
|
|
18085
19516
|
Usage:
|
|
18086
|
-
verimu Scan current directory
|
|
19517
|
+
verimu Scan current directory (recursively)
|
|
18087
19518
|
verimu scan [options] Full scan (SBOM + CVE check)
|
|
18088
19519
|
verimu generate-sbom [options] Generate SBOM only (no CVE check)
|
|
18089
19520
|
verimu help Show this help
|
|
@@ -18092,23 +19523,35 @@ function printHelp() {
|
|
|
18092
19523
|
Options:
|
|
18093
19524
|
--path, -p <dir> Project directory to scan (default: .)
|
|
18094
19525
|
--output, -o <file> CycloneDX output path (SPDX/SWID are written alongside it)
|
|
19526
|
+
--group-name <name> Group name for organizing related projects in dashboard
|
|
18095
19527
|
--fail-on <severity> Exit 1 if vulns at or above: CRITICAL, HIGH, MEDIUM, LOW
|
|
18096
19528
|
--skip-cve Skip CVE vulnerability checking
|
|
18097
19529
|
--skip-upload Don't sync to Verimu platform (even if API key is set)
|
|
18098
19530
|
--context-lines <n> Snippet context lines around matches (default: 4, clamped to 0..20)
|
|
18099
19531
|
--cdx-version <ver> CycloneDX spec: 1.4, 1.5, 1.6, 1.7 (default: 1.7)
|
|
18100
19532
|
|
|
19533
|
+
Project Discovery:
|
|
19534
|
+
--no-recursive Disable recursive discovery (scan only root directory)
|
|
19535
|
+
--exclude <patterns> Exclude paths matching patterns (comma-separated globs)
|
|
19536
|
+
|
|
19537
|
+
Note: Verimu automatically discovers all projects recursively by default.
|
|
19538
|
+
For monorepos with multiple lockfiles, projects are auto-grouped by directory name.
|
|
19539
|
+
Single lockfile projects are treated normally without grouping.
|
|
19540
|
+
|
|
18101
19541
|
Environment:
|
|
18102
19542
|
VERIMU_API_KEY API key for Verimu platform (from app.verimu.com)
|
|
18103
19543
|
VERIMU_API_URL Custom API URL (default: https://api.verimu.com)
|
|
18104
19544
|
|
|
18105
19545
|
Examples:
|
|
18106
|
-
npx verimu #
|
|
19546
|
+
npx verimu # Scan all projects recursively
|
|
18107
19547
|
VERIMU_API_KEY=vmu_xxx npx verimu # Scan + sync to platform
|
|
18108
19548
|
npx verimu scan --fail-on HIGH # Fail CI on HIGH+ vulns
|
|
19549
|
+
npx verimu scan --group-name my-app # Group projects with custom name
|
|
18109
19550
|
npx verimu scan --context-lines 8 # Wider context around usage snippets
|
|
18110
19551
|
npx verimu scan --cdx-version 1.5 # Specify CycloneDX version
|
|
18111
19552
|
npx verimu scan --path ./backend --output ./reports/sbom.json
|
|
19553
|
+
npx verimu scan --no-recursive # Scan only root directory
|
|
19554
|
+
npx verimu scan --exclude "legacy/*" # Exclude legacy projects
|
|
18112
19555
|
|
|
18113
19556
|
Supported ecosystems:
|
|
18114
19557
|
npm (package-lock.json) pip (requirements.txt)
|