@vibgrate/cli 1.0.46 → 1.0.47
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/DOCS.md +95 -10
- package/README.md +68 -3
- package/dist/{baseline-AWRL3ITR.js → baseline-K7V6GAP3.js} +2 -2
- package/dist/{chunk-SKROLJET.js → chunk-NASGRGXK.js} +1 -1
- package/dist/{chunk-HEILEAVO.js → chunk-UVFIFNYG.js} +542 -196
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +37 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -227,6 +227,10 @@ function computeProjectId(relativePath, projectName, workspaceId) {
|
|
|
227
227
|
const input = workspaceId ? `${relativePath}:${projectName}:${workspaceId}` : `${relativePath}:${projectName}`;
|
|
228
228
|
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
229
229
|
}
|
|
230
|
+
function computeSolutionId(relativePath, solutionName, workspaceId) {
|
|
231
|
+
const input = workspaceId ? `${relativePath}:${solutionName}:${workspaceId}` : `${relativePath}:${solutionName}`;
|
|
232
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
233
|
+
}
|
|
230
234
|
|
|
231
235
|
// src/version.ts
|
|
232
236
|
import { createRequire } from "module";
|
|
@@ -324,6 +328,18 @@ function formatText(artifact) {
|
|
|
324
328
|
lines.push(chalk.dim(artifact.relationshipDiagram.mermaid));
|
|
325
329
|
lines.push("");
|
|
326
330
|
}
|
|
331
|
+
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
332
|
+
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
333
|
+
lines.push(chalk.bold.cyan("\u2551 Solution Drift Summary \u2551"));
|
|
334
|
+
lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
335
|
+
lines.push("");
|
|
336
|
+
for (const solution of artifact.solutions) {
|
|
337
|
+
const solScore = solution.drift?.score;
|
|
338
|
+
const color = typeof solScore === "number" ? solScore >= 70 ? chalk.green : solScore >= 40 ? chalk.yellow : chalk.red : chalk.dim;
|
|
339
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk.dim("n/a")}`);
|
|
340
|
+
}
|
|
341
|
+
lines.push("");
|
|
342
|
+
}
|
|
327
343
|
const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
|
|
328
344
|
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
329
345
|
lines.push(chalk.bold.cyan("\u2551 Drift Score Summary \u2551"));
|
|
@@ -1084,19 +1100,19 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1084
1100
|
});
|
|
1085
1101
|
|
|
1086
1102
|
// src/commands/scan.ts
|
|
1087
|
-
import * as
|
|
1103
|
+
import * as path21 from "path";
|
|
1088
1104
|
import { Command as Command3 } from "commander";
|
|
1089
1105
|
import chalk6 from "chalk";
|
|
1090
1106
|
|
|
1091
1107
|
// src/scanners/node-scanner.ts
|
|
1092
|
-
import * as
|
|
1108
|
+
import * as path4 from "path";
|
|
1093
1109
|
import * as semver2 from "semver";
|
|
1094
1110
|
|
|
1095
1111
|
// src/utils/timeout.ts
|
|
1096
1112
|
async function withTimeout(promise, ms) {
|
|
1097
1113
|
let timer;
|
|
1098
|
-
const timeout = new Promise((
|
|
1099
|
-
timer = setTimeout(() =>
|
|
1114
|
+
const timeout = new Promise((resolve10) => {
|
|
1115
|
+
timer = setTimeout(() => resolve10({ ok: false }), ms);
|
|
1100
1116
|
});
|
|
1101
1117
|
try {
|
|
1102
1118
|
const result = await Promise.race([
|
|
@@ -1110,8 +1126,78 @@ async function withTimeout(promise, ms) {
|
|
|
1110
1126
|
}
|
|
1111
1127
|
|
|
1112
1128
|
// src/scanners/npm-cache.ts
|
|
1113
|
-
import { spawn } from "child_process";
|
|
1129
|
+
import { spawn as spawn2 } from "child_process";
|
|
1114
1130
|
import * as semver from "semver";
|
|
1131
|
+
|
|
1132
|
+
// src/package-version-manifest.ts
|
|
1133
|
+
import { mkdtemp, readFile, rm } from "fs/promises";
|
|
1134
|
+
import * as path3 from "path";
|
|
1135
|
+
import * as os from "os";
|
|
1136
|
+
import { spawn } from "child_process";
|
|
1137
|
+
function runCommand(cmd, args) {
|
|
1138
|
+
return new Promise((resolve10, reject) => {
|
|
1139
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1140
|
+
let out = "";
|
|
1141
|
+
let err = "";
|
|
1142
|
+
child.stdout.on("data", (d) => out += String(d));
|
|
1143
|
+
child.stderr.on("data", (d) => err += String(d));
|
|
1144
|
+
child.on("error", reject);
|
|
1145
|
+
child.on("close", (code) => {
|
|
1146
|
+
if (code !== 0) {
|
|
1147
|
+
reject(new Error(`${cmd} ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
resolve10(out);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
async function parseManifestText(text, source) {
|
|
1155
|
+
try {
|
|
1156
|
+
return JSON.parse(text);
|
|
1157
|
+
} catch {
|
|
1158
|
+
throw new Error(`Invalid JSON in package version manifest: ${source}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async function loadManifestFromZip(zipPath) {
|
|
1162
|
+
const tmpDir = await mkdtemp(path3.join(os.tmpdir(), "vibgrate-manifest-"));
|
|
1163
|
+
try {
|
|
1164
|
+
await runCommand("unzip", ["-qq", zipPath, "-d", tmpDir]);
|
|
1165
|
+
const candidates = [
|
|
1166
|
+
path3.join(tmpDir, "package-versions.json"),
|
|
1167
|
+
path3.join(tmpDir, "manifest.json"),
|
|
1168
|
+
path3.join(tmpDir, "index.json")
|
|
1169
|
+
];
|
|
1170
|
+
for (const candidate of candidates) {
|
|
1171
|
+
try {
|
|
1172
|
+
const text = await readFile(candidate, "utf8");
|
|
1173
|
+
return await parseManifestText(text, candidate);
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
throw new Error("Zip must contain package-versions.json, manifest.json, or index.json");
|
|
1178
|
+
} finally {
|
|
1179
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function loadPackageVersionManifest(filePath) {
|
|
1183
|
+
const resolved = path3.resolve(filePath);
|
|
1184
|
+
if (resolved.toLowerCase().endsWith(".zip")) {
|
|
1185
|
+
return loadManifestFromZip(resolved);
|
|
1186
|
+
}
|
|
1187
|
+
const text = await readFile(resolved, "utf8");
|
|
1188
|
+
return parseManifestText(text, resolved);
|
|
1189
|
+
}
|
|
1190
|
+
function getManifestEntry(manifest, ecosystem, packageName) {
|
|
1191
|
+
if (!manifest) return void 0;
|
|
1192
|
+
const table = manifest[ecosystem];
|
|
1193
|
+
if (!table) return void 0;
|
|
1194
|
+
if (ecosystem === "nuget") {
|
|
1195
|
+
return table[packageName.toLowerCase()] ?? table[packageName];
|
|
1196
|
+
}
|
|
1197
|
+
return table[packageName];
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/scanners/npm-cache.ts
|
|
1115
1201
|
function stableOnly(versions) {
|
|
1116
1202
|
return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
|
|
1117
1203
|
}
|
|
@@ -1121,8 +1207,8 @@ function maxStable(versions) {
|
|
|
1121
1207
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
1122
1208
|
}
|
|
1123
1209
|
async function npmViewJson(args, cwd) {
|
|
1124
|
-
return new Promise((
|
|
1125
|
-
const child =
|
|
1210
|
+
return new Promise((resolve10, reject) => {
|
|
1211
|
+
const child = spawn2("npm", ["view", ...args, "--json"], {
|
|
1126
1212
|
cwd,
|
|
1127
1213
|
shell: true,
|
|
1128
1214
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1139,27 +1225,42 @@ async function npmViewJson(args, cwd) {
|
|
|
1139
1225
|
}
|
|
1140
1226
|
const trimmed = out.trim();
|
|
1141
1227
|
if (!trimmed) {
|
|
1142
|
-
|
|
1228
|
+
resolve10(null);
|
|
1143
1229
|
return;
|
|
1144
1230
|
}
|
|
1145
1231
|
try {
|
|
1146
|
-
|
|
1232
|
+
resolve10(JSON.parse(trimmed));
|
|
1147
1233
|
} catch {
|
|
1148
|
-
|
|
1234
|
+
resolve10(trimmed.replace(/^"|"$/g, ""));
|
|
1149
1235
|
}
|
|
1150
1236
|
});
|
|
1151
1237
|
});
|
|
1152
1238
|
}
|
|
1153
1239
|
var NpmCache = class {
|
|
1154
|
-
constructor(cwd, sem) {
|
|
1240
|
+
constructor(cwd, sem, manifest, offline = false) {
|
|
1155
1241
|
this.cwd = cwd;
|
|
1156
1242
|
this.sem = sem;
|
|
1243
|
+
this.manifest = manifest;
|
|
1244
|
+
this.offline = offline;
|
|
1157
1245
|
}
|
|
1158
1246
|
meta = /* @__PURE__ */ new Map();
|
|
1159
1247
|
get(pkg2) {
|
|
1160
1248
|
const existing = this.meta.get(pkg2);
|
|
1161
1249
|
if (existing) return existing;
|
|
1162
1250
|
const p = this.sem.run(async () => {
|
|
1251
|
+
const manifestEntry = getManifestEntry(this.manifest, "npm", pkg2);
|
|
1252
|
+
if (manifestEntry) {
|
|
1253
|
+
const stable2 = stableOnly(manifestEntry.versions ?? []);
|
|
1254
|
+
const latestStableOverall2 = maxStable(stable2);
|
|
1255
|
+
return {
|
|
1256
|
+
latest: manifestEntry.latest ?? latestStableOverall2,
|
|
1257
|
+
stableVersions: stable2,
|
|
1258
|
+
latestStableOverall: latestStableOverall2
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
if (this.offline) {
|
|
1262
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
1263
|
+
}
|
|
1163
1264
|
let latest = null;
|
|
1164
1265
|
let versions = [];
|
|
1165
1266
|
try {
|
|
@@ -1313,7 +1414,7 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1313
1414
|
results.push(result.value);
|
|
1314
1415
|
packageNameToPath.set(result.value.name, result.value.path);
|
|
1315
1416
|
} else {
|
|
1316
|
-
const relPath =
|
|
1417
|
+
const relPath = path4.relative(rootDir, path4.dirname(pjPath));
|
|
1317
1418
|
if (cache) {
|
|
1318
1419
|
cache.addStuckPath(relPath || ".");
|
|
1319
1420
|
}
|
|
@@ -1344,8 +1445,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1344
1445
|
}
|
|
1345
1446
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
1346
1447
|
const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
|
|
1347
|
-
const absProjectPath =
|
|
1348
|
-
const projectPath =
|
|
1448
|
+
const absProjectPath = path4.dirname(packageJsonPath);
|
|
1449
|
+
const projectPath = path4.relative(rootDir, absProjectPath) || ".";
|
|
1349
1450
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
1350
1451
|
let runtimeLatest;
|
|
1351
1452
|
let runtimeMajorsBehind;
|
|
@@ -1432,7 +1533,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1432
1533
|
return {
|
|
1433
1534
|
type: "node",
|
|
1434
1535
|
path: projectPath,
|
|
1435
|
-
name: pj.name ??
|
|
1536
|
+
name: pj.name ?? path4.basename(absProjectPath),
|
|
1436
1537
|
runtime: nodeEngine,
|
|
1437
1538
|
runtimeLatest,
|
|
1438
1539
|
runtimeMajorsBehind,
|
|
@@ -1444,7 +1545,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1444
1545
|
}
|
|
1445
1546
|
|
|
1446
1547
|
// src/scanners/dotnet-scanner.ts
|
|
1447
|
-
import * as
|
|
1548
|
+
import * as path5 from "path";
|
|
1448
1549
|
import * as semver3 from "semver";
|
|
1449
1550
|
import { XMLParser } from "fast-xml-parser";
|
|
1450
1551
|
var parser = new XMLParser({
|
|
@@ -1646,7 +1747,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1646
1747
|
const parsed = parser.parse(xml);
|
|
1647
1748
|
const project = parsed?.Project;
|
|
1648
1749
|
if (!project) {
|
|
1649
|
-
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName:
|
|
1750
|
+
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path5.basename(filePath, ".csproj") };
|
|
1650
1751
|
}
|
|
1651
1752
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1652
1753
|
const targetFrameworks = [];
|
|
@@ -1683,7 +1784,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1683
1784
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1684
1785
|
packageReferences,
|
|
1685
1786
|
projectReferences,
|
|
1686
|
-
projectName:
|
|
1787
|
+
projectName: path5.basename(filePath, ".csproj")
|
|
1687
1788
|
};
|
|
1688
1789
|
}
|
|
1689
1790
|
async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
@@ -1693,12 +1794,12 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1693
1794
|
for (const slnPath of slnFiles) {
|
|
1694
1795
|
try {
|
|
1695
1796
|
const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
|
|
1696
|
-
const slnDir =
|
|
1797
|
+
const slnDir = path5.dirname(slnPath);
|
|
1697
1798
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1698
1799
|
let match;
|
|
1699
1800
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1700
1801
|
if (match[1]) {
|
|
1701
|
-
const csprojPath =
|
|
1802
|
+
const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1702
1803
|
slnCsprojPaths.add(csprojPath);
|
|
1703
1804
|
}
|
|
1704
1805
|
}
|
|
@@ -1715,7 +1816,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1715
1816
|
if (result.ok) {
|
|
1716
1817
|
results.push(result.value);
|
|
1717
1818
|
} else {
|
|
1718
|
-
const relPath =
|
|
1819
|
+
const relPath = path5.relative(rootDir, path5.dirname(csprojPath));
|
|
1719
1820
|
if (cache) {
|
|
1720
1821
|
cache.addStuckPath(relPath || ".");
|
|
1721
1822
|
}
|
|
@@ -1731,7 +1832,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1731
1832
|
async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
1732
1833
|
const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
|
|
1733
1834
|
const data = parseCsproj(xml, csprojPath);
|
|
1734
|
-
const csprojDir =
|
|
1835
|
+
const csprojDir = path5.dirname(csprojPath);
|
|
1735
1836
|
const primaryTfm = data.targetFrameworks[0];
|
|
1736
1837
|
let runtimeMajorsBehind;
|
|
1737
1838
|
let targetFramework = primaryTfm;
|
|
@@ -1809,9 +1910,9 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1809
1910
|
}
|
|
1810
1911
|
}
|
|
1811
1912
|
const projectReferences = data.projectReferences.map((refPath) => {
|
|
1812
|
-
const absRefPath =
|
|
1813
|
-
const relRefPath =
|
|
1814
|
-
const refName =
|
|
1913
|
+
const absRefPath = path5.resolve(csprojDir, refPath);
|
|
1914
|
+
const relRefPath = path5.relative(rootDir, path5.dirname(absRefPath));
|
|
1915
|
+
const refName = path5.basename(absRefPath, ".csproj");
|
|
1815
1916
|
return {
|
|
1816
1917
|
path: relRefPath || ".",
|
|
1817
1918
|
name: refName,
|
|
@@ -1832,7 +1933,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1832
1933
|
const buckets = bucketsMut;
|
|
1833
1934
|
return {
|
|
1834
1935
|
type: "dotnet",
|
|
1835
|
-
path:
|
|
1936
|
+
path: path5.relative(rootDir, csprojDir) || ".",
|
|
1836
1937
|
name: data.projectName,
|
|
1837
1938
|
targetFramework,
|
|
1838
1939
|
runtime: primaryTfm,
|
|
@@ -1847,7 +1948,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1847
1948
|
}
|
|
1848
1949
|
|
|
1849
1950
|
// src/scanners/python-scanner.ts
|
|
1850
|
-
import * as
|
|
1951
|
+
import * as path6 from "path";
|
|
1851
1952
|
import * as semver4 from "semver";
|
|
1852
1953
|
var KNOWN_PYTHON_FRAMEWORKS = {
|
|
1853
1954
|
// ── Web Frameworks ──
|
|
@@ -2088,7 +2189,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
|
|
|
2088
2189
|
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
|
|
2089
2190
|
const projectDirs = /* @__PURE__ */ new Map();
|
|
2090
2191
|
for (const f of manifestFiles) {
|
|
2091
|
-
const dir =
|
|
2192
|
+
const dir = path6.dirname(f);
|
|
2092
2193
|
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2093
2194
|
projectDirs.get(dir).push(f);
|
|
2094
2195
|
}
|
|
@@ -2101,7 +2202,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
|
|
|
2101
2202
|
if (result.ok) {
|
|
2102
2203
|
results.push(result.value);
|
|
2103
2204
|
} else {
|
|
2104
|
-
const relPath =
|
|
2205
|
+
const relPath = path6.relative(rootDir, dir);
|
|
2105
2206
|
if (cache) cache.addStuckPath(relPath || ".");
|
|
2106
2207
|
console.error(`Timeout scanning Python project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2107
2208
|
}
|
|
@@ -2117,12 +2218,12 @@ async function findPythonManifests(rootDir) {
|
|
|
2117
2218
|
return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
|
|
2118
2219
|
}
|
|
2119
2220
|
async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
|
|
2120
|
-
const relDir =
|
|
2121
|
-
let projectName =
|
|
2221
|
+
const relDir = path6.relative(rootDir, dir) || ".";
|
|
2222
|
+
let projectName = path6.basename(dir === rootDir ? rootDir : dir);
|
|
2122
2223
|
let pythonVersion;
|
|
2123
2224
|
const allDeps = /* @__PURE__ */ new Map();
|
|
2124
2225
|
for (const f of manifestFiles) {
|
|
2125
|
-
const fileName =
|
|
2226
|
+
const fileName = path6.basename(f);
|
|
2126
2227
|
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2127
2228
|
if (fileName === "pyproject.toml") {
|
|
2128
2229
|
const parsed = parsePyprojectToml(content);
|
|
@@ -2239,7 +2340,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
|
|
|
2239
2340
|
}
|
|
2240
2341
|
|
|
2241
2342
|
// src/scanners/java-scanner.ts
|
|
2242
|
-
import * as
|
|
2343
|
+
import * as path7 from "path";
|
|
2243
2344
|
import * as semver5 from "semver";
|
|
2244
2345
|
import { XMLParser as XMLParser2 } from "fast-xml-parser";
|
|
2245
2346
|
var parser2 = new XMLParser2({
|
|
@@ -2345,7 +2446,7 @@ function parsePom(xml, filePath) {
|
|
|
2345
2446
|
const project = parsed?.project;
|
|
2346
2447
|
if (!project) {
|
|
2347
2448
|
return {
|
|
2348
|
-
artifactId:
|
|
2449
|
+
artifactId: path7.basename(path7.dirname(filePath)),
|
|
2349
2450
|
dependencies: [],
|
|
2350
2451
|
modules: [],
|
|
2351
2452
|
properties: {}
|
|
@@ -2405,7 +2506,7 @@ function parsePom(xml, filePath) {
|
|
|
2405
2506
|
}
|
|
2406
2507
|
return {
|
|
2407
2508
|
groupId: project.groupId ? String(project.groupId) : parent?.groupId,
|
|
2408
|
-
artifactId: String(project.artifactId ??
|
|
2509
|
+
artifactId: String(project.artifactId ?? path7.basename(path7.dirname(filePath))),
|
|
2409
2510
|
version: project.version ? String(project.version) : parent?.version,
|
|
2410
2511
|
packaging: project.packaging ? String(project.packaging) : void 0,
|
|
2411
2512
|
javaVersion,
|
|
@@ -2420,7 +2521,7 @@ function resolveProperty(value, properties) {
|
|
|
2420
2521
|
}
|
|
2421
2522
|
function parseGradleBuild(content, filePath) {
|
|
2422
2523
|
const deps = [];
|
|
2423
|
-
const projectName =
|
|
2524
|
+
const projectName = path7.basename(path7.dirname(filePath));
|
|
2424
2525
|
let javaVersion;
|
|
2425
2526
|
const compatMatch = content.match(/(?:sourceCompatibility|targetCompatibility|javaVersion)\s*[=:]\s*['"]?(?:JavaVersion\.VERSION_)?(\d+)['"]?/);
|
|
2426
2527
|
if (compatMatch) javaVersion = compatMatch[1];
|
|
@@ -2483,7 +2584,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
|
|
|
2483
2584
|
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
|
|
2484
2585
|
const projectDirs = /* @__PURE__ */ new Map();
|
|
2485
2586
|
for (const f of manifestFiles) {
|
|
2486
|
-
const dir =
|
|
2587
|
+
const dir = path7.dirname(f);
|
|
2487
2588
|
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2488
2589
|
projectDirs.get(dir).push(f);
|
|
2489
2590
|
}
|
|
@@ -2496,7 +2597,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
|
|
|
2496
2597
|
if (result.ok) {
|
|
2497
2598
|
results.push(result.value);
|
|
2498
2599
|
} else {
|
|
2499
|
-
const relPath =
|
|
2600
|
+
const relPath = path7.relative(rootDir, dir);
|
|
2500
2601
|
if (cache) cache.addStuckPath(relPath || ".");
|
|
2501
2602
|
console.error(`Timeout scanning Java project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2502
2603
|
}
|
|
@@ -2516,13 +2617,13 @@ async function findJavaManifests(rootDir) {
|
|
|
2516
2617
|
return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
|
|
2517
2618
|
}
|
|
2518
2619
|
async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
|
|
2519
|
-
const relDir =
|
|
2520
|
-
let projectName =
|
|
2620
|
+
const relDir = path7.relative(rootDir, dir) || ".";
|
|
2621
|
+
let projectName = path7.basename(dir === rootDir ? rootDir : dir);
|
|
2521
2622
|
let javaVersion;
|
|
2522
2623
|
const allDeps = /* @__PURE__ */ new Map();
|
|
2523
2624
|
const projectReferences = [];
|
|
2524
2625
|
for (const f of manifestFiles) {
|
|
2525
|
-
const fileName =
|
|
2626
|
+
const fileName = path7.basename(f);
|
|
2526
2627
|
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2527
2628
|
if (fileName === "pom.xml") {
|
|
2528
2629
|
const pom = parsePom(content, f);
|
|
@@ -2536,7 +2637,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
|
|
|
2536
2637
|
}
|
|
2537
2638
|
for (const mod of pom.modules) {
|
|
2538
2639
|
projectReferences.push({
|
|
2539
|
-
path:
|
|
2640
|
+
path: path7.join(relDir, mod),
|
|
2540
2641
|
name: mod,
|
|
2541
2642
|
refType: "project"
|
|
2542
2643
|
});
|
|
@@ -2642,8 +2743,10 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
|
|
|
2642
2743
|
// src/scanners/nuget-cache.ts
|
|
2643
2744
|
import * as semver6 from "semver";
|
|
2644
2745
|
var NuGetCache = class {
|
|
2645
|
-
constructor(sem) {
|
|
2746
|
+
constructor(sem, manifest, offline = false) {
|
|
2646
2747
|
this.sem = sem;
|
|
2748
|
+
this.manifest = manifest;
|
|
2749
|
+
this.offline = offline;
|
|
2647
2750
|
}
|
|
2648
2751
|
meta = /* @__PURE__ */ new Map();
|
|
2649
2752
|
baseUrl = "https://api.nuget.org/v3-flatcontainer";
|
|
@@ -2651,6 +2754,23 @@ var NuGetCache = class {
|
|
|
2651
2754
|
const existing = this.meta.get(pkg2);
|
|
2652
2755
|
if (existing) return existing;
|
|
2653
2756
|
const p = this.sem.run(async () => {
|
|
2757
|
+
const manifestEntry = getManifestEntry(this.manifest, "nuget", pkg2);
|
|
2758
|
+
if (manifestEntry) {
|
|
2759
|
+
const stableVersions = (manifestEntry.versions ?? []).filter((v) => {
|
|
2760
|
+
const parsed = semver6.valid(v);
|
|
2761
|
+
return parsed && semver6.prerelease(v) === null;
|
|
2762
|
+
});
|
|
2763
|
+
const sorted = [...stableVersions].sort(semver6.rcompare);
|
|
2764
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2765
|
+
return {
|
|
2766
|
+
latest: manifestEntry.latest ?? latestStableOverall,
|
|
2767
|
+
stableVersions,
|
|
2768
|
+
latestStableOverall
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
if (this.offline) {
|
|
2772
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2773
|
+
}
|
|
2654
2774
|
try {
|
|
2655
2775
|
const url = `${this.baseUrl}/${pkg2.toLowerCase()}/index.json`;
|
|
2656
2776
|
const response = await fetch(url, {
|
|
@@ -2693,14 +2813,34 @@ function pep440ToSemver2(ver) {
|
|
|
2693
2813
|
return semver7.valid(v);
|
|
2694
2814
|
}
|
|
2695
2815
|
var PyPICache = class {
|
|
2696
|
-
constructor(sem) {
|
|
2816
|
+
constructor(sem, manifest, offline = false) {
|
|
2697
2817
|
this.sem = sem;
|
|
2818
|
+
this.manifest = manifest;
|
|
2819
|
+
this.offline = offline;
|
|
2698
2820
|
}
|
|
2699
2821
|
meta = /* @__PURE__ */ new Map();
|
|
2700
2822
|
get(pkg2) {
|
|
2701
2823
|
const existing = this.meta.get(pkg2);
|
|
2702
2824
|
if (existing) return existing;
|
|
2703
2825
|
const p = this.sem.run(async () => {
|
|
2826
|
+
const manifestEntry = getManifestEntry(this.manifest, "pypi", pkg2);
|
|
2827
|
+
if (manifestEntry) {
|
|
2828
|
+
const stableVersions = [];
|
|
2829
|
+
for (const ver of manifestEntry.versions ?? []) {
|
|
2830
|
+
const sv = pep440ToSemver2(ver);
|
|
2831
|
+
if (sv) stableVersions.push(sv);
|
|
2832
|
+
}
|
|
2833
|
+
const sorted = [...stableVersions].sort(semver7.rcompare);
|
|
2834
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2835
|
+
return {
|
|
2836
|
+
latest: manifestEntry.latest ? pep440ToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
|
|
2837
|
+
stableVersions,
|
|
2838
|
+
latestStableOverall
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
if (this.offline) {
|
|
2842
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2843
|
+
}
|
|
2704
2844
|
try {
|
|
2705
2845
|
const url = `https://pypi.org/pypi/${encodeURIComponent(pkg2)}/json`;
|
|
2706
2846
|
const response = await fetch(url, {
|
|
@@ -2747,8 +2887,10 @@ function mavenToSemver2(ver) {
|
|
|
2747
2887
|
return semver8.valid(v);
|
|
2748
2888
|
}
|
|
2749
2889
|
var MavenCache = class {
|
|
2750
|
-
constructor(sem) {
|
|
2890
|
+
constructor(sem, manifest, offline = false) {
|
|
2751
2891
|
this.sem = sem;
|
|
2892
|
+
this.manifest = manifest;
|
|
2893
|
+
this.offline = offline;
|
|
2752
2894
|
}
|
|
2753
2895
|
meta = /* @__PURE__ */ new Map();
|
|
2754
2896
|
/**
|
|
@@ -2761,6 +2903,25 @@ var MavenCache = class {
|
|
|
2761
2903
|
const existing = this.meta.get(key);
|
|
2762
2904
|
if (existing) return existing;
|
|
2763
2905
|
const p = this.sem.run(async () => {
|
|
2906
|
+
const key2 = `${groupId}:${artifactId}`;
|
|
2907
|
+
const manifestEntry = getManifestEntry(this.manifest, "maven", key2);
|
|
2908
|
+
if (manifestEntry) {
|
|
2909
|
+
const stableVersions = [];
|
|
2910
|
+
for (const ver of manifestEntry.versions ?? []) {
|
|
2911
|
+
const sv = mavenToSemver2(ver);
|
|
2912
|
+
if (sv) stableVersions.push(sv);
|
|
2913
|
+
}
|
|
2914
|
+
const sorted = [...stableVersions].sort(semver8.rcompare);
|
|
2915
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2916
|
+
return {
|
|
2917
|
+
latest: manifestEntry.latest ? mavenToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
|
|
2918
|
+
stableVersions,
|
|
2919
|
+
latestStableOverall
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
if (this.offline) {
|
|
2923
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2924
|
+
}
|
|
2764
2925
|
try {
|
|
2765
2926
|
const url = `https://search.maven.org/solrsearch/select?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22&core=gav&rows=100&wt=json`;
|
|
2766
2927
|
const response = await fetch(url, {
|
|
@@ -2795,7 +2956,7 @@ var MavenCache = class {
|
|
|
2795
2956
|
};
|
|
2796
2957
|
|
|
2797
2958
|
// src/config.ts
|
|
2798
|
-
import * as
|
|
2959
|
+
import * as path8 from "path";
|
|
2799
2960
|
import * as fs from "fs/promises";
|
|
2800
2961
|
var CONFIG_FILES = [
|
|
2801
2962
|
"vibgrate.config.ts",
|
|
@@ -2821,7 +2982,7 @@ var DEFAULT_CONFIG = {
|
|
|
2821
2982
|
async function loadConfig(rootDir) {
|
|
2822
2983
|
let config = DEFAULT_CONFIG;
|
|
2823
2984
|
for (const file of CONFIG_FILES) {
|
|
2824
|
-
const configPath =
|
|
2985
|
+
const configPath = path8.join(rootDir, file);
|
|
2825
2986
|
if (await pathExists(configPath)) {
|
|
2826
2987
|
if (file.endsWith(".json")) {
|
|
2827
2988
|
const txt = await readTextFile(configPath);
|
|
@@ -2836,7 +2997,7 @@ async function loadConfig(rootDir) {
|
|
|
2836
2997
|
}
|
|
2837
2998
|
}
|
|
2838
2999
|
}
|
|
2839
|
-
const sidecarPath =
|
|
3000
|
+
const sidecarPath = path8.join(rootDir, ".vibgrate", "auto-excludes.json");
|
|
2840
3001
|
if (await pathExists(sidecarPath)) {
|
|
2841
3002
|
try {
|
|
2842
3003
|
const txt = await readTextFile(sidecarPath);
|
|
@@ -2851,7 +3012,7 @@ async function loadConfig(rootDir) {
|
|
|
2851
3012
|
return config;
|
|
2852
3013
|
}
|
|
2853
3014
|
async function writeDefaultConfig(rootDir) {
|
|
2854
|
-
const configPath =
|
|
3015
|
+
const configPath = path8.join(rootDir, "vibgrate.config.ts");
|
|
2855
3016
|
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
2856
3017
|
|
|
2857
3018
|
const config: VibgrateConfig = {
|
|
@@ -2877,7 +3038,7 @@ export default config;
|
|
|
2877
3038
|
}
|
|
2878
3039
|
async function appendExcludePatterns(rootDir, newPatterns) {
|
|
2879
3040
|
if (newPatterns.length === 0) return false;
|
|
2880
|
-
const jsonPath =
|
|
3041
|
+
const jsonPath = path8.join(rootDir, "vibgrate.config.json");
|
|
2881
3042
|
if (await pathExists(jsonPath)) {
|
|
2882
3043
|
try {
|
|
2883
3044
|
const txt = await readTextFile(jsonPath);
|
|
@@ -2890,8 +3051,8 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2890
3051
|
} catch {
|
|
2891
3052
|
}
|
|
2892
3053
|
}
|
|
2893
|
-
const vibgrateDir =
|
|
2894
|
-
const sidecarPath =
|
|
3054
|
+
const vibgrateDir = path8.join(rootDir, ".vibgrate");
|
|
3055
|
+
const sidecarPath = path8.join(vibgrateDir, "auto-excludes.json");
|
|
2895
3056
|
let existing = [];
|
|
2896
3057
|
if (await pathExists(sidecarPath)) {
|
|
2897
3058
|
try {
|
|
@@ -2912,7 +3073,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2912
3073
|
}
|
|
2913
3074
|
|
|
2914
3075
|
// src/utils/vcs.ts
|
|
2915
|
-
import * as
|
|
3076
|
+
import * as path9 from "path";
|
|
2916
3077
|
import * as fs2 from "fs/promises";
|
|
2917
3078
|
async function detectVcs(rootDir) {
|
|
2918
3079
|
try {
|
|
@@ -2926,7 +3087,7 @@ async function detectGit(rootDir) {
|
|
|
2926
3087
|
if (!gitDir) {
|
|
2927
3088
|
return { type: "unknown" };
|
|
2928
3089
|
}
|
|
2929
|
-
const headPath =
|
|
3090
|
+
const headPath = path9.join(gitDir, "HEAD");
|
|
2930
3091
|
let headContent;
|
|
2931
3092
|
try {
|
|
2932
3093
|
headContent = (await fs2.readFile(headPath, "utf8")).trim();
|
|
@@ -2942,18 +3103,20 @@ async function detectGit(rootDir) {
|
|
|
2942
3103
|
} else if (/^[0-9a-f]{40}$/i.test(headContent)) {
|
|
2943
3104
|
sha = headContent;
|
|
2944
3105
|
}
|
|
3106
|
+
const remoteUrl = await readGitRemoteUrl(gitDir);
|
|
2945
3107
|
return {
|
|
2946
3108
|
type: "git",
|
|
2947
3109
|
sha: sha ?? void 0,
|
|
2948
3110
|
shortSha: sha ? sha.slice(0, 7) : void 0,
|
|
2949
|
-
branch: branch ?? void 0
|
|
3111
|
+
branch: branch ?? void 0,
|
|
3112
|
+
remoteUrl
|
|
2950
3113
|
};
|
|
2951
3114
|
}
|
|
2952
3115
|
async function findGitDir(startDir) {
|
|
2953
|
-
let dir =
|
|
2954
|
-
const root =
|
|
3116
|
+
let dir = path9.resolve(startDir);
|
|
3117
|
+
const root = path9.parse(dir).root;
|
|
2955
3118
|
while (dir !== root) {
|
|
2956
|
-
const gitPath =
|
|
3119
|
+
const gitPath = path9.join(dir, ".git");
|
|
2957
3120
|
try {
|
|
2958
3121
|
const stat3 = await fs2.stat(gitPath);
|
|
2959
3122
|
if (stat3.isDirectory()) {
|
|
@@ -2962,18 +3125,18 @@ async function findGitDir(startDir) {
|
|
|
2962
3125
|
if (stat3.isFile()) {
|
|
2963
3126
|
const content = (await fs2.readFile(gitPath, "utf8")).trim();
|
|
2964
3127
|
if (content.startsWith("gitdir: ")) {
|
|
2965
|
-
const resolved =
|
|
3128
|
+
const resolved = path9.resolve(dir, content.slice(8));
|
|
2966
3129
|
return resolved;
|
|
2967
3130
|
}
|
|
2968
3131
|
}
|
|
2969
3132
|
} catch {
|
|
2970
3133
|
}
|
|
2971
|
-
dir =
|
|
3134
|
+
dir = path9.dirname(dir);
|
|
2972
3135
|
}
|
|
2973
3136
|
return null;
|
|
2974
3137
|
}
|
|
2975
3138
|
async function resolveRef(gitDir, refPath) {
|
|
2976
|
-
const loosePath =
|
|
3139
|
+
const loosePath = path9.join(gitDir, refPath);
|
|
2977
3140
|
try {
|
|
2978
3141
|
const sha = (await fs2.readFile(loosePath, "utf8")).trim();
|
|
2979
3142
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
@@ -2981,7 +3144,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
2981
3144
|
}
|
|
2982
3145
|
} catch {
|
|
2983
3146
|
}
|
|
2984
|
-
const packedPath =
|
|
3147
|
+
const packedPath = path9.join(gitDir, "packed-refs");
|
|
2985
3148
|
try {
|
|
2986
3149
|
const packed = await fs2.readFile(packedPath, "utf8");
|
|
2987
3150
|
for (const line of packed.split("\n")) {
|
|
@@ -2995,6 +3158,39 @@ async function resolveRef(gitDir, refPath) {
|
|
|
2995
3158
|
}
|
|
2996
3159
|
return void 0;
|
|
2997
3160
|
}
|
|
3161
|
+
async function readGitRemoteUrl(gitDir) {
|
|
3162
|
+
const configPath = await resolveGitConfigPath(gitDir);
|
|
3163
|
+
if (!configPath) return void 0;
|
|
3164
|
+
try {
|
|
3165
|
+
const config = await fs2.readFile(configPath, "utf8");
|
|
3166
|
+
const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
|
|
3167
|
+
if (!originBlock) return void 0;
|
|
3168
|
+
const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
|
|
3169
|
+
return urlMatch?.[1]?.trim();
|
|
3170
|
+
} catch {
|
|
3171
|
+
return void 0;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
async function resolveGitConfigPath(gitDir) {
|
|
3175
|
+
const directConfig = path9.join(gitDir, "config");
|
|
3176
|
+
try {
|
|
3177
|
+
const stat3 = await fs2.stat(directConfig);
|
|
3178
|
+
if (stat3.isFile()) return directConfig;
|
|
3179
|
+
} catch {
|
|
3180
|
+
}
|
|
3181
|
+
const commonDirFile = path9.join(gitDir, "commondir");
|
|
3182
|
+
try {
|
|
3183
|
+
const commonDir = (await fs2.readFile(commonDirFile, "utf8")).trim();
|
|
3184
|
+
if (!commonDir) return void 0;
|
|
3185
|
+
const resolvedCommonDir = path9.resolve(gitDir, commonDir);
|
|
3186
|
+
const commonConfig = path9.join(resolvedCommonDir, "config");
|
|
3187
|
+
const stat3 = await fs2.stat(commonConfig);
|
|
3188
|
+
if (stat3.isFile()) return commonConfig;
|
|
3189
|
+
} catch {
|
|
3190
|
+
return void 0;
|
|
3191
|
+
}
|
|
3192
|
+
return void 0;
|
|
3193
|
+
}
|
|
2998
3194
|
|
|
2999
3195
|
// src/ui/progress.ts
|
|
3000
3196
|
import chalk4 from "chalk";
|
|
@@ -3378,11 +3574,11 @@ var ScanProgress = class {
|
|
|
3378
3574
|
|
|
3379
3575
|
// src/ui/scan-history.ts
|
|
3380
3576
|
import * as fs3 from "fs/promises";
|
|
3381
|
-
import * as
|
|
3577
|
+
import * as path10 from "path";
|
|
3382
3578
|
var HISTORY_FILENAME = "scan_history.json";
|
|
3383
3579
|
var MAX_RECORDS = 10;
|
|
3384
3580
|
async function loadScanHistory(rootDir) {
|
|
3385
|
-
const filePath =
|
|
3581
|
+
const filePath = path10.join(rootDir, ".vibgrate", HISTORY_FILENAME);
|
|
3386
3582
|
try {
|
|
3387
3583
|
const txt = await fs3.readFile(filePath, "utf8");
|
|
3388
3584
|
const data = JSON.parse(txt);
|
|
@@ -3395,8 +3591,8 @@ async function loadScanHistory(rootDir) {
|
|
|
3395
3591
|
}
|
|
3396
3592
|
}
|
|
3397
3593
|
async function saveScanHistory(rootDir, record) {
|
|
3398
|
-
const dir =
|
|
3399
|
-
const filePath =
|
|
3594
|
+
const dir = path10.join(rootDir, ".vibgrate");
|
|
3595
|
+
const filePath = path10.join(dir, HISTORY_FILENAME);
|
|
3400
3596
|
let history;
|
|
3401
3597
|
const existing = await loadScanHistory(rootDir);
|
|
3402
3598
|
if (existing) {
|
|
@@ -3460,7 +3656,7 @@ function estimateStepDurations(history, currentFileCount) {
|
|
|
3460
3656
|
}
|
|
3461
3657
|
|
|
3462
3658
|
// src/scanners/platform-matrix.ts
|
|
3463
|
-
import * as
|
|
3659
|
+
import * as path11 from "path";
|
|
3464
3660
|
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
3465
3661
|
// Image / media processing
|
|
3466
3662
|
"sharp",
|
|
@@ -3740,7 +3936,7 @@ async function scanPlatformMatrix(rootDir, cache) {
|
|
|
3740
3936
|
}
|
|
3741
3937
|
result.dockerBaseImages = [...baseImages].sort();
|
|
3742
3938
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
3743
|
-
const exists = cache ? await cache.pathExists(
|
|
3939
|
+
const exists = cache ? await cache.pathExists(path11.join(rootDir, file)) : await pathExists(path11.join(rootDir, file));
|
|
3744
3940
|
if (exists) {
|
|
3745
3941
|
result.nodeVersionFiles.push(file);
|
|
3746
3942
|
}
|
|
@@ -3817,7 +4013,7 @@ function scanDependencyRisk(projects) {
|
|
|
3817
4013
|
}
|
|
3818
4014
|
|
|
3819
4015
|
// src/scanners/dependency-graph.ts
|
|
3820
|
-
import * as
|
|
4016
|
+
import * as path12 from "path";
|
|
3821
4017
|
function parsePnpmLock(content) {
|
|
3822
4018
|
const entries = [];
|
|
3823
4019
|
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
@@ -3876,9 +4072,9 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
3876
4072
|
phantomDependencies: []
|
|
3877
4073
|
};
|
|
3878
4074
|
let entries = [];
|
|
3879
|
-
const pnpmLock =
|
|
3880
|
-
const npmLock =
|
|
3881
|
-
const yarnLock =
|
|
4075
|
+
const pnpmLock = path12.join(rootDir, "pnpm-lock.yaml");
|
|
4076
|
+
const npmLock = path12.join(rootDir, "package-lock.json");
|
|
4077
|
+
const yarnLock = path12.join(rootDir, "yarn.lock");
|
|
3882
4078
|
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
3883
4079
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3884
4080
|
if (await _pathExists(pnpmLock)) {
|
|
@@ -3925,7 +4121,7 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
3925
4121
|
for (const pjPath of pkgFiles) {
|
|
3926
4122
|
try {
|
|
3927
4123
|
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
3928
|
-
const relPath =
|
|
4124
|
+
const relPath = path12.relative(rootDir, pjPath);
|
|
3929
4125
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
3930
4126
|
const deps = pj[section];
|
|
3931
4127
|
if (!deps) continue;
|
|
@@ -4271,7 +4467,7 @@ function scanToolingInventory(projects) {
|
|
|
4271
4467
|
}
|
|
4272
4468
|
|
|
4273
4469
|
// src/scanners/build-deploy.ts
|
|
4274
|
-
import * as
|
|
4470
|
+
import * as path13 from "path";
|
|
4275
4471
|
var CI_FILES = {
|
|
4276
4472
|
".github/workflows": "github-actions",
|
|
4277
4473
|
".gitlab-ci.yml": "gitlab-ci",
|
|
@@ -4324,17 +4520,17 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4324
4520
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
4325
4521
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
4326
4522
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
4327
|
-
const fullPath =
|
|
4523
|
+
const fullPath = path13.join(rootDir, file);
|
|
4328
4524
|
if (await _pathExists(fullPath)) {
|
|
4329
4525
|
ciSystems.add(system);
|
|
4330
4526
|
}
|
|
4331
4527
|
}
|
|
4332
|
-
const ghWorkflowDir =
|
|
4528
|
+
const ghWorkflowDir = path13.join(rootDir, ".github", "workflows");
|
|
4333
4529
|
if (await _pathExists(ghWorkflowDir)) {
|
|
4334
4530
|
try {
|
|
4335
4531
|
if (cache) {
|
|
4336
4532
|
const entries = await cache.walkDir(rootDir);
|
|
4337
|
-
const ghPrefix =
|
|
4533
|
+
const ghPrefix = path13.relative(rootDir, ghWorkflowDir) + path13.sep;
|
|
4338
4534
|
result.ciWorkflowCount = entries.filter(
|
|
4339
4535
|
(e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
|
|
4340
4536
|
).length;
|
|
@@ -4385,11 +4581,11 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4385
4581
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
4386
4582
|
);
|
|
4387
4583
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
4388
|
-
if (await _pathExists(
|
|
4584
|
+
if (await _pathExists(path13.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
4389
4585
|
result.iac = [...iacSystems].sort();
|
|
4390
4586
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
4391
4587
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
4392
|
-
if (await _pathExists(
|
|
4588
|
+
if (await _pathExists(path13.join(rootDir, file))) releaseTools.add(tool);
|
|
4393
4589
|
}
|
|
4394
4590
|
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
4395
4591
|
for (const pjPath of pkgFiles) {
|
|
@@ -4414,19 +4610,19 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4414
4610
|
};
|
|
4415
4611
|
const managers = /* @__PURE__ */ new Set();
|
|
4416
4612
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
4417
|
-
if (await _pathExists(
|
|
4613
|
+
if (await _pathExists(path13.join(rootDir, file))) managers.add(manager);
|
|
4418
4614
|
}
|
|
4419
4615
|
result.packageManagers = [...managers].sort();
|
|
4420
4616
|
const monoTools = /* @__PURE__ */ new Set();
|
|
4421
4617
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
4422
|
-
if (await _pathExists(
|
|
4618
|
+
if (await _pathExists(path13.join(rootDir, file))) monoTools.add(tool);
|
|
4423
4619
|
}
|
|
4424
4620
|
result.monorepoTools = [...monoTools].sort();
|
|
4425
4621
|
return result;
|
|
4426
4622
|
}
|
|
4427
4623
|
|
|
4428
4624
|
// src/scanners/ts-modernity.ts
|
|
4429
|
-
import * as
|
|
4625
|
+
import * as path14 from "path";
|
|
4430
4626
|
async function scanTsModernity(rootDir, cache) {
|
|
4431
4627
|
const result = {
|
|
4432
4628
|
typescriptVersion: null,
|
|
@@ -4464,7 +4660,7 @@ async function scanTsModernity(rootDir, cache) {
|
|
|
4464
4660
|
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
4465
4661
|
else if (hasEsm) result.moduleType = "esm";
|
|
4466
4662
|
else if (hasCjs) result.moduleType = "cjs";
|
|
4467
|
-
let tsConfigPath =
|
|
4663
|
+
let tsConfigPath = path14.join(rootDir, "tsconfig.json");
|
|
4468
4664
|
const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
|
|
4469
4665
|
if (!tsConfigExists) {
|
|
4470
4666
|
const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
@@ -4811,7 +5007,7 @@ function scanBreakingChangeExposure(projects) {
|
|
|
4811
5007
|
|
|
4812
5008
|
// src/scanners/file-hotspots.ts
|
|
4813
5009
|
import * as fs4 from "fs/promises";
|
|
4814
|
-
import * as
|
|
5010
|
+
import * as path15 from "path";
|
|
4815
5011
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4816
5012
|
"node_modules",
|
|
4817
5013
|
".git",
|
|
@@ -4856,9 +5052,9 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4856
5052
|
const entries = await cache.walkDir(rootDir);
|
|
4857
5053
|
for (const entry of entries) {
|
|
4858
5054
|
if (!entry.isFile) continue;
|
|
4859
|
-
const ext =
|
|
5055
|
+
const ext = path15.extname(entry.name).toLowerCase();
|
|
4860
5056
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4861
|
-
const depth = entry.relPath.split(
|
|
5057
|
+
const depth = entry.relPath.split(path15.sep).length - 1;
|
|
4862
5058
|
if (depth > maxDepth) maxDepth = depth;
|
|
4863
5059
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4864
5060
|
try {
|
|
@@ -4887,15 +5083,15 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4887
5083
|
for (const e of entries) {
|
|
4888
5084
|
if (e.isDirectory) {
|
|
4889
5085
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
4890
|
-
await walk(
|
|
5086
|
+
await walk(path15.join(dir, e.name), depth + 1);
|
|
4891
5087
|
} else if (e.isFile) {
|
|
4892
|
-
const ext =
|
|
5088
|
+
const ext = path15.extname(e.name).toLowerCase();
|
|
4893
5089
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4894
5090
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4895
5091
|
try {
|
|
4896
|
-
const stat3 = await fs4.stat(
|
|
5092
|
+
const stat3 = await fs4.stat(path15.join(dir, e.name));
|
|
4897
5093
|
allFiles.push({
|
|
4898
|
-
path:
|
|
5094
|
+
path: path15.relative(rootDir, path15.join(dir, e.name)),
|
|
4899
5095
|
bytes: stat3.size
|
|
4900
5096
|
});
|
|
4901
5097
|
} catch {
|
|
@@ -4918,7 +5114,7 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4918
5114
|
}
|
|
4919
5115
|
|
|
4920
5116
|
// src/scanners/security-posture.ts
|
|
4921
|
-
import * as
|
|
5117
|
+
import * as path16 from "path";
|
|
4922
5118
|
var LOCKFILES = {
|
|
4923
5119
|
"pnpm-lock.yaml": "pnpm",
|
|
4924
5120
|
"package-lock.json": "npm",
|
|
@@ -4939,14 +5135,14 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4939
5135
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
4940
5136
|
const foundLockfiles = [];
|
|
4941
5137
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
4942
|
-
if (await _pathExists(
|
|
5138
|
+
if (await _pathExists(path16.join(rootDir, file))) {
|
|
4943
5139
|
foundLockfiles.push(type);
|
|
4944
5140
|
}
|
|
4945
5141
|
}
|
|
4946
5142
|
result.lockfilePresent = foundLockfiles.length > 0;
|
|
4947
5143
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
4948
5144
|
result.lockfileTypes = foundLockfiles.sort();
|
|
4949
|
-
const gitignorePath =
|
|
5145
|
+
const gitignorePath = path16.join(rootDir, ".gitignore");
|
|
4950
5146
|
if (await _pathExists(gitignorePath)) {
|
|
4951
5147
|
try {
|
|
4952
5148
|
const content = await _readTextFile(gitignorePath);
|
|
@@ -4961,7 +5157,7 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4961
5157
|
}
|
|
4962
5158
|
}
|
|
4963
5159
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
4964
|
-
if (await _pathExists(
|
|
5160
|
+
if (await _pathExists(path16.join(rootDir, envFile))) {
|
|
4965
5161
|
if (!result.gitignoreCoversEnv) {
|
|
4966
5162
|
result.envFilesTracked = true;
|
|
4967
5163
|
break;
|
|
@@ -4972,8 +5168,8 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4972
5168
|
}
|
|
4973
5169
|
|
|
4974
5170
|
// src/scanners/security-scanners.ts
|
|
4975
|
-
import { spawn as
|
|
4976
|
-
import * as
|
|
5171
|
+
import { spawn as spawn3 } from "child_process";
|
|
5172
|
+
import * as path17 from "path";
|
|
4977
5173
|
var TOOL_MATRIX = [
|
|
4978
5174
|
{ key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
|
|
4979
5175
|
{ key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
|
|
@@ -4986,8 +5182,8 @@ var SECRET_HEURISTICS = [
|
|
|
4986
5182
|
{ detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
4987
5183
|
{ detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
|
|
4988
5184
|
];
|
|
4989
|
-
var defaultRunner = (command, args) => new Promise((
|
|
4990
|
-
const child =
|
|
5185
|
+
var defaultRunner = (command, args) => new Promise((resolve10, reject) => {
|
|
5186
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4991
5187
|
let stdout = "";
|
|
4992
5188
|
let stderr = "";
|
|
4993
5189
|
child.stdout.on("data", (d) => {
|
|
@@ -4998,7 +5194,7 @@ var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
|
|
|
4998
5194
|
});
|
|
4999
5195
|
child.on("error", reject);
|
|
5000
5196
|
child.on("close", (code) => {
|
|
5001
|
-
|
|
5197
|
+
resolve10({ stdout, stderr, exitCode: code ?? 1 });
|
|
5002
5198
|
});
|
|
5003
5199
|
});
|
|
5004
5200
|
function compareSemver(a, b) {
|
|
@@ -5068,7 +5264,7 @@ async function detectSecretHeuristics(rootDir, cache) {
|
|
|
5068
5264
|
const findings = [];
|
|
5069
5265
|
for (const entry of entries) {
|
|
5070
5266
|
if (!entry.isFile) continue;
|
|
5071
|
-
const ext =
|
|
5267
|
+
const ext = path17.extname(entry.name).toLowerCase();
|
|
5072
5268
|
if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
|
|
5073
5269
|
const content = await cache.readTextFile(entry.absPath);
|
|
5074
5270
|
if (!content || content.length > 3e5) continue;
|
|
@@ -5091,9 +5287,9 @@ async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
|
|
|
5091
5287
|
const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
|
|
5092
5288
|
const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
|
|
5093
5289
|
const configFiles = {
|
|
5094
|
-
semgrep: await cache.pathExists(
|
|
5095
|
-
gitleaks: await cache.pathExists(
|
|
5096
|
-
trufflehog: await cache.pathExists(
|
|
5290
|
+
semgrep: await cache.pathExists(path17.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path17.join(rootDir, ".semgrep.yaml")),
|
|
5291
|
+
gitleaks: await cache.pathExists(path17.join(rootDir, ".gitleaks.toml")),
|
|
5292
|
+
trufflehog: await cache.pathExists(path17.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path17.join(rootDir, ".trufflehog.yaml"))
|
|
5097
5293
|
};
|
|
5098
5294
|
return {
|
|
5099
5295
|
semgrep,
|
|
@@ -5518,7 +5714,7 @@ function scanServiceDependencies(projects) {
|
|
|
5518
5714
|
}
|
|
5519
5715
|
|
|
5520
5716
|
// src/scanners/architecture.ts
|
|
5521
|
-
import * as
|
|
5717
|
+
import * as path18 from "path";
|
|
5522
5718
|
import * as fs5 from "fs/promises";
|
|
5523
5719
|
var ARCHETYPE_SIGNALS = [
|
|
5524
5720
|
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
@@ -5817,9 +6013,9 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5817
6013
|
const entries = await cache.walkDir(rootDir);
|
|
5818
6014
|
return entries.filter((e) => {
|
|
5819
6015
|
if (!e.isFile) return false;
|
|
5820
|
-
const name =
|
|
6016
|
+
const name = path18.basename(e.absPath);
|
|
5821
6017
|
if (name.startsWith(".") && name !== ".") return false;
|
|
5822
|
-
const ext =
|
|
6018
|
+
const ext = path18.extname(name);
|
|
5823
6019
|
return SOURCE_EXTENSIONS.has(ext);
|
|
5824
6020
|
}).map((e) => e.relPath);
|
|
5825
6021
|
}
|
|
@@ -5833,15 +6029,15 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5833
6029
|
}
|
|
5834
6030
|
for (const entry of entries) {
|
|
5835
6031
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
5836
|
-
const fullPath =
|
|
6032
|
+
const fullPath = path18.join(dir, entry.name);
|
|
5837
6033
|
if (entry.isDirectory()) {
|
|
5838
6034
|
if (!IGNORE_DIRS.has(entry.name)) {
|
|
5839
6035
|
await walk(fullPath);
|
|
5840
6036
|
}
|
|
5841
6037
|
} else if (entry.isFile()) {
|
|
5842
|
-
const ext =
|
|
6038
|
+
const ext = path18.extname(entry.name);
|
|
5843
6039
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
5844
|
-
files.push(
|
|
6040
|
+
files.push(path18.relative(rootDir, fullPath));
|
|
5845
6041
|
}
|
|
5846
6042
|
}
|
|
5847
6043
|
}
|
|
@@ -5865,7 +6061,7 @@ function classifyFile(filePath, archetype) {
|
|
|
5865
6061
|
}
|
|
5866
6062
|
}
|
|
5867
6063
|
if (!bestMatch || bestMatch.confidence < 0.7) {
|
|
5868
|
-
const baseName =
|
|
6064
|
+
const baseName = path18.basename(filePath, path18.extname(filePath));
|
|
5869
6065
|
const cleanBase = baseName.replace(/\.(test|spec)$/, "");
|
|
5870
6066
|
for (const rule of SUFFIX_RULES) {
|
|
5871
6067
|
if (cleanBase.endsWith(rule.suffix)) {
|
|
@@ -5974,7 +6170,7 @@ function generateLayerFlowMermaid(layers) {
|
|
|
5974
6170
|
return lines.join("\n");
|
|
5975
6171
|
}
|
|
5976
6172
|
async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
|
|
5977
|
-
const projectRoot =
|
|
6173
|
+
const projectRoot = path18.resolve(rootDir, project.path || ".");
|
|
5978
6174
|
const allFiles = await walkSourceFiles(projectRoot, cache);
|
|
5979
6175
|
const layerSet = /* @__PURE__ */ new Set();
|
|
5980
6176
|
for (const rel of allFiles) {
|
|
@@ -6100,7 +6296,7 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
|
|
|
6100
6296
|
}
|
|
6101
6297
|
|
|
6102
6298
|
// src/scanners/code-quality.ts
|
|
6103
|
-
import * as
|
|
6299
|
+
import * as path19 from "path";
|
|
6104
6300
|
import * as ts from "typescript";
|
|
6105
6301
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6106
6302
|
var DEFAULT_RESULT = {
|
|
@@ -6131,9 +6327,9 @@ async function scanCodeQuality(rootDir, cache) {
|
|
|
6131
6327
|
continue;
|
|
6132
6328
|
}
|
|
6133
6329
|
if (!raw.trim()) continue;
|
|
6134
|
-
const rel = normalizeModuleId(
|
|
6330
|
+
const rel = normalizeModuleId(path19.relative(rootDir, filePath));
|
|
6135
6331
|
const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
|
|
6136
|
-
const imports = collectLocalImports(source,
|
|
6332
|
+
const imports = collectLocalImports(source, path19.dirname(filePath), rootDir);
|
|
6137
6333
|
depGraph.set(rel, imports);
|
|
6138
6334
|
const fileMetrics = computeFileMetrics(source, raw);
|
|
6139
6335
|
totalFunctions += fileMetrics.functionsAnalyzed;
|
|
@@ -6167,9 +6363,9 @@ async function scanCodeQuality(rootDir, cache) {
|
|
|
6167
6363
|
async function findSourceFiles(rootDir, cache) {
|
|
6168
6364
|
if (cache) {
|
|
6169
6365
|
const entries = await cache.walkDir(rootDir);
|
|
6170
|
-
return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(
|
|
6366
|
+
return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path19.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
|
|
6171
6367
|
}
|
|
6172
|
-
const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(
|
|
6368
|
+
const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path19.extname(name).toLowerCase()));
|
|
6173
6369
|
return files;
|
|
6174
6370
|
}
|
|
6175
6371
|
function collectLocalImports(source, fileDir, rootDir) {
|
|
@@ -6190,8 +6386,8 @@ function collectLocalImports(source, fileDir, rootDir) {
|
|
|
6190
6386
|
}
|
|
6191
6387
|
function resolveLocalImport(specifier, fileDir, rootDir) {
|
|
6192
6388
|
if (!specifier.startsWith(".")) return null;
|
|
6193
|
-
const rawTarget =
|
|
6194
|
-
const normalized =
|
|
6389
|
+
const rawTarget = path19.resolve(fileDir, specifier);
|
|
6390
|
+
const normalized = path19.relative(rootDir, rawTarget).replace(/\\/g, "/");
|
|
6195
6391
|
if (!normalized || normalized.startsWith("..")) return null;
|
|
6196
6392
|
return normalizeModuleId(normalized);
|
|
6197
6393
|
}
|
|
@@ -6332,8 +6528,8 @@ function visitEach(node, cb) {
|
|
|
6332
6528
|
}
|
|
6333
6529
|
|
|
6334
6530
|
// src/scanners/owasp-category-mapping.ts
|
|
6335
|
-
import { spawn as
|
|
6336
|
-
import * as
|
|
6531
|
+
import { spawn as spawn4 } from "child_process";
|
|
6532
|
+
import * as path20 from "path";
|
|
6337
6533
|
var OWASP_CONFIG = "p/owasp-top-ten";
|
|
6338
6534
|
var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6339
6535
|
".js",
|
|
@@ -6358,8 +6554,8 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
6358
6554
|
".env"
|
|
6359
6555
|
]);
|
|
6360
6556
|
async function runSemgrep(args, cwd, stdin) {
|
|
6361
|
-
return new Promise((
|
|
6362
|
-
const child =
|
|
6557
|
+
return new Promise((resolve10, reject) => {
|
|
6558
|
+
const child = spawn4("semgrep", args, {
|
|
6363
6559
|
cwd,
|
|
6364
6560
|
shell: true,
|
|
6365
6561
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6374,7 +6570,7 @@ async function runSemgrep(args, cwd, stdin) {
|
|
|
6374
6570
|
});
|
|
6375
6571
|
child.on("error", reject);
|
|
6376
6572
|
child.on("close", (code) => {
|
|
6377
|
-
|
|
6573
|
+
resolve10({ code: code ?? 1, stdout, stderr });
|
|
6378
6574
|
});
|
|
6379
6575
|
if (stdin !== void 0) child.stdin.write(stdin);
|
|
6380
6576
|
child.stdin.end();
|
|
@@ -6412,7 +6608,7 @@ function parseFindings(results, rootDir) {
|
|
|
6412
6608
|
const metadata = r.extra?.metadata;
|
|
6413
6609
|
return {
|
|
6414
6610
|
ruleId: r.check_id ?? "unknown",
|
|
6415
|
-
path: r.path ?
|
|
6611
|
+
path: r.path ? path20.relative(rootDir, path20.resolve(rootDir, r.path)) : "",
|
|
6416
6612
|
line: r.start?.line ?? 1,
|
|
6417
6613
|
endLine: r.end?.line,
|
|
6418
6614
|
message: r.extra?.message ?? "Potential security issue",
|
|
@@ -6472,7 +6668,7 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
|
|
|
6472
6668
|
}
|
|
6473
6669
|
}
|
|
6474
6670
|
const entries = cache ? await cache.walkDir(rootDir) : [];
|
|
6475
|
-
const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(
|
|
6671
|
+
const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path20.extname(e.name).toLowerCase()));
|
|
6476
6672
|
const findings = [];
|
|
6477
6673
|
const errors = [];
|
|
6478
6674
|
let scannedFiles = 0;
|
|
@@ -6720,7 +6916,7 @@ function isUsefulString(s) {
|
|
|
6720
6916
|
}
|
|
6721
6917
|
|
|
6722
6918
|
// src/utils/tool-installer.ts
|
|
6723
|
-
import { spawn as
|
|
6919
|
+
import { spawn as spawn5 } from "child_process";
|
|
6724
6920
|
import chalk5 from "chalk";
|
|
6725
6921
|
var SECURITY_TOOLS = [
|
|
6726
6922
|
{ name: "semgrep", command: "semgrep", brew: "semgrep", winget: null, scoop: null, pip: "semgrep" },
|
|
@@ -6728,9 +6924,9 @@ var SECURITY_TOOLS = [
|
|
|
6728
6924
|
{ name: "trufflehog", command: "trufflehog", brew: "trufflehog", winget: null, scoop: "trufflehog", pip: null }
|
|
6729
6925
|
];
|
|
6730
6926
|
var IS_WIN = process.platform === "win32";
|
|
6731
|
-
function
|
|
6732
|
-
return new Promise((
|
|
6733
|
-
const child =
|
|
6927
|
+
function runCommand2(cmd, args) {
|
|
6928
|
+
return new Promise((resolve10) => {
|
|
6929
|
+
const child = spawn5(cmd, args, {
|
|
6734
6930
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6735
6931
|
shell: IS_WIN
|
|
6736
6932
|
// required for .cmd/.ps1 wrappers on Windows
|
|
@@ -6743,19 +6939,19 @@ function runCommand(cmd, args) {
|
|
|
6743
6939
|
child.stderr.on("data", (d) => {
|
|
6744
6940
|
stderr += d.toString();
|
|
6745
6941
|
});
|
|
6746
|
-
child.on("error", () =>
|
|
6747
|
-
child.on("close", (code) =>
|
|
6942
|
+
child.on("error", () => resolve10({ exitCode: 127, stdout, stderr }));
|
|
6943
|
+
child.on("close", (code) => resolve10({ exitCode: code ?? 1, stdout, stderr }));
|
|
6748
6944
|
});
|
|
6749
6945
|
}
|
|
6750
6946
|
async function commandExists(command) {
|
|
6751
6947
|
const checker = IS_WIN ? "where" : "which";
|
|
6752
|
-
const { exitCode } = await
|
|
6948
|
+
const { exitCode } = await runCommand2(checker, [command]);
|
|
6753
6949
|
return exitCode === 0;
|
|
6754
6950
|
}
|
|
6755
6951
|
async function tryInstall(tool, strategies, log) {
|
|
6756
6952
|
for (const { pm, args } of strategies) {
|
|
6757
6953
|
log(chalk5.dim(` ${pm} ${args.join(" ")}\u2026`));
|
|
6758
|
-
const { exitCode, stderr } = await
|
|
6954
|
+
const { exitCode, stderr } = await runCommand2(pm, args);
|
|
6759
6955
|
if (exitCode === 0) {
|
|
6760
6956
|
log(` ${chalk5.green("\u2714")} ${tool.name} installed via ${pm}`);
|
|
6761
6957
|
return { ok: true, pm };
|
|
@@ -6880,21 +7076,94 @@ function generateProjectRelationshipMermaid(project, projects) {
|
|
|
6880
7076
|
lines.push(...buildDefs());
|
|
6881
7077
|
return { mermaid: lines.join("\n") };
|
|
6882
7078
|
}
|
|
7079
|
+
function generateSolutionRelationshipMermaid(solution, projects) {
|
|
7080
|
+
const lines = ["flowchart TB"];
|
|
7081
|
+
const solutionNodeId = sanitizeId(solution.solutionId || solution.path || solution.name);
|
|
7082
|
+
const solutionScore = solution.drift?.score;
|
|
7083
|
+
const solutionScoreText = typeof solutionScore === "number" ? ` (${solutionScore})` : " (n/a)";
|
|
7084
|
+
lines.push(`${solutionNodeId}["${escapeLabel(`${solution.name}${solutionScoreText}`)}"]`);
|
|
7085
|
+
lines.push(`class ${solutionNodeId} ${scoreClass(solutionScore)}`);
|
|
7086
|
+
const projectByPath = new Map(projects.map((p) => [p.path, p]));
|
|
7087
|
+
for (const projectPath of solution.projectPaths) {
|
|
7088
|
+
const project = projectByPath.get(projectPath);
|
|
7089
|
+
if (!project) continue;
|
|
7090
|
+
const projectNodeId = sanitizeId(project.projectId || project.path || project.name);
|
|
7091
|
+
lines.push(`${projectNodeId}["${escapeLabel(nodeLabel(project))}"]`);
|
|
7092
|
+
lines.push(`class ${projectNodeId} ${scoreClass(project.drift?.score)}`);
|
|
7093
|
+
lines.push(`${solutionNodeId} --> ${projectNodeId}`);
|
|
7094
|
+
for (const ref of project.projectReferences ?? []) {
|
|
7095
|
+
const target = projectByPath.get(ref.path);
|
|
7096
|
+
if (!target) continue;
|
|
7097
|
+
const toId = sanitizeId(target.projectId || target.path || target.name);
|
|
7098
|
+
lines.push(`${projectNodeId} --> ${toId}`);
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
lines.push(...buildDefs());
|
|
7102
|
+
return { mermaid: lines.join("\n") };
|
|
7103
|
+
}
|
|
6883
7104
|
|
|
6884
7105
|
// src/commands/scan.ts
|
|
7106
|
+
async function discoverSolutions(rootDir, fileCache) {
|
|
7107
|
+
const solutionFiles = await fileCache.findSolutionFiles(rootDir);
|
|
7108
|
+
const parsed = [];
|
|
7109
|
+
for (const solutionFile of solutionFiles) {
|
|
7110
|
+
try {
|
|
7111
|
+
const content = await fileCache.readTextFile(solutionFile);
|
|
7112
|
+
const dir = path21.dirname(solutionFile);
|
|
7113
|
+
const relSolutionPath = path21.relative(rootDir, solutionFile).replace(/\\/g, "/");
|
|
7114
|
+
const projectPaths = /* @__PURE__ */ new Set();
|
|
7115
|
+
const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.csproj)"/g;
|
|
7116
|
+
let match;
|
|
7117
|
+
while ((match = projectRegex.exec(content)) !== null) {
|
|
7118
|
+
const projectRelative = match[2];
|
|
7119
|
+
const absProjectPath = path21.resolve(dir, projectRelative.replace(/\\/g, "/"));
|
|
7120
|
+
projectPaths.add(path21.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
|
|
7121
|
+
}
|
|
7122
|
+
const solutionName = path21.basename(solutionFile, path21.extname(solutionFile));
|
|
7123
|
+
parsed.push({
|
|
7124
|
+
path: relSolutionPath,
|
|
7125
|
+
name: solutionName,
|
|
7126
|
+
type: "dotnet-sln",
|
|
7127
|
+
projectPaths: [...projectPaths]
|
|
7128
|
+
});
|
|
7129
|
+
} catch {
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
return parsed;
|
|
7133
|
+
}
|
|
6885
7134
|
async function runScan(rootDir, opts) {
|
|
6886
7135
|
const scanStart = Date.now();
|
|
6887
7136
|
const config = await loadConfig(rootDir);
|
|
6888
7137
|
const sem = new Semaphore(opts.concurrency);
|
|
6889
|
-
const
|
|
6890
|
-
const
|
|
6891
|
-
const
|
|
6892
|
-
const
|
|
7138
|
+
const packageManifest = opts.packageManifest ? await loadPackageVersionManifest(opts.packageManifest) : void 0;
|
|
7139
|
+
const offlineMode = opts.offline === true;
|
|
7140
|
+
const maxPrivacyMode = opts.maxPrivacy === true;
|
|
7141
|
+
const npmCache = new NpmCache(rootDir, sem, packageManifest, offlineMode);
|
|
7142
|
+
const nugetCache = new NuGetCache(sem, packageManifest, offlineMode);
|
|
7143
|
+
const pypiCache = new PyPICache(sem, packageManifest, offlineMode);
|
|
7144
|
+
const mavenCache = new MavenCache(sem, packageManifest, offlineMode);
|
|
6893
7145
|
const fileCache = new FileCache();
|
|
6894
7146
|
const excludePatterns = config.exclude ?? [];
|
|
6895
7147
|
fileCache.setExcludePatterns(excludePatterns);
|
|
6896
7148
|
fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
|
|
6897
7149
|
const scanners = config.scanners;
|
|
7150
|
+
const scannerPolicy = {
|
|
7151
|
+
platformMatrix: !maxPrivacyMode,
|
|
7152
|
+
toolingInventory: true,
|
|
7153
|
+
serviceDependencies: !maxPrivacyMode,
|
|
7154
|
+
breakingChangeExposure: !maxPrivacyMode,
|
|
7155
|
+
securityPosture: true,
|
|
7156
|
+
securityScanners: !maxPrivacyMode,
|
|
7157
|
+
buildDeploy: !maxPrivacyMode,
|
|
7158
|
+
tsModernity: !maxPrivacyMode,
|
|
7159
|
+
fileHotspots: !maxPrivacyMode,
|
|
7160
|
+
dependencyGraph: true,
|
|
7161
|
+
dependencyRisk: true,
|
|
7162
|
+
architecture: !maxPrivacyMode,
|
|
7163
|
+
codeQuality: !maxPrivacyMode,
|
|
7164
|
+
owaspCategoryMapping: !maxPrivacyMode,
|
|
7165
|
+
uiPurpose: !maxPrivacyMode
|
|
7166
|
+
};
|
|
6898
7167
|
let filesScanned = 0;
|
|
6899
7168
|
const progress = new ScanProgress(rootDir);
|
|
6900
7169
|
const steps = [
|
|
@@ -6907,28 +7176,28 @@ async function runScan(rootDir, opts) {
|
|
|
6907
7176
|
{ id: "python", label: "Scanning Python projects", weight: 3 },
|
|
6908
7177
|
{ id: "java", label: "Scanning Java projects", weight: 3 },
|
|
6909
7178
|
...scanners !== false ? [
|
|
6910
|
-
...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
6911
|
-
...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
6912
|
-
...scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
|
|
6913
|
-
...scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
|
|
6914
|
-
...scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
|
|
6915
|
-
...scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
|
|
6916
|
-
...scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
|
|
6917
|
-
...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
6918
|
-
...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
6919
|
-
...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
6920
|
-
...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
6921
|
-
...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
|
|
6922
|
-
...scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
|
|
6923
|
-
...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
|
|
6924
|
-
|
|
7179
|
+
...scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
7180
|
+
...scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
7181
|
+
...scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
|
|
7182
|
+
...scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
|
|
7183
|
+
...scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
|
|
7184
|
+
...scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
|
|
7185
|
+
...scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
|
|
7186
|
+
...scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
7187
|
+
...scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
7188
|
+
...scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
7189
|
+
...scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
7190
|
+
...scannerPolicy.architecture && scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
|
|
7191
|
+
...scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
|
|
7192
|
+
...scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
|
|
7193
|
+
...!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
|
|
6925
7194
|
] : [],
|
|
6926
7195
|
{ id: "drift", label: "Computing drift score" },
|
|
6927
7196
|
{ id: "findings", label: "Generating findings" }
|
|
6928
7197
|
];
|
|
6929
7198
|
progress.setSteps(steps);
|
|
6930
7199
|
progress.completeStep("config", "loaded");
|
|
6931
|
-
const registryOk = await checkRegistryAccess(rootDir);
|
|
7200
|
+
const registryOk = offlineMode ? true : await checkRegistryAccess(rootDir);
|
|
6932
7201
|
if (!registryOk) {
|
|
6933
7202
|
progress.finish();
|
|
6934
7203
|
const msg = [
|
|
@@ -7010,14 +7279,45 @@ async function runScan(rootDir, opts) {
|
|
|
7010
7279
|
project.drift = computeDriftScore([project]);
|
|
7011
7280
|
project.projectId = computeProjectId(project.path, project.name, workspaceId);
|
|
7012
7281
|
}
|
|
7282
|
+
const solutionsManifestPath = path21.join(rootDir, ".vibgrate", "solutions.json");
|
|
7283
|
+
const persistedSolutionIds = /* @__PURE__ */ new Map();
|
|
7284
|
+
if (await pathExists(solutionsManifestPath)) {
|
|
7285
|
+
try {
|
|
7286
|
+
const persisted = await readJsonFile(solutionsManifestPath);
|
|
7287
|
+
for (const solution of persisted.solutions ?? []) {
|
|
7288
|
+
if (solution.path && solution.solutionId) persistedSolutionIds.set(solution.path, solution.solutionId);
|
|
7289
|
+
}
|
|
7290
|
+
} catch {
|
|
7291
|
+
}
|
|
7292
|
+
}
|
|
7293
|
+
const discoveredSolutions = await discoverSolutions(rootDir, fileCache);
|
|
7294
|
+
const solutions = discoveredSolutions.map((solution) => ({
|
|
7295
|
+
solutionId: persistedSolutionIds.get(solution.path) ?? computeSolutionId(solution.path, solution.name, workspaceId),
|
|
7296
|
+
path: solution.path,
|
|
7297
|
+
name: solution.name,
|
|
7298
|
+
type: solution.type,
|
|
7299
|
+
projectPaths: solution.projectPaths
|
|
7300
|
+
}));
|
|
7301
|
+
const projectsByPath = new Map(allProjects.map((project) => [project.path, project]));
|
|
7302
|
+
for (const solution of solutions) {
|
|
7303
|
+
const includedProjects = solution.projectPaths.map((projectPath) => projectsByPath.get(projectPath)).filter((project) => Boolean(project));
|
|
7304
|
+
solution.drift = includedProjects.length > 0 ? computeDriftScore(includedProjects) : void 0;
|
|
7305
|
+
for (const project of includedProjects) {
|
|
7306
|
+
project.solutionId = solution.solutionId;
|
|
7307
|
+
project.solutionName = solution.name;
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7013
7310
|
for (const project of allProjects) {
|
|
7014
7311
|
project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
|
|
7015
7312
|
}
|
|
7313
|
+
for (const solution of solutions) {
|
|
7314
|
+
solution.relationshipDiagram = generateSolutionRelationshipMermaid(solution, allProjects);
|
|
7315
|
+
}
|
|
7016
7316
|
const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
|
|
7017
7317
|
const extended = {};
|
|
7018
7318
|
if (scanners !== false) {
|
|
7019
7319
|
const scannerTasks = [];
|
|
7020
|
-
if (scanners?.platformMatrix?.enabled !== false) {
|
|
7320
|
+
if (scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false) {
|
|
7021
7321
|
progress.startStep("platform");
|
|
7022
7322
|
scannerTasks.push(
|
|
7023
7323
|
scanPlatformMatrix(rootDir, fileCache).then((result) => {
|
|
@@ -7031,7 +7331,7 @@ async function runScan(rootDir, opts) {
|
|
|
7031
7331
|
})
|
|
7032
7332
|
);
|
|
7033
7333
|
}
|
|
7034
|
-
if (scanners?.toolingInventory?.enabled !== false) {
|
|
7334
|
+
if (scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false) {
|
|
7035
7335
|
progress.startStep("tooling");
|
|
7036
7336
|
scannerTasks.push(
|
|
7037
7337
|
Promise.resolve().then(() => {
|
|
@@ -7041,7 +7341,7 @@ async function runScan(rootDir, opts) {
|
|
|
7041
7341
|
})
|
|
7042
7342
|
);
|
|
7043
7343
|
}
|
|
7044
|
-
if (scanners?.serviceDependencies?.enabled !== false) {
|
|
7344
|
+
if (scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false) {
|
|
7045
7345
|
progress.startStep("services");
|
|
7046
7346
|
scannerTasks.push(
|
|
7047
7347
|
Promise.resolve().then(() => {
|
|
@@ -7051,7 +7351,7 @@ async function runScan(rootDir, opts) {
|
|
|
7051
7351
|
})
|
|
7052
7352
|
);
|
|
7053
7353
|
}
|
|
7054
|
-
if (scanners?.breakingChangeExposure?.enabled !== false) {
|
|
7354
|
+
if (scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false) {
|
|
7055
7355
|
progress.startStep("breaking");
|
|
7056
7356
|
scannerTasks.push(
|
|
7057
7357
|
Promise.resolve().then(() => {
|
|
@@ -7066,7 +7366,7 @@ async function runScan(rootDir, opts) {
|
|
|
7066
7366
|
})
|
|
7067
7367
|
);
|
|
7068
7368
|
}
|
|
7069
|
-
if (scanners?.securityPosture?.enabled !== false) {
|
|
7369
|
+
if (scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false) {
|
|
7070
7370
|
progress.startStep("security");
|
|
7071
7371
|
scannerTasks.push(
|
|
7072
7372
|
scanSecurityPosture(rootDir, fileCache).then((result) => {
|
|
@@ -7076,7 +7376,7 @@ async function runScan(rootDir, opts) {
|
|
|
7076
7376
|
})
|
|
7077
7377
|
);
|
|
7078
7378
|
}
|
|
7079
|
-
if (scanners?.securityScanners?.enabled !== false) {
|
|
7379
|
+
if (scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false) {
|
|
7080
7380
|
if (opts.installTools) {
|
|
7081
7381
|
const installResult = await installMissingTools();
|
|
7082
7382
|
if (installResult.installed.length > 0) {
|
|
@@ -7099,7 +7399,7 @@ async function runScan(rootDir, opts) {
|
|
|
7099
7399
|
})
|
|
7100
7400
|
);
|
|
7101
7401
|
}
|
|
7102
|
-
if (scanners?.buildDeploy?.enabled !== false) {
|
|
7402
|
+
if (scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false) {
|
|
7103
7403
|
progress.startStep("build");
|
|
7104
7404
|
scannerTasks.push(
|
|
7105
7405
|
scanBuildDeploy(rootDir, fileCache).then((result) => {
|
|
@@ -7111,7 +7411,7 @@ async function runScan(rootDir, opts) {
|
|
|
7111
7411
|
})
|
|
7112
7412
|
);
|
|
7113
7413
|
}
|
|
7114
|
-
if (scanners?.tsModernity?.enabled !== false) {
|
|
7414
|
+
if (scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false) {
|
|
7115
7415
|
progress.startStep("ts");
|
|
7116
7416
|
scannerTasks.push(
|
|
7117
7417
|
scanTsModernity(rootDir, fileCache).then((result) => {
|
|
@@ -7124,7 +7424,7 @@ async function runScan(rootDir, opts) {
|
|
|
7124
7424
|
})
|
|
7125
7425
|
);
|
|
7126
7426
|
}
|
|
7127
|
-
if (scanners?.fileHotspots?.enabled !== false) {
|
|
7427
|
+
if (scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false) {
|
|
7128
7428
|
progress.startStep("hotspots");
|
|
7129
7429
|
scannerTasks.push(
|
|
7130
7430
|
scanFileHotspots(rootDir, fileCache).then((result) => {
|
|
@@ -7133,7 +7433,7 @@ async function runScan(rootDir, opts) {
|
|
|
7133
7433
|
})
|
|
7134
7434
|
);
|
|
7135
7435
|
}
|
|
7136
|
-
if (scanners?.dependencyGraph?.enabled !== false) {
|
|
7436
|
+
if (scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false) {
|
|
7137
7437
|
progress.startStep("depgraph");
|
|
7138
7438
|
scannerTasks.push(
|
|
7139
7439
|
scanDependencyGraph(rootDir, fileCache).then((result) => {
|
|
@@ -7143,7 +7443,7 @@ async function runScan(rootDir, opts) {
|
|
|
7143
7443
|
})
|
|
7144
7444
|
);
|
|
7145
7445
|
}
|
|
7146
|
-
if (scanners?.codeQuality?.enabled !== false) {
|
|
7446
|
+
if (scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false) {
|
|
7147
7447
|
progress.startStep("codequality");
|
|
7148
7448
|
scannerTasks.push(
|
|
7149
7449
|
scanCodeQuality(rootDir, fileCache).then((result) => {
|
|
@@ -7156,7 +7456,7 @@ async function runScan(rootDir, opts) {
|
|
|
7156
7456
|
})
|
|
7157
7457
|
);
|
|
7158
7458
|
}
|
|
7159
|
-
if (scanners?.dependencyRisk?.enabled !== false) {
|
|
7459
|
+
if (scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false) {
|
|
7160
7460
|
progress.startStep("deprisk");
|
|
7161
7461
|
scannerTasks.push(
|
|
7162
7462
|
Promise.resolve().then(() => {
|
|
@@ -7170,7 +7470,7 @@ async function runScan(rootDir, opts) {
|
|
|
7170
7470
|
);
|
|
7171
7471
|
}
|
|
7172
7472
|
await Promise.all(scannerTasks);
|
|
7173
|
-
if (scanners?.owaspCategoryMapping?.enabled !== false) {
|
|
7473
|
+
if (scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false) {
|
|
7174
7474
|
progress.startStep("owasp");
|
|
7175
7475
|
extended.owaspCategoryMapping = await scanOwaspCategoryMapping(
|
|
7176
7476
|
rootDir,
|
|
@@ -7189,14 +7489,14 @@ async function runScan(rootDir, opts) {
|
|
|
7189
7489
|
);
|
|
7190
7490
|
}
|
|
7191
7491
|
}
|
|
7192
|
-
if (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) {
|
|
7492
|
+
if (!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true)) {
|
|
7193
7493
|
progress.startStep("uipurpose");
|
|
7194
7494
|
extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
|
|
7195
7495
|
const up = extended.uiPurpose;
|
|
7196
7496
|
const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
|
|
7197
7497
|
progress.completeStep("uipurpose", summary, up.topEvidence.length);
|
|
7198
7498
|
}
|
|
7199
|
-
if (scanners?.architecture?.enabled !== false) {
|
|
7499
|
+
if (scannerPolicy.architecture && scanners?.architecture?.enabled !== false) {
|
|
7200
7500
|
progress.startStep("architecture");
|
|
7201
7501
|
extended.architecture = await scanArchitecture(
|
|
7202
7502
|
rootDir,
|
|
@@ -7284,13 +7584,16 @@ async function runScan(rootDir, opts) {
|
|
|
7284
7584
|
if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
|
|
7285
7585
|
if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
|
|
7286
7586
|
const durationMs = Date.now() - scanStart;
|
|
7587
|
+
const repository = await buildRepositoryInfo(rootDir, vcs.remoteUrl, extended.buildDeploy?.ci);
|
|
7287
7588
|
const artifact = {
|
|
7288
7589
|
schemaVersion: "1.0",
|
|
7289
7590
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7290
7591
|
vibgrateVersion: VERSION,
|
|
7291
|
-
rootPath:
|
|
7592
|
+
rootPath: path21.basename(rootDir),
|
|
7292
7593
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
7594
|
+
repository,
|
|
7293
7595
|
projects: allProjects,
|
|
7596
|
+
...solutions.length > 0 ? { solutions } : {},
|
|
7294
7597
|
drift,
|
|
7295
7598
|
findings,
|
|
7296
7599
|
...Object.keys(extended).length > 0 ? { extended } : {},
|
|
@@ -7300,7 +7603,7 @@ async function runScan(rootDir, opts) {
|
|
|
7300
7603
|
relationshipDiagram
|
|
7301
7604
|
};
|
|
7302
7605
|
if (opts.baseline) {
|
|
7303
|
-
const baselinePath =
|
|
7606
|
+
const baselinePath = path21.resolve(opts.baseline);
|
|
7304
7607
|
if (await pathExists(baselinePath)) {
|
|
7305
7608
|
try {
|
|
7306
7609
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -7311,9 +7614,21 @@ async function runScan(rootDir, opts) {
|
|
|
7311
7614
|
}
|
|
7312
7615
|
}
|
|
7313
7616
|
}
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7617
|
+
if (!opts.noLocalArtifacts && !maxPrivacyMode) {
|
|
7618
|
+
const vibgrateDir = path21.join(rootDir, ".vibgrate");
|
|
7619
|
+
await ensureDir(vibgrateDir);
|
|
7620
|
+
await writeJsonFile(path21.join(vibgrateDir, "scan_result.json"), artifact);
|
|
7621
|
+
await writeJsonFile(path21.join(vibgrateDir, "solutions.json"), {
|
|
7622
|
+
scannedAt: artifact.timestamp,
|
|
7623
|
+
solutions: solutions.map((solution) => ({
|
|
7624
|
+
solutionId: solution.solutionId,
|
|
7625
|
+
name: solution.name,
|
|
7626
|
+
path: solution.path,
|
|
7627
|
+
type: solution.type,
|
|
7628
|
+
projectPaths: solution.projectPaths
|
|
7629
|
+
}))
|
|
7630
|
+
});
|
|
7631
|
+
}
|
|
7317
7632
|
await saveScanHistory(rootDir, {
|
|
7318
7633
|
timestamp: artifact.timestamp,
|
|
7319
7634
|
totalDurationMs: durationMs,
|
|
@@ -7321,29 +7636,33 @@ async function runScan(rootDir, opts) {
|
|
|
7321
7636
|
totalDirs: treeCount.totalDirs,
|
|
7322
7637
|
steps: progress.getStepTimings()
|
|
7323
7638
|
});
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7639
|
+
if (!opts.noLocalArtifacts && !maxPrivacyMode) {
|
|
7640
|
+
for (const project of allProjects) {
|
|
7641
|
+
if (project.drift && project.path) {
|
|
7642
|
+
const projectDir = path21.resolve(rootDir, project.path);
|
|
7643
|
+
const projectVibgrateDir = path21.join(projectDir, ".vibgrate");
|
|
7644
|
+
await ensureDir(projectVibgrateDir);
|
|
7645
|
+
await writeJsonFile(path21.join(projectVibgrateDir, "project_score.json"), {
|
|
7646
|
+
projectId: project.projectId,
|
|
7647
|
+
name: project.name,
|
|
7648
|
+
type: project.type,
|
|
7649
|
+
path: project.path,
|
|
7650
|
+
score: project.drift.score,
|
|
7651
|
+
riskLevel: project.drift.riskLevel,
|
|
7652
|
+
components: project.drift.components,
|
|
7653
|
+
measured: project.drift.measured,
|
|
7654
|
+
scannedAt: artifact.timestamp,
|
|
7655
|
+
vibgrateVersion: VERSION,
|
|
7656
|
+
solutionId: project.solutionId,
|
|
7657
|
+
solutionName: project.solutionName
|
|
7658
|
+
});
|
|
7659
|
+
}
|
|
7341
7660
|
}
|
|
7342
7661
|
}
|
|
7343
7662
|
if (opts.format === "json") {
|
|
7344
7663
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
7345
7664
|
if (opts.out) {
|
|
7346
|
-
await writeTextFile(
|
|
7665
|
+
await writeTextFile(path21.resolve(opts.out), jsonStr);
|
|
7347
7666
|
console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
7348
7667
|
} else {
|
|
7349
7668
|
console.log(jsonStr);
|
|
@@ -7352,7 +7671,7 @@ async function runScan(rootDir, opts) {
|
|
|
7352
7671
|
const sarif = formatSarif(artifact);
|
|
7353
7672
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
7354
7673
|
if (opts.out) {
|
|
7355
|
-
await writeTextFile(
|
|
7674
|
+
await writeTextFile(path21.resolve(opts.out), sarifStr);
|
|
7356
7675
|
console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
7357
7676
|
} else {
|
|
7358
7677
|
console.log(sarifStr);
|
|
@@ -7361,11 +7680,34 @@ async function runScan(rootDir, opts) {
|
|
|
7361
7680
|
const text = formatText(artifact);
|
|
7362
7681
|
console.log(text);
|
|
7363
7682
|
if (opts.out) {
|
|
7364
|
-
await writeTextFile(
|
|
7683
|
+
await writeTextFile(path21.resolve(opts.out), text);
|
|
7365
7684
|
}
|
|
7366
7685
|
}
|
|
7367
7686
|
return artifact;
|
|
7368
7687
|
}
|
|
7688
|
+
async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
|
|
7689
|
+
const packageJsonPath = path21.join(rootDir, "package.json");
|
|
7690
|
+
let name = path21.basename(rootDir);
|
|
7691
|
+
let version;
|
|
7692
|
+
if (await pathExists(packageJsonPath)) {
|
|
7693
|
+
try {
|
|
7694
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
7695
|
+
if (typeof packageJson.name === "string" && packageJson.name.trim()) {
|
|
7696
|
+
name = packageJson.name.trim();
|
|
7697
|
+
}
|
|
7698
|
+
if (typeof packageJson.version === "string" && packageJson.version.trim()) {
|
|
7699
|
+
version = packageJson.version.trim();
|
|
7700
|
+
}
|
|
7701
|
+
} catch {
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
return {
|
|
7705
|
+
name,
|
|
7706
|
+
...version ? { version } : {},
|
|
7707
|
+
...ciSystems && ciSystems.length > 0 ? { pipeline: ciSystems.join(",") } : {},
|
|
7708
|
+
...remoteUrl ? { remoteUrl } : {}
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7369
7711
|
async function autoPush(artifact, rootDir, opts) {
|
|
7370
7712
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
7371
7713
|
if (!dsn) {
|
|
@@ -7436,8 +7778,8 @@ function parseNonNegativeNumber(value, label) {
|
|
|
7436
7778
|
}
|
|
7437
7779
|
return parsed;
|
|
7438
7780
|
}
|
|
7439
|
-
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
|
|
7440
|
-
const rootDir =
|
|
7781
|
+
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
|
|
7782
|
+
const rootDir = path21.resolve(targetPath);
|
|
7441
7783
|
if (!await pathExists(rootDir)) {
|
|
7442
7784
|
console.error(chalk6.red(`Path does not exist: ${rootDir}`));
|
|
7443
7785
|
process.exit(1);
|
|
@@ -7455,6 +7797,10 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
7455
7797
|
strict: opts.strict,
|
|
7456
7798
|
installTools: opts.installTools,
|
|
7457
7799
|
uiPurpose: opts.uiPurpose,
|
|
7800
|
+
noLocalArtifacts: opts.noLocalArtifacts,
|
|
7801
|
+
maxPrivacy: opts.maxPrivacy,
|
|
7802
|
+
offline: opts.offline,
|
|
7803
|
+
packageManifest: opts.packageManifest,
|
|
7458
7804
|
driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
|
|
7459
7805
|
driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
|
|
7460
7806
|
};
|
|
@@ -7495,7 +7841,7 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
|
|
|
7495
7841
|
}
|
|
7496
7842
|
}
|
|
7497
7843
|
const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
|
|
7498
|
-
if (opts.push || hasDsn) {
|
|
7844
|
+
if (!scanOpts.offline && (opts.push || hasDsn)) {
|
|
7499
7845
|
await autoPush(artifact, rootDir, scanOpts);
|
|
7500
7846
|
}
|
|
7501
7847
|
});
|