@vibgrate/cli 1.0.46 → 1.0.48
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-37IN66KS.js} +2 -2
- package/dist/{chunk-HEILEAVO.js → chunk-EOULHF5E.js} +715 -205
- package/dist/{chunk-SKROLJET.js → chunk-XPDOGGRY.js} +1 -1
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +38 -1
- 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";
|
|
@@ -320,8 +324,25 @@ function formatText(artifact) {
|
|
|
320
324
|
lines.push(chalk.bold.cyan("\u2551 Project Relationship Diagram \u2551"));
|
|
321
325
|
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"));
|
|
322
326
|
lines.push("");
|
|
323
|
-
lines.push(chalk.bold(" Mermaid"));
|
|
324
|
-
lines.push(
|
|
327
|
+
lines.push(chalk.bold(" Mermaid") + chalk.dim(" (copy into https://mermaid.live or a ```mermaid code block)"));
|
|
328
|
+
lines.push("");
|
|
329
|
+
lines.push(chalk.dim(" ```mermaid"));
|
|
330
|
+
for (const mLine of artifact.relationshipDiagram.mermaid.split("\n")) {
|
|
331
|
+
lines.push(chalk.dim(` ${mLine}`));
|
|
332
|
+
}
|
|
333
|
+
lines.push(chalk.dim(" ```"));
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
337
|
+
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"));
|
|
338
|
+
lines.push(chalk.bold.cyan("\u2551 Solution Drift Summary \u2551"));
|
|
339
|
+
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"));
|
|
340
|
+
lines.push("");
|
|
341
|
+
for (const solution of artifact.solutions) {
|
|
342
|
+
const solScore = solution.drift?.score;
|
|
343
|
+
const color = typeof solScore === "number" ? solScore >= 70 ? chalk.green : solScore >= 40 ? chalk.yellow : chalk.red : chalk.dim;
|
|
344
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk.dim("n/a")}`);
|
|
345
|
+
}
|
|
325
346
|
lines.push("");
|
|
326
347
|
}
|
|
327
348
|
const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
|
|
@@ -1084,19 +1105,19 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1084
1105
|
});
|
|
1085
1106
|
|
|
1086
1107
|
// src/commands/scan.ts
|
|
1087
|
-
import * as
|
|
1108
|
+
import * as path22 from "path";
|
|
1088
1109
|
import { Command as Command3 } from "commander";
|
|
1089
1110
|
import chalk6 from "chalk";
|
|
1090
1111
|
|
|
1091
1112
|
// src/scanners/node-scanner.ts
|
|
1092
|
-
import * as
|
|
1113
|
+
import * as path4 from "path";
|
|
1093
1114
|
import * as semver2 from "semver";
|
|
1094
1115
|
|
|
1095
1116
|
// src/utils/timeout.ts
|
|
1096
1117
|
async function withTimeout(promise, ms) {
|
|
1097
1118
|
let timer;
|
|
1098
|
-
const timeout = new Promise((
|
|
1099
|
-
timer = setTimeout(() =>
|
|
1119
|
+
const timeout = new Promise((resolve10) => {
|
|
1120
|
+
timer = setTimeout(() => resolve10({ ok: false }), ms);
|
|
1100
1121
|
});
|
|
1101
1122
|
try {
|
|
1102
1123
|
const result = await Promise.race([
|
|
@@ -1110,8 +1131,78 @@ async function withTimeout(promise, ms) {
|
|
|
1110
1131
|
}
|
|
1111
1132
|
|
|
1112
1133
|
// src/scanners/npm-cache.ts
|
|
1113
|
-
import { spawn } from "child_process";
|
|
1134
|
+
import { spawn as spawn2 } from "child_process";
|
|
1114
1135
|
import * as semver from "semver";
|
|
1136
|
+
|
|
1137
|
+
// src/package-version-manifest.ts
|
|
1138
|
+
import { mkdtemp, readFile, rm } from "fs/promises";
|
|
1139
|
+
import * as path3 from "path";
|
|
1140
|
+
import * as os from "os";
|
|
1141
|
+
import { spawn } from "child_process";
|
|
1142
|
+
function runCommand(cmd, args) {
|
|
1143
|
+
return new Promise((resolve10, reject) => {
|
|
1144
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1145
|
+
let out = "";
|
|
1146
|
+
let err = "";
|
|
1147
|
+
child.stdout.on("data", (d) => out += String(d));
|
|
1148
|
+
child.stderr.on("data", (d) => err += String(d));
|
|
1149
|
+
child.on("error", reject);
|
|
1150
|
+
child.on("close", (code) => {
|
|
1151
|
+
if (code !== 0) {
|
|
1152
|
+
reject(new Error(`${cmd} ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
resolve10(out);
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
async function parseManifestText(text, source) {
|
|
1160
|
+
try {
|
|
1161
|
+
return JSON.parse(text);
|
|
1162
|
+
} catch {
|
|
1163
|
+
throw new Error(`Invalid JSON in package version manifest: ${source}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
async function loadManifestFromZip(zipPath) {
|
|
1167
|
+
const tmpDir = await mkdtemp(path3.join(os.tmpdir(), "vibgrate-manifest-"));
|
|
1168
|
+
try {
|
|
1169
|
+
await runCommand("unzip", ["-qq", zipPath, "-d", tmpDir]);
|
|
1170
|
+
const candidates = [
|
|
1171
|
+
path3.join(tmpDir, "package-versions.json"),
|
|
1172
|
+
path3.join(tmpDir, "manifest.json"),
|
|
1173
|
+
path3.join(tmpDir, "index.json")
|
|
1174
|
+
];
|
|
1175
|
+
for (const candidate of candidates) {
|
|
1176
|
+
try {
|
|
1177
|
+
const text = await readFile(candidate, "utf8");
|
|
1178
|
+
return await parseManifestText(text, candidate);
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
throw new Error("Zip must contain package-versions.json, manifest.json, or index.json");
|
|
1183
|
+
} finally {
|
|
1184
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function loadPackageVersionManifest(filePath) {
|
|
1188
|
+
const resolved = path3.resolve(filePath);
|
|
1189
|
+
if (resolved.toLowerCase().endsWith(".zip")) {
|
|
1190
|
+
return loadManifestFromZip(resolved);
|
|
1191
|
+
}
|
|
1192
|
+
const text = await readFile(resolved, "utf8");
|
|
1193
|
+
return parseManifestText(text, resolved);
|
|
1194
|
+
}
|
|
1195
|
+
function getManifestEntry(manifest, ecosystem, packageName) {
|
|
1196
|
+
if (!manifest) return void 0;
|
|
1197
|
+
const table = manifest[ecosystem];
|
|
1198
|
+
if (!table) return void 0;
|
|
1199
|
+
if (ecosystem === "nuget") {
|
|
1200
|
+
return table[packageName.toLowerCase()] ?? table[packageName];
|
|
1201
|
+
}
|
|
1202
|
+
return table[packageName];
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/scanners/npm-cache.ts
|
|
1115
1206
|
function stableOnly(versions) {
|
|
1116
1207
|
return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
|
|
1117
1208
|
}
|
|
@@ -1121,8 +1212,8 @@ function maxStable(versions) {
|
|
|
1121
1212
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
1122
1213
|
}
|
|
1123
1214
|
async function npmViewJson(args, cwd) {
|
|
1124
|
-
return new Promise((
|
|
1125
|
-
const child =
|
|
1215
|
+
return new Promise((resolve10, reject) => {
|
|
1216
|
+
const child = spawn2("npm", ["view", ...args, "--json"], {
|
|
1126
1217
|
cwd,
|
|
1127
1218
|
shell: true,
|
|
1128
1219
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1139,27 +1230,42 @@ async function npmViewJson(args, cwd) {
|
|
|
1139
1230
|
}
|
|
1140
1231
|
const trimmed = out.trim();
|
|
1141
1232
|
if (!trimmed) {
|
|
1142
|
-
|
|
1233
|
+
resolve10(null);
|
|
1143
1234
|
return;
|
|
1144
1235
|
}
|
|
1145
1236
|
try {
|
|
1146
|
-
|
|
1237
|
+
resolve10(JSON.parse(trimmed));
|
|
1147
1238
|
} catch {
|
|
1148
|
-
|
|
1239
|
+
resolve10(trimmed.replace(/^"|"$/g, ""));
|
|
1149
1240
|
}
|
|
1150
1241
|
});
|
|
1151
1242
|
});
|
|
1152
1243
|
}
|
|
1153
1244
|
var NpmCache = class {
|
|
1154
|
-
constructor(cwd, sem) {
|
|
1245
|
+
constructor(cwd, sem, manifest, offline = false) {
|
|
1155
1246
|
this.cwd = cwd;
|
|
1156
1247
|
this.sem = sem;
|
|
1248
|
+
this.manifest = manifest;
|
|
1249
|
+
this.offline = offline;
|
|
1157
1250
|
}
|
|
1158
1251
|
meta = /* @__PURE__ */ new Map();
|
|
1159
1252
|
get(pkg2) {
|
|
1160
1253
|
const existing = this.meta.get(pkg2);
|
|
1161
1254
|
if (existing) return existing;
|
|
1162
1255
|
const p = this.sem.run(async () => {
|
|
1256
|
+
const manifestEntry = getManifestEntry(this.manifest, "npm", pkg2);
|
|
1257
|
+
if (manifestEntry) {
|
|
1258
|
+
const stable2 = stableOnly(manifestEntry.versions ?? []);
|
|
1259
|
+
const latestStableOverall2 = maxStable(stable2);
|
|
1260
|
+
return {
|
|
1261
|
+
latest: manifestEntry.latest ?? latestStableOverall2,
|
|
1262
|
+
stableVersions: stable2,
|
|
1263
|
+
latestStableOverall: latestStableOverall2
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
if (this.offline) {
|
|
1267
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
1268
|
+
}
|
|
1163
1269
|
let latest = null;
|
|
1164
1270
|
let versions = [];
|
|
1165
1271
|
try {
|
|
@@ -1313,7 +1419,7 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1313
1419
|
results.push(result.value);
|
|
1314
1420
|
packageNameToPath.set(result.value.name, result.value.path);
|
|
1315
1421
|
} else {
|
|
1316
|
-
const relPath =
|
|
1422
|
+
const relPath = path4.relative(rootDir, path4.dirname(pjPath));
|
|
1317
1423
|
if (cache) {
|
|
1318
1424
|
cache.addStuckPath(relPath || ".");
|
|
1319
1425
|
}
|
|
@@ -1344,8 +1450,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1344
1450
|
}
|
|
1345
1451
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
1346
1452
|
const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
|
|
1347
|
-
const absProjectPath =
|
|
1348
|
-
const projectPath =
|
|
1453
|
+
const absProjectPath = path4.dirname(packageJsonPath);
|
|
1454
|
+
const projectPath = path4.relative(rootDir, absProjectPath) || ".";
|
|
1349
1455
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
1350
1456
|
let runtimeLatest;
|
|
1351
1457
|
let runtimeMajorsBehind;
|
|
@@ -1432,7 +1538,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1432
1538
|
return {
|
|
1433
1539
|
type: "node",
|
|
1434
1540
|
path: projectPath,
|
|
1435
|
-
name: pj.name ??
|
|
1541
|
+
name: pj.name ?? path4.basename(absProjectPath),
|
|
1436
1542
|
runtime: nodeEngine,
|
|
1437
1543
|
runtimeLatest,
|
|
1438
1544
|
runtimeMajorsBehind,
|
|
@@ -1444,7 +1550,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1444
1550
|
}
|
|
1445
1551
|
|
|
1446
1552
|
// src/scanners/dotnet-scanner.ts
|
|
1447
|
-
import * as
|
|
1553
|
+
import * as path5 from "path";
|
|
1448
1554
|
import * as semver3 from "semver";
|
|
1449
1555
|
import { XMLParser } from "fast-xml-parser";
|
|
1450
1556
|
var parser = new XMLParser({
|
|
@@ -1646,7 +1752,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1646
1752
|
const parsed = parser.parse(xml);
|
|
1647
1753
|
const project = parsed?.Project;
|
|
1648
1754
|
if (!project) {
|
|
1649
|
-
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName:
|
|
1755
|
+
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path5.basename(filePath, ".csproj") };
|
|
1650
1756
|
}
|
|
1651
1757
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1652
1758
|
const targetFrameworks = [];
|
|
@@ -1683,7 +1789,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1683
1789
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1684
1790
|
packageReferences,
|
|
1685
1791
|
projectReferences,
|
|
1686
|
-
projectName:
|
|
1792
|
+
projectName: path5.basename(filePath, ".csproj")
|
|
1687
1793
|
};
|
|
1688
1794
|
}
|
|
1689
1795
|
async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
@@ -1693,12 +1799,12 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1693
1799
|
for (const slnPath of slnFiles) {
|
|
1694
1800
|
try {
|
|
1695
1801
|
const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
|
|
1696
|
-
const slnDir =
|
|
1802
|
+
const slnDir = path5.dirname(slnPath);
|
|
1697
1803
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1698
1804
|
let match;
|
|
1699
1805
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1700
1806
|
if (match[1]) {
|
|
1701
|
-
const csprojPath =
|
|
1807
|
+
const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1702
1808
|
slnCsprojPaths.add(csprojPath);
|
|
1703
1809
|
}
|
|
1704
1810
|
}
|
|
@@ -1715,7 +1821,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1715
1821
|
if (result.ok) {
|
|
1716
1822
|
results.push(result.value);
|
|
1717
1823
|
} else {
|
|
1718
|
-
const relPath =
|
|
1824
|
+
const relPath = path5.relative(rootDir, path5.dirname(csprojPath));
|
|
1719
1825
|
if (cache) {
|
|
1720
1826
|
cache.addStuckPath(relPath || ".");
|
|
1721
1827
|
}
|
|
@@ -1731,7 +1837,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1731
1837
|
async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
1732
1838
|
const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
|
|
1733
1839
|
const data = parseCsproj(xml, csprojPath);
|
|
1734
|
-
const csprojDir =
|
|
1840
|
+
const csprojDir = path5.dirname(csprojPath);
|
|
1735
1841
|
const primaryTfm = data.targetFrameworks[0];
|
|
1736
1842
|
let runtimeMajorsBehind;
|
|
1737
1843
|
let targetFramework = primaryTfm;
|
|
@@ -1809,9 +1915,9 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1809
1915
|
}
|
|
1810
1916
|
}
|
|
1811
1917
|
const projectReferences = data.projectReferences.map((refPath) => {
|
|
1812
|
-
const absRefPath =
|
|
1813
|
-
const relRefPath =
|
|
1814
|
-
const refName =
|
|
1918
|
+
const absRefPath = path5.resolve(csprojDir, refPath);
|
|
1919
|
+
const relRefPath = path5.relative(rootDir, path5.dirname(absRefPath));
|
|
1920
|
+
const refName = path5.basename(absRefPath, ".csproj");
|
|
1815
1921
|
return {
|
|
1816
1922
|
path: relRefPath || ".",
|
|
1817
1923
|
name: refName,
|
|
@@ -1832,7 +1938,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1832
1938
|
const buckets = bucketsMut;
|
|
1833
1939
|
return {
|
|
1834
1940
|
type: "dotnet",
|
|
1835
|
-
path:
|
|
1941
|
+
path: path5.relative(rootDir, csprojDir) || ".",
|
|
1836
1942
|
name: data.projectName,
|
|
1837
1943
|
targetFramework,
|
|
1838
1944
|
runtime: primaryTfm,
|
|
@@ -1847,7 +1953,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1847
1953
|
}
|
|
1848
1954
|
|
|
1849
1955
|
// src/scanners/python-scanner.ts
|
|
1850
|
-
import * as
|
|
1956
|
+
import * as path6 from "path";
|
|
1851
1957
|
import * as semver4 from "semver";
|
|
1852
1958
|
var KNOWN_PYTHON_FRAMEWORKS = {
|
|
1853
1959
|
// ── Web Frameworks ──
|
|
@@ -2088,7 +2194,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
|
|
|
2088
2194
|
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
|
|
2089
2195
|
const projectDirs = /* @__PURE__ */ new Map();
|
|
2090
2196
|
for (const f of manifestFiles) {
|
|
2091
|
-
const dir =
|
|
2197
|
+
const dir = path6.dirname(f);
|
|
2092
2198
|
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2093
2199
|
projectDirs.get(dir).push(f);
|
|
2094
2200
|
}
|
|
@@ -2101,7 +2207,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
|
|
|
2101
2207
|
if (result.ok) {
|
|
2102
2208
|
results.push(result.value);
|
|
2103
2209
|
} else {
|
|
2104
|
-
const relPath =
|
|
2210
|
+
const relPath = path6.relative(rootDir, dir);
|
|
2105
2211
|
if (cache) cache.addStuckPath(relPath || ".");
|
|
2106
2212
|
console.error(`Timeout scanning Python project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2107
2213
|
}
|
|
@@ -2117,12 +2223,12 @@ async function findPythonManifests(rootDir) {
|
|
|
2117
2223
|
return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
|
|
2118
2224
|
}
|
|
2119
2225
|
async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
|
|
2120
|
-
const relDir =
|
|
2121
|
-
let projectName =
|
|
2226
|
+
const relDir = path6.relative(rootDir, dir) || ".";
|
|
2227
|
+
let projectName = path6.basename(dir === rootDir ? rootDir : dir);
|
|
2122
2228
|
let pythonVersion;
|
|
2123
2229
|
const allDeps = /* @__PURE__ */ new Map();
|
|
2124
2230
|
for (const f of manifestFiles) {
|
|
2125
|
-
const fileName =
|
|
2231
|
+
const fileName = path6.basename(f);
|
|
2126
2232
|
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2127
2233
|
if (fileName === "pyproject.toml") {
|
|
2128
2234
|
const parsed = parsePyprojectToml(content);
|
|
@@ -2239,7 +2345,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
|
|
|
2239
2345
|
}
|
|
2240
2346
|
|
|
2241
2347
|
// src/scanners/java-scanner.ts
|
|
2242
|
-
import * as
|
|
2348
|
+
import * as path7 from "path";
|
|
2243
2349
|
import * as semver5 from "semver";
|
|
2244
2350
|
import { XMLParser as XMLParser2 } from "fast-xml-parser";
|
|
2245
2351
|
var parser2 = new XMLParser2({
|
|
@@ -2345,7 +2451,7 @@ function parsePom(xml, filePath) {
|
|
|
2345
2451
|
const project = parsed?.project;
|
|
2346
2452
|
if (!project) {
|
|
2347
2453
|
return {
|
|
2348
|
-
artifactId:
|
|
2454
|
+
artifactId: path7.basename(path7.dirname(filePath)),
|
|
2349
2455
|
dependencies: [],
|
|
2350
2456
|
modules: [],
|
|
2351
2457
|
properties: {}
|
|
@@ -2405,7 +2511,7 @@ function parsePom(xml, filePath) {
|
|
|
2405
2511
|
}
|
|
2406
2512
|
return {
|
|
2407
2513
|
groupId: project.groupId ? String(project.groupId) : parent?.groupId,
|
|
2408
|
-
artifactId: String(project.artifactId ??
|
|
2514
|
+
artifactId: String(project.artifactId ?? path7.basename(path7.dirname(filePath))),
|
|
2409
2515
|
version: project.version ? String(project.version) : parent?.version,
|
|
2410
2516
|
packaging: project.packaging ? String(project.packaging) : void 0,
|
|
2411
2517
|
javaVersion,
|
|
@@ -2420,7 +2526,7 @@ function resolveProperty(value, properties) {
|
|
|
2420
2526
|
}
|
|
2421
2527
|
function parseGradleBuild(content, filePath) {
|
|
2422
2528
|
const deps = [];
|
|
2423
|
-
const projectName =
|
|
2529
|
+
const projectName = path7.basename(path7.dirname(filePath));
|
|
2424
2530
|
let javaVersion;
|
|
2425
2531
|
const compatMatch = content.match(/(?:sourceCompatibility|targetCompatibility|javaVersion)\s*[=:]\s*['"]?(?:JavaVersion\.VERSION_)?(\d+)['"]?/);
|
|
2426
2532
|
if (compatMatch) javaVersion = compatMatch[1];
|
|
@@ -2483,7 +2589,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
|
|
|
2483
2589
|
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
|
|
2484
2590
|
const projectDirs = /* @__PURE__ */ new Map();
|
|
2485
2591
|
for (const f of manifestFiles) {
|
|
2486
|
-
const dir =
|
|
2592
|
+
const dir = path7.dirname(f);
|
|
2487
2593
|
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2488
2594
|
projectDirs.get(dir).push(f);
|
|
2489
2595
|
}
|
|
@@ -2496,7 +2602,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
|
|
|
2496
2602
|
if (result.ok) {
|
|
2497
2603
|
results.push(result.value);
|
|
2498
2604
|
} else {
|
|
2499
|
-
const relPath =
|
|
2605
|
+
const relPath = path7.relative(rootDir, dir);
|
|
2500
2606
|
if (cache) cache.addStuckPath(relPath || ".");
|
|
2501
2607
|
console.error(`Timeout scanning Java project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2502
2608
|
}
|
|
@@ -2516,13 +2622,13 @@ async function findJavaManifests(rootDir) {
|
|
|
2516
2622
|
return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
|
|
2517
2623
|
}
|
|
2518
2624
|
async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
|
|
2519
|
-
const relDir =
|
|
2520
|
-
let projectName =
|
|
2625
|
+
const relDir = path7.relative(rootDir, dir) || ".";
|
|
2626
|
+
let projectName = path7.basename(dir === rootDir ? rootDir : dir);
|
|
2521
2627
|
let javaVersion;
|
|
2522
2628
|
const allDeps = /* @__PURE__ */ new Map();
|
|
2523
2629
|
const projectReferences = [];
|
|
2524
2630
|
for (const f of manifestFiles) {
|
|
2525
|
-
const fileName =
|
|
2631
|
+
const fileName = path7.basename(f);
|
|
2526
2632
|
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2527
2633
|
if (fileName === "pom.xml") {
|
|
2528
2634
|
const pom = parsePom(content, f);
|
|
@@ -2536,7 +2642,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
|
|
|
2536
2642
|
}
|
|
2537
2643
|
for (const mod of pom.modules) {
|
|
2538
2644
|
projectReferences.push({
|
|
2539
|
-
path:
|
|
2645
|
+
path: path7.join(relDir, mod),
|
|
2540
2646
|
name: mod,
|
|
2541
2647
|
refType: "project"
|
|
2542
2648
|
});
|
|
@@ -2639,11 +2745,156 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
|
|
|
2639
2745
|
};
|
|
2640
2746
|
}
|
|
2641
2747
|
|
|
2748
|
+
// src/scanners/polyglot-scanner.ts
|
|
2749
|
+
import * as path8 from "path";
|
|
2750
|
+
var MANIFEST_TO_LANGUAGE = [
|
|
2751
|
+
{ name: "go.mod", type: "go" },
|
|
2752
|
+
{ name: "Cargo.toml", type: "rust" },
|
|
2753
|
+
{ name: "composer.json", type: "php" },
|
|
2754
|
+
{ name: "Gemfile", type: "ruby" },
|
|
2755
|
+
{ name: "Package.swift", type: "swift" },
|
|
2756
|
+
{ name: "pubspec.yaml", type: "dart" },
|
|
2757
|
+
{ name: "build.gradle.kts", type: "kotlin" },
|
|
2758
|
+
{ name: "build.sbt", type: "scala" },
|
|
2759
|
+
{ name: "DESCRIPTION", type: "r" },
|
|
2760
|
+
{ name: "Podfile", type: "objective-c" },
|
|
2761
|
+
{ name: "mix.exs", type: "elixir" },
|
|
2762
|
+
{ name: "cpanfile", type: "perl" },
|
|
2763
|
+
{ name: "Project.toml", type: "julia" },
|
|
2764
|
+
{ name: "deps.edn", type: "clojure" },
|
|
2765
|
+
{ name: "build.gradle", type: "groovy" },
|
|
2766
|
+
{ name: "tsconfig.json", type: "typescript" },
|
|
2767
|
+
{ name: "Makefile", type: "c" },
|
|
2768
|
+
{ name: "CMakeLists.txt", type: "cpp" },
|
|
2769
|
+
{ name: "*.vbp", type: "visual-basic" }
|
|
2770
|
+
];
|
|
2771
|
+
var EXTENSION_TO_LANGUAGE = [
|
|
2772
|
+
{ extensions: [".m", ".mm"], type: "objective-c" },
|
|
2773
|
+
{ extensions: [".c", ".h"], type: "c" },
|
|
2774
|
+
{ extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"], type: "cpp" },
|
|
2775
|
+
{ extensions: [".cob", ".cbl", ".cpy"], type: "cobol" },
|
|
2776
|
+
{ extensions: [".f", ".for", ".f90", ".f95", ".f03", ".f08"], type: "fortran" },
|
|
2777
|
+
{ extensions: [".pas", ".pp", ".lpr"], type: "pascal" },
|
|
2778
|
+
{ extensions: [".adb", ".ads", ".ada"], type: "ada" },
|
|
2779
|
+
{ extensions: [".asm", ".s", ".s43", ".s65"], type: "assembly" },
|
|
2780
|
+
{ extensions: [".rpg", ".rpgle", ".sqlrpgle"], type: "rpg" }
|
|
2781
|
+
];
|
|
2782
|
+
function makeDep(pkg2, spec = "unknown") {
|
|
2783
|
+
return {
|
|
2784
|
+
package: pkg2,
|
|
2785
|
+
section: "dependencies",
|
|
2786
|
+
currentSpec: spec,
|
|
2787
|
+
resolvedVersion: null,
|
|
2788
|
+
latestStable: null,
|
|
2789
|
+
majorsBehind: null,
|
|
2790
|
+
drift: "unknown"
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
function parseLineDependencies(content, regex, capture = 1) {
|
|
2794
|
+
const deps = /* @__PURE__ */ new Set();
|
|
2795
|
+
for (const line of content.split(/\r?\n/)) {
|
|
2796
|
+
const match = line.match(regex);
|
|
2797
|
+
if (match?.[capture]) deps.add(match[capture]);
|
|
2798
|
+
}
|
|
2799
|
+
return [...deps];
|
|
2800
|
+
}
|
|
2801
|
+
function getProjectName(projectPath, rootDir) {
|
|
2802
|
+
return path8.basename(projectPath) || path8.basename(rootDir);
|
|
2803
|
+
}
|
|
2804
|
+
function addProject(projects, seen, type, projectPath, rootDir, dependencies = []) {
|
|
2805
|
+
const normalizedPath = projectPath || ".";
|
|
2806
|
+
const key = `${type}:${normalizedPath}`;
|
|
2807
|
+
if (seen.has(key)) return;
|
|
2808
|
+
seen.add(key);
|
|
2809
|
+
projects.push({
|
|
2810
|
+
type,
|
|
2811
|
+
path: normalizedPath,
|
|
2812
|
+
name: getProjectName(normalizedPath, rootDir),
|
|
2813
|
+
frameworks: [],
|
|
2814
|
+
dependencies,
|
|
2815
|
+
dependencyAgeBuckets: {
|
|
2816
|
+
current: 0,
|
|
2817
|
+
oneBehind: 0,
|
|
2818
|
+
twoPlusBehind: 0,
|
|
2819
|
+
unknown: dependencies.length
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
async function parseDepsByManifest(manifestPath, cache) {
|
|
2824
|
+
const filename = path8.basename(manifestPath);
|
|
2825
|
+
const readText = async () => cache ? cache.readTextFile(manifestPath) : readTextFile(manifestPath);
|
|
2826
|
+
if (filename === "composer.json") {
|
|
2827
|
+
const content = cache ? await cache.readJsonFile(manifestPath) : await readJsonFile(manifestPath);
|
|
2828
|
+
if (!content || typeof content !== "object") return [];
|
|
2829
|
+
const deps = /* @__PURE__ */ new Set();
|
|
2830
|
+
for (const key of ["require", "require-dev"]) {
|
|
2831
|
+
const section = content[key];
|
|
2832
|
+
if (section && typeof section === "object") {
|
|
2833
|
+
for (const dep of Object.keys(section)) deps.add(dep);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return [...deps].map((dep) => makeDep(dep));
|
|
2837
|
+
}
|
|
2838
|
+
const text = await readText();
|
|
2839
|
+
if (!text) return [];
|
|
2840
|
+
if (filename === "go.mod") return parseLineDependencies(text, /^\s*require\s+([^\s]+)\s+(.+)$/).map((dep) => makeDep(dep));
|
|
2841
|
+
if (filename === "Gemfile") return parseLineDependencies(text, /^\s*gem\s+['"]([^'"]+)['"]/).map((dep) => makeDep(dep));
|
|
2842
|
+
if (filename === "Cargo.toml") return parseLineDependencies(text, /^\s*([A-Za-z0-9_\-]+)\s*=\s*['"{]/).map((dep) => makeDep(dep));
|
|
2843
|
+
if (filename === "pubspec.yaml") return parseLineDependencies(text, /^\s{2,}([A-Za-z0-9_\-]+):\s*.+$/).map((dep) => makeDep(dep));
|
|
2844
|
+
if (filename === "mix.exs") return parseLineDependencies(text, /\{\s*:([a-zA-Z0-9_]+),/).map((dep) => makeDep(dep));
|
|
2845
|
+
if (filename === "cpanfile") return parseLineDependencies(text, /^\s*requires\s+['"]([^'"]+)['"]/).map((dep) => makeDep(dep));
|
|
2846
|
+
if (filename === "build.sbt") return parseLineDependencies(text, /"([A-Za-z0-9_.\-]+)"\s*%{1,2}\s*"([A-Za-z0-9_.\-]+)"/, 2).map((dep) => makeDep(dep));
|
|
2847
|
+
return [];
|
|
2848
|
+
}
|
|
2849
|
+
function detectExtensionBackedProjects(entries) {
|
|
2850
|
+
const byDir = /* @__PURE__ */ new Map();
|
|
2851
|
+
for (const entry of entries) {
|
|
2852
|
+
if (!entry.isFile) continue;
|
|
2853
|
+
const ext = path8.extname(entry.name).toLowerCase();
|
|
2854
|
+
if (!ext) continue;
|
|
2855
|
+
for (const mapping of EXTENSION_TO_LANGUAGE) {
|
|
2856
|
+
if (!mapping.extensions.includes(ext)) continue;
|
|
2857
|
+
const dir = path8.dirname(entry.relPath) || ".";
|
|
2858
|
+
if (!byDir.has(dir)) byDir.set(dir, /* @__PURE__ */ new Set());
|
|
2859
|
+
byDir.get(dir)?.add(mapping.type);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return byDir;
|
|
2863
|
+
}
|
|
2864
|
+
async function scanPolyglotProjects(rootDir, cache) {
|
|
2865
|
+
const entries = cache ? await cache.walkDir(rootDir) : [];
|
|
2866
|
+
const candidateFiles = entries.filter((entry) => entry.isFile && MANIFEST_TO_LANGUAGE.some((m) => m.name === entry.name || m.name.startsWith("*.") && entry.name.endsWith(m.name.slice(1))));
|
|
2867
|
+
const shellDirs = new Set(
|
|
2868
|
+
entries.filter((entry) => entry.isFile && entry.name.endsWith(".sh")).map((entry) => path8.dirname(entry.relPath) || ".")
|
|
2869
|
+
);
|
|
2870
|
+
const projects = [];
|
|
2871
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2872
|
+
for (const file of candidateFiles) {
|
|
2873
|
+
const mapping = MANIFEST_TO_LANGUAGE.find((m) => m.name === file.name || m.name.startsWith("*.") && file.name.endsWith(m.name.slice(1)));
|
|
2874
|
+
if (!mapping) continue;
|
|
2875
|
+
const projectPath = path8.dirname(file.relPath) || ".";
|
|
2876
|
+
const dependencies = await parseDepsByManifest(file.absPath, cache);
|
|
2877
|
+
addProject(projects, seen, mapping.type, projectPath, rootDir, dependencies);
|
|
2878
|
+
}
|
|
2879
|
+
for (const dir of shellDirs) {
|
|
2880
|
+
addProject(projects, seen, "shell", dir, rootDir);
|
|
2881
|
+
}
|
|
2882
|
+
const extensionProjects = detectExtensionBackedProjects(entries);
|
|
2883
|
+
for (const [dir, types] of extensionProjects) {
|
|
2884
|
+
for (const type of types) {
|
|
2885
|
+
addProject(projects, seen, type, dir, rootDir);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
return projects;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2642
2891
|
// src/scanners/nuget-cache.ts
|
|
2643
2892
|
import * as semver6 from "semver";
|
|
2644
2893
|
var NuGetCache = class {
|
|
2645
|
-
constructor(sem) {
|
|
2894
|
+
constructor(sem, manifest, offline = false) {
|
|
2646
2895
|
this.sem = sem;
|
|
2896
|
+
this.manifest = manifest;
|
|
2897
|
+
this.offline = offline;
|
|
2647
2898
|
}
|
|
2648
2899
|
meta = /* @__PURE__ */ new Map();
|
|
2649
2900
|
baseUrl = "https://api.nuget.org/v3-flatcontainer";
|
|
@@ -2651,6 +2902,23 @@ var NuGetCache = class {
|
|
|
2651
2902
|
const existing = this.meta.get(pkg2);
|
|
2652
2903
|
if (existing) return existing;
|
|
2653
2904
|
const p = this.sem.run(async () => {
|
|
2905
|
+
const manifestEntry = getManifestEntry(this.manifest, "nuget", pkg2);
|
|
2906
|
+
if (manifestEntry) {
|
|
2907
|
+
const stableVersions = (manifestEntry.versions ?? []).filter((v) => {
|
|
2908
|
+
const parsed = semver6.valid(v);
|
|
2909
|
+
return parsed && semver6.prerelease(v) === null;
|
|
2910
|
+
});
|
|
2911
|
+
const sorted = [...stableVersions].sort(semver6.rcompare);
|
|
2912
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2913
|
+
return {
|
|
2914
|
+
latest: manifestEntry.latest ?? latestStableOverall,
|
|
2915
|
+
stableVersions,
|
|
2916
|
+
latestStableOverall
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
if (this.offline) {
|
|
2920
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2921
|
+
}
|
|
2654
2922
|
try {
|
|
2655
2923
|
const url = `${this.baseUrl}/${pkg2.toLowerCase()}/index.json`;
|
|
2656
2924
|
const response = await fetch(url, {
|
|
@@ -2693,14 +2961,34 @@ function pep440ToSemver2(ver) {
|
|
|
2693
2961
|
return semver7.valid(v);
|
|
2694
2962
|
}
|
|
2695
2963
|
var PyPICache = class {
|
|
2696
|
-
constructor(sem) {
|
|
2964
|
+
constructor(sem, manifest, offline = false) {
|
|
2697
2965
|
this.sem = sem;
|
|
2966
|
+
this.manifest = manifest;
|
|
2967
|
+
this.offline = offline;
|
|
2698
2968
|
}
|
|
2699
2969
|
meta = /* @__PURE__ */ new Map();
|
|
2700
2970
|
get(pkg2) {
|
|
2701
2971
|
const existing = this.meta.get(pkg2);
|
|
2702
2972
|
if (existing) return existing;
|
|
2703
2973
|
const p = this.sem.run(async () => {
|
|
2974
|
+
const manifestEntry = getManifestEntry(this.manifest, "pypi", pkg2);
|
|
2975
|
+
if (manifestEntry) {
|
|
2976
|
+
const stableVersions = [];
|
|
2977
|
+
for (const ver of manifestEntry.versions ?? []) {
|
|
2978
|
+
const sv = pep440ToSemver2(ver);
|
|
2979
|
+
if (sv) stableVersions.push(sv);
|
|
2980
|
+
}
|
|
2981
|
+
const sorted = [...stableVersions].sort(semver7.rcompare);
|
|
2982
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2983
|
+
return {
|
|
2984
|
+
latest: manifestEntry.latest ? pep440ToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
|
|
2985
|
+
stableVersions,
|
|
2986
|
+
latestStableOverall
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
if (this.offline) {
|
|
2990
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2991
|
+
}
|
|
2704
2992
|
try {
|
|
2705
2993
|
const url = `https://pypi.org/pypi/${encodeURIComponent(pkg2)}/json`;
|
|
2706
2994
|
const response = await fetch(url, {
|
|
@@ -2747,8 +3035,10 @@ function mavenToSemver2(ver) {
|
|
|
2747
3035
|
return semver8.valid(v);
|
|
2748
3036
|
}
|
|
2749
3037
|
var MavenCache = class {
|
|
2750
|
-
constructor(sem) {
|
|
3038
|
+
constructor(sem, manifest, offline = false) {
|
|
2751
3039
|
this.sem = sem;
|
|
3040
|
+
this.manifest = manifest;
|
|
3041
|
+
this.offline = offline;
|
|
2752
3042
|
}
|
|
2753
3043
|
meta = /* @__PURE__ */ new Map();
|
|
2754
3044
|
/**
|
|
@@ -2761,6 +3051,25 @@ var MavenCache = class {
|
|
|
2761
3051
|
const existing = this.meta.get(key);
|
|
2762
3052
|
if (existing) return existing;
|
|
2763
3053
|
const p = this.sem.run(async () => {
|
|
3054
|
+
const key2 = `${groupId}:${artifactId}`;
|
|
3055
|
+
const manifestEntry = getManifestEntry(this.manifest, "maven", key2);
|
|
3056
|
+
if (manifestEntry) {
|
|
3057
|
+
const stableVersions = [];
|
|
3058
|
+
for (const ver of manifestEntry.versions ?? []) {
|
|
3059
|
+
const sv = mavenToSemver2(ver);
|
|
3060
|
+
if (sv) stableVersions.push(sv);
|
|
3061
|
+
}
|
|
3062
|
+
const sorted = [...stableVersions].sort(semver8.rcompare);
|
|
3063
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
3064
|
+
return {
|
|
3065
|
+
latest: manifestEntry.latest ? mavenToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
|
|
3066
|
+
stableVersions,
|
|
3067
|
+
latestStableOverall
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
if (this.offline) {
|
|
3071
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
3072
|
+
}
|
|
2764
3073
|
try {
|
|
2765
3074
|
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
3075
|
const response = await fetch(url, {
|
|
@@ -2795,7 +3104,7 @@ var MavenCache = class {
|
|
|
2795
3104
|
};
|
|
2796
3105
|
|
|
2797
3106
|
// src/config.ts
|
|
2798
|
-
import * as
|
|
3107
|
+
import * as path9 from "path";
|
|
2799
3108
|
import * as fs from "fs/promises";
|
|
2800
3109
|
var CONFIG_FILES = [
|
|
2801
3110
|
"vibgrate.config.ts",
|
|
@@ -2821,7 +3130,7 @@ var DEFAULT_CONFIG = {
|
|
|
2821
3130
|
async function loadConfig(rootDir) {
|
|
2822
3131
|
let config = DEFAULT_CONFIG;
|
|
2823
3132
|
for (const file of CONFIG_FILES) {
|
|
2824
|
-
const configPath =
|
|
3133
|
+
const configPath = path9.join(rootDir, file);
|
|
2825
3134
|
if (await pathExists(configPath)) {
|
|
2826
3135
|
if (file.endsWith(".json")) {
|
|
2827
3136
|
const txt = await readTextFile(configPath);
|
|
@@ -2836,7 +3145,7 @@ async function loadConfig(rootDir) {
|
|
|
2836
3145
|
}
|
|
2837
3146
|
}
|
|
2838
3147
|
}
|
|
2839
|
-
const sidecarPath =
|
|
3148
|
+
const sidecarPath = path9.join(rootDir, ".vibgrate", "auto-excludes.json");
|
|
2840
3149
|
if (await pathExists(sidecarPath)) {
|
|
2841
3150
|
try {
|
|
2842
3151
|
const txt = await readTextFile(sidecarPath);
|
|
@@ -2851,7 +3160,7 @@ async function loadConfig(rootDir) {
|
|
|
2851
3160
|
return config;
|
|
2852
3161
|
}
|
|
2853
3162
|
async function writeDefaultConfig(rootDir) {
|
|
2854
|
-
const configPath =
|
|
3163
|
+
const configPath = path9.join(rootDir, "vibgrate.config.ts");
|
|
2855
3164
|
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
2856
3165
|
|
|
2857
3166
|
const config: VibgrateConfig = {
|
|
@@ -2877,7 +3186,7 @@ export default config;
|
|
|
2877
3186
|
}
|
|
2878
3187
|
async function appendExcludePatterns(rootDir, newPatterns) {
|
|
2879
3188
|
if (newPatterns.length === 0) return false;
|
|
2880
|
-
const jsonPath =
|
|
3189
|
+
const jsonPath = path9.join(rootDir, "vibgrate.config.json");
|
|
2881
3190
|
if (await pathExists(jsonPath)) {
|
|
2882
3191
|
try {
|
|
2883
3192
|
const txt = await readTextFile(jsonPath);
|
|
@@ -2890,8 +3199,8 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2890
3199
|
} catch {
|
|
2891
3200
|
}
|
|
2892
3201
|
}
|
|
2893
|
-
const vibgrateDir =
|
|
2894
|
-
const sidecarPath =
|
|
3202
|
+
const vibgrateDir = path9.join(rootDir, ".vibgrate");
|
|
3203
|
+
const sidecarPath = path9.join(vibgrateDir, "auto-excludes.json");
|
|
2895
3204
|
let existing = [];
|
|
2896
3205
|
if (await pathExists(sidecarPath)) {
|
|
2897
3206
|
try {
|
|
@@ -2912,7 +3221,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2912
3221
|
}
|
|
2913
3222
|
|
|
2914
3223
|
// src/utils/vcs.ts
|
|
2915
|
-
import * as
|
|
3224
|
+
import * as path10 from "path";
|
|
2916
3225
|
import * as fs2 from "fs/promises";
|
|
2917
3226
|
async function detectVcs(rootDir) {
|
|
2918
3227
|
try {
|
|
@@ -2926,7 +3235,7 @@ async function detectGit(rootDir) {
|
|
|
2926
3235
|
if (!gitDir) {
|
|
2927
3236
|
return { type: "unknown" };
|
|
2928
3237
|
}
|
|
2929
|
-
const headPath =
|
|
3238
|
+
const headPath = path10.join(gitDir, "HEAD");
|
|
2930
3239
|
let headContent;
|
|
2931
3240
|
try {
|
|
2932
3241
|
headContent = (await fs2.readFile(headPath, "utf8")).trim();
|
|
@@ -2942,18 +3251,20 @@ async function detectGit(rootDir) {
|
|
|
2942
3251
|
} else if (/^[0-9a-f]{40}$/i.test(headContent)) {
|
|
2943
3252
|
sha = headContent;
|
|
2944
3253
|
}
|
|
3254
|
+
const remoteUrl = await readGitRemoteUrl(gitDir);
|
|
2945
3255
|
return {
|
|
2946
3256
|
type: "git",
|
|
2947
3257
|
sha: sha ?? void 0,
|
|
2948
3258
|
shortSha: sha ? sha.slice(0, 7) : void 0,
|
|
2949
|
-
branch: branch ?? void 0
|
|
3259
|
+
branch: branch ?? void 0,
|
|
3260
|
+
remoteUrl
|
|
2950
3261
|
};
|
|
2951
3262
|
}
|
|
2952
3263
|
async function findGitDir(startDir) {
|
|
2953
|
-
let dir =
|
|
2954
|
-
const root =
|
|
3264
|
+
let dir = path10.resolve(startDir);
|
|
3265
|
+
const root = path10.parse(dir).root;
|
|
2955
3266
|
while (dir !== root) {
|
|
2956
|
-
const gitPath =
|
|
3267
|
+
const gitPath = path10.join(dir, ".git");
|
|
2957
3268
|
try {
|
|
2958
3269
|
const stat3 = await fs2.stat(gitPath);
|
|
2959
3270
|
if (stat3.isDirectory()) {
|
|
@@ -2962,18 +3273,18 @@ async function findGitDir(startDir) {
|
|
|
2962
3273
|
if (stat3.isFile()) {
|
|
2963
3274
|
const content = (await fs2.readFile(gitPath, "utf8")).trim();
|
|
2964
3275
|
if (content.startsWith("gitdir: ")) {
|
|
2965
|
-
const resolved =
|
|
3276
|
+
const resolved = path10.resolve(dir, content.slice(8));
|
|
2966
3277
|
return resolved;
|
|
2967
3278
|
}
|
|
2968
3279
|
}
|
|
2969
3280
|
} catch {
|
|
2970
3281
|
}
|
|
2971
|
-
dir =
|
|
3282
|
+
dir = path10.dirname(dir);
|
|
2972
3283
|
}
|
|
2973
3284
|
return null;
|
|
2974
3285
|
}
|
|
2975
3286
|
async function resolveRef(gitDir, refPath) {
|
|
2976
|
-
const loosePath =
|
|
3287
|
+
const loosePath = path10.join(gitDir, refPath);
|
|
2977
3288
|
try {
|
|
2978
3289
|
const sha = (await fs2.readFile(loosePath, "utf8")).trim();
|
|
2979
3290
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
@@ -2981,7 +3292,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
2981
3292
|
}
|
|
2982
3293
|
} catch {
|
|
2983
3294
|
}
|
|
2984
|
-
const packedPath =
|
|
3295
|
+
const packedPath = path10.join(gitDir, "packed-refs");
|
|
2985
3296
|
try {
|
|
2986
3297
|
const packed = await fs2.readFile(packedPath, "utf8");
|
|
2987
3298
|
for (const line of packed.split("\n")) {
|
|
@@ -2995,6 +3306,39 @@ async function resolveRef(gitDir, refPath) {
|
|
|
2995
3306
|
}
|
|
2996
3307
|
return void 0;
|
|
2997
3308
|
}
|
|
3309
|
+
async function readGitRemoteUrl(gitDir) {
|
|
3310
|
+
const configPath = await resolveGitConfigPath(gitDir);
|
|
3311
|
+
if (!configPath) return void 0;
|
|
3312
|
+
try {
|
|
3313
|
+
const config = await fs2.readFile(configPath, "utf8");
|
|
3314
|
+
const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
|
|
3315
|
+
if (!originBlock) return void 0;
|
|
3316
|
+
const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
|
|
3317
|
+
return urlMatch?.[1]?.trim();
|
|
3318
|
+
} catch {
|
|
3319
|
+
return void 0;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
async function resolveGitConfigPath(gitDir) {
|
|
3323
|
+
const directConfig = path10.join(gitDir, "config");
|
|
3324
|
+
try {
|
|
3325
|
+
const stat3 = await fs2.stat(directConfig);
|
|
3326
|
+
if (stat3.isFile()) return directConfig;
|
|
3327
|
+
} catch {
|
|
3328
|
+
}
|
|
3329
|
+
const commonDirFile = path10.join(gitDir, "commondir");
|
|
3330
|
+
try {
|
|
3331
|
+
const commonDir = (await fs2.readFile(commonDirFile, "utf8")).trim();
|
|
3332
|
+
if (!commonDir) return void 0;
|
|
3333
|
+
const resolvedCommonDir = path10.resolve(gitDir, commonDir);
|
|
3334
|
+
const commonConfig = path10.join(resolvedCommonDir, "config");
|
|
3335
|
+
const stat3 = await fs2.stat(commonConfig);
|
|
3336
|
+
if (stat3.isFile()) return commonConfig;
|
|
3337
|
+
} catch {
|
|
3338
|
+
return void 0;
|
|
3339
|
+
}
|
|
3340
|
+
return void 0;
|
|
3341
|
+
}
|
|
2998
3342
|
|
|
2999
3343
|
// src/ui/progress.ts
|
|
3000
3344
|
import chalk4 from "chalk";
|
|
@@ -3378,11 +3722,11 @@ var ScanProgress = class {
|
|
|
3378
3722
|
|
|
3379
3723
|
// src/ui/scan-history.ts
|
|
3380
3724
|
import * as fs3 from "fs/promises";
|
|
3381
|
-
import * as
|
|
3725
|
+
import * as path11 from "path";
|
|
3382
3726
|
var HISTORY_FILENAME = "scan_history.json";
|
|
3383
3727
|
var MAX_RECORDS = 10;
|
|
3384
3728
|
async function loadScanHistory(rootDir) {
|
|
3385
|
-
const filePath =
|
|
3729
|
+
const filePath = path11.join(rootDir, ".vibgrate", HISTORY_FILENAME);
|
|
3386
3730
|
try {
|
|
3387
3731
|
const txt = await fs3.readFile(filePath, "utf8");
|
|
3388
3732
|
const data = JSON.parse(txt);
|
|
@@ -3395,8 +3739,8 @@ async function loadScanHistory(rootDir) {
|
|
|
3395
3739
|
}
|
|
3396
3740
|
}
|
|
3397
3741
|
async function saveScanHistory(rootDir, record) {
|
|
3398
|
-
const dir =
|
|
3399
|
-
const filePath =
|
|
3742
|
+
const dir = path11.join(rootDir, ".vibgrate");
|
|
3743
|
+
const filePath = path11.join(dir, HISTORY_FILENAME);
|
|
3400
3744
|
let history;
|
|
3401
3745
|
const existing = await loadScanHistory(rootDir);
|
|
3402
3746
|
if (existing) {
|
|
@@ -3460,7 +3804,7 @@ function estimateStepDurations(history, currentFileCount) {
|
|
|
3460
3804
|
}
|
|
3461
3805
|
|
|
3462
3806
|
// src/scanners/platform-matrix.ts
|
|
3463
|
-
import * as
|
|
3807
|
+
import * as path12 from "path";
|
|
3464
3808
|
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
3465
3809
|
// Image / media processing
|
|
3466
3810
|
"sharp",
|
|
@@ -3740,7 +4084,7 @@ async function scanPlatformMatrix(rootDir, cache) {
|
|
|
3740
4084
|
}
|
|
3741
4085
|
result.dockerBaseImages = [...baseImages].sort();
|
|
3742
4086
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
3743
|
-
const exists = cache ? await cache.pathExists(
|
|
4087
|
+
const exists = cache ? await cache.pathExists(path12.join(rootDir, file)) : await pathExists(path12.join(rootDir, file));
|
|
3744
4088
|
if (exists) {
|
|
3745
4089
|
result.nodeVersionFiles.push(file);
|
|
3746
4090
|
}
|
|
@@ -3817,7 +4161,7 @@ function scanDependencyRisk(projects) {
|
|
|
3817
4161
|
}
|
|
3818
4162
|
|
|
3819
4163
|
// src/scanners/dependency-graph.ts
|
|
3820
|
-
import * as
|
|
4164
|
+
import * as path13 from "path";
|
|
3821
4165
|
function parsePnpmLock(content) {
|
|
3822
4166
|
const entries = [];
|
|
3823
4167
|
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
@@ -3876,9 +4220,9 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
3876
4220
|
phantomDependencies: []
|
|
3877
4221
|
};
|
|
3878
4222
|
let entries = [];
|
|
3879
|
-
const pnpmLock =
|
|
3880
|
-
const npmLock =
|
|
3881
|
-
const yarnLock =
|
|
4223
|
+
const pnpmLock = path13.join(rootDir, "pnpm-lock.yaml");
|
|
4224
|
+
const npmLock = path13.join(rootDir, "package-lock.json");
|
|
4225
|
+
const yarnLock = path13.join(rootDir, "yarn.lock");
|
|
3882
4226
|
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
3883
4227
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3884
4228
|
if (await _pathExists(pnpmLock)) {
|
|
@@ -3925,7 +4269,7 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
3925
4269
|
for (const pjPath of pkgFiles) {
|
|
3926
4270
|
try {
|
|
3927
4271
|
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
3928
|
-
const relPath =
|
|
4272
|
+
const relPath = path13.relative(rootDir, pjPath);
|
|
3929
4273
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
3930
4274
|
const deps = pj[section];
|
|
3931
4275
|
if (!deps) continue;
|
|
@@ -4271,7 +4615,7 @@ function scanToolingInventory(projects) {
|
|
|
4271
4615
|
}
|
|
4272
4616
|
|
|
4273
4617
|
// src/scanners/build-deploy.ts
|
|
4274
|
-
import * as
|
|
4618
|
+
import * as path14 from "path";
|
|
4275
4619
|
var CI_FILES = {
|
|
4276
4620
|
".github/workflows": "github-actions",
|
|
4277
4621
|
".gitlab-ci.yml": "gitlab-ci",
|
|
@@ -4324,17 +4668,17 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4324
4668
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
4325
4669
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
4326
4670
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
4327
|
-
const fullPath =
|
|
4671
|
+
const fullPath = path14.join(rootDir, file);
|
|
4328
4672
|
if (await _pathExists(fullPath)) {
|
|
4329
4673
|
ciSystems.add(system);
|
|
4330
4674
|
}
|
|
4331
4675
|
}
|
|
4332
|
-
const ghWorkflowDir =
|
|
4676
|
+
const ghWorkflowDir = path14.join(rootDir, ".github", "workflows");
|
|
4333
4677
|
if (await _pathExists(ghWorkflowDir)) {
|
|
4334
4678
|
try {
|
|
4335
4679
|
if (cache) {
|
|
4336
4680
|
const entries = await cache.walkDir(rootDir);
|
|
4337
|
-
const ghPrefix =
|
|
4681
|
+
const ghPrefix = path14.relative(rootDir, ghWorkflowDir) + path14.sep;
|
|
4338
4682
|
result.ciWorkflowCount = entries.filter(
|
|
4339
4683
|
(e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
|
|
4340
4684
|
).length;
|
|
@@ -4385,11 +4729,11 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4385
4729
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
4386
4730
|
);
|
|
4387
4731
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
4388
|
-
if (await _pathExists(
|
|
4732
|
+
if (await _pathExists(path14.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
4389
4733
|
result.iac = [...iacSystems].sort();
|
|
4390
4734
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
4391
4735
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
4392
|
-
if (await _pathExists(
|
|
4736
|
+
if (await _pathExists(path14.join(rootDir, file))) releaseTools.add(tool);
|
|
4393
4737
|
}
|
|
4394
4738
|
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
4395
4739
|
for (const pjPath of pkgFiles) {
|
|
@@ -4414,19 +4758,19 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4414
4758
|
};
|
|
4415
4759
|
const managers = /* @__PURE__ */ new Set();
|
|
4416
4760
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
4417
|
-
if (await _pathExists(
|
|
4761
|
+
if (await _pathExists(path14.join(rootDir, file))) managers.add(manager);
|
|
4418
4762
|
}
|
|
4419
4763
|
result.packageManagers = [...managers].sort();
|
|
4420
4764
|
const monoTools = /* @__PURE__ */ new Set();
|
|
4421
4765
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
4422
|
-
if (await _pathExists(
|
|
4766
|
+
if (await _pathExists(path14.join(rootDir, file))) monoTools.add(tool);
|
|
4423
4767
|
}
|
|
4424
4768
|
result.monorepoTools = [...monoTools].sort();
|
|
4425
4769
|
return result;
|
|
4426
4770
|
}
|
|
4427
4771
|
|
|
4428
4772
|
// src/scanners/ts-modernity.ts
|
|
4429
|
-
import * as
|
|
4773
|
+
import * as path15 from "path";
|
|
4430
4774
|
async function scanTsModernity(rootDir, cache) {
|
|
4431
4775
|
const result = {
|
|
4432
4776
|
typescriptVersion: null,
|
|
@@ -4464,7 +4808,7 @@ async function scanTsModernity(rootDir, cache) {
|
|
|
4464
4808
|
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
4465
4809
|
else if (hasEsm) result.moduleType = "esm";
|
|
4466
4810
|
else if (hasCjs) result.moduleType = "cjs";
|
|
4467
|
-
let tsConfigPath =
|
|
4811
|
+
let tsConfigPath = path15.join(rootDir, "tsconfig.json");
|
|
4468
4812
|
const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
|
|
4469
4813
|
if (!tsConfigExists) {
|
|
4470
4814
|
const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
@@ -4811,7 +5155,7 @@ function scanBreakingChangeExposure(projects) {
|
|
|
4811
5155
|
|
|
4812
5156
|
// src/scanners/file-hotspots.ts
|
|
4813
5157
|
import * as fs4 from "fs/promises";
|
|
4814
|
-
import * as
|
|
5158
|
+
import * as path16 from "path";
|
|
4815
5159
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4816
5160
|
"node_modules",
|
|
4817
5161
|
".git",
|
|
@@ -4856,9 +5200,9 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4856
5200
|
const entries = await cache.walkDir(rootDir);
|
|
4857
5201
|
for (const entry of entries) {
|
|
4858
5202
|
if (!entry.isFile) continue;
|
|
4859
|
-
const ext =
|
|
5203
|
+
const ext = path16.extname(entry.name).toLowerCase();
|
|
4860
5204
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4861
|
-
const depth = entry.relPath.split(
|
|
5205
|
+
const depth = entry.relPath.split(path16.sep).length - 1;
|
|
4862
5206
|
if (depth > maxDepth) maxDepth = depth;
|
|
4863
5207
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4864
5208
|
try {
|
|
@@ -4887,15 +5231,15 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4887
5231
|
for (const e of entries) {
|
|
4888
5232
|
if (e.isDirectory) {
|
|
4889
5233
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
4890
|
-
await walk(
|
|
5234
|
+
await walk(path16.join(dir, e.name), depth + 1);
|
|
4891
5235
|
} else if (e.isFile) {
|
|
4892
|
-
const ext =
|
|
5236
|
+
const ext = path16.extname(e.name).toLowerCase();
|
|
4893
5237
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4894
5238
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4895
5239
|
try {
|
|
4896
|
-
const stat3 = await fs4.stat(
|
|
5240
|
+
const stat3 = await fs4.stat(path16.join(dir, e.name));
|
|
4897
5241
|
allFiles.push({
|
|
4898
|
-
path:
|
|
5242
|
+
path: path16.relative(rootDir, path16.join(dir, e.name)),
|
|
4899
5243
|
bytes: stat3.size
|
|
4900
5244
|
});
|
|
4901
5245
|
} catch {
|
|
@@ -4918,7 +5262,7 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4918
5262
|
}
|
|
4919
5263
|
|
|
4920
5264
|
// src/scanners/security-posture.ts
|
|
4921
|
-
import * as
|
|
5265
|
+
import * as path17 from "path";
|
|
4922
5266
|
var LOCKFILES = {
|
|
4923
5267
|
"pnpm-lock.yaml": "pnpm",
|
|
4924
5268
|
"package-lock.json": "npm",
|
|
@@ -4939,14 +5283,14 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4939
5283
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
4940
5284
|
const foundLockfiles = [];
|
|
4941
5285
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
4942
|
-
if (await _pathExists(
|
|
5286
|
+
if (await _pathExists(path17.join(rootDir, file))) {
|
|
4943
5287
|
foundLockfiles.push(type);
|
|
4944
5288
|
}
|
|
4945
5289
|
}
|
|
4946
5290
|
result.lockfilePresent = foundLockfiles.length > 0;
|
|
4947
5291
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
4948
5292
|
result.lockfileTypes = foundLockfiles.sort();
|
|
4949
|
-
const gitignorePath =
|
|
5293
|
+
const gitignorePath = path17.join(rootDir, ".gitignore");
|
|
4950
5294
|
if (await _pathExists(gitignorePath)) {
|
|
4951
5295
|
try {
|
|
4952
5296
|
const content = await _readTextFile(gitignorePath);
|
|
@@ -4961,7 +5305,7 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4961
5305
|
}
|
|
4962
5306
|
}
|
|
4963
5307
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
4964
|
-
if (await _pathExists(
|
|
5308
|
+
if (await _pathExists(path17.join(rootDir, envFile))) {
|
|
4965
5309
|
if (!result.gitignoreCoversEnv) {
|
|
4966
5310
|
result.envFilesTracked = true;
|
|
4967
5311
|
break;
|
|
@@ -4972,8 +5316,8 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4972
5316
|
}
|
|
4973
5317
|
|
|
4974
5318
|
// src/scanners/security-scanners.ts
|
|
4975
|
-
import { spawn as
|
|
4976
|
-
import * as
|
|
5319
|
+
import { spawn as spawn3 } from "child_process";
|
|
5320
|
+
import * as path18 from "path";
|
|
4977
5321
|
var TOOL_MATRIX = [
|
|
4978
5322
|
{ key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
|
|
4979
5323
|
{ key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
|
|
@@ -4986,8 +5330,8 @@ var SECRET_HEURISTICS = [
|
|
|
4986
5330
|
{ detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
4987
5331
|
{ detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
|
|
4988
5332
|
];
|
|
4989
|
-
var defaultRunner = (command, args) => new Promise((
|
|
4990
|
-
const child =
|
|
5333
|
+
var defaultRunner = (command, args) => new Promise((resolve10, reject) => {
|
|
5334
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4991
5335
|
let stdout = "";
|
|
4992
5336
|
let stderr = "";
|
|
4993
5337
|
child.stdout.on("data", (d) => {
|
|
@@ -4998,7 +5342,7 @@ var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
|
|
|
4998
5342
|
});
|
|
4999
5343
|
child.on("error", reject);
|
|
5000
5344
|
child.on("close", (code) => {
|
|
5001
|
-
|
|
5345
|
+
resolve10({ stdout, stderr, exitCode: code ?? 1 });
|
|
5002
5346
|
});
|
|
5003
5347
|
});
|
|
5004
5348
|
function compareSemver(a, b) {
|
|
@@ -5068,7 +5412,7 @@ async function detectSecretHeuristics(rootDir, cache) {
|
|
|
5068
5412
|
const findings = [];
|
|
5069
5413
|
for (const entry of entries) {
|
|
5070
5414
|
if (!entry.isFile) continue;
|
|
5071
|
-
const ext =
|
|
5415
|
+
const ext = path18.extname(entry.name).toLowerCase();
|
|
5072
5416
|
if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
|
|
5073
5417
|
const content = await cache.readTextFile(entry.absPath);
|
|
5074
5418
|
if (!content || content.length > 3e5) continue;
|
|
@@ -5091,9 +5435,9 @@ async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
|
|
|
5091
5435
|
const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
|
|
5092
5436
|
const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
|
|
5093
5437
|
const configFiles = {
|
|
5094
|
-
semgrep: await cache.pathExists(
|
|
5095
|
-
gitleaks: await cache.pathExists(
|
|
5096
|
-
trufflehog: await cache.pathExists(
|
|
5438
|
+
semgrep: await cache.pathExists(path18.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path18.join(rootDir, ".semgrep.yaml")),
|
|
5439
|
+
gitleaks: await cache.pathExists(path18.join(rootDir, ".gitleaks.toml")),
|
|
5440
|
+
trufflehog: await cache.pathExists(path18.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path18.join(rootDir, ".trufflehog.yaml"))
|
|
5097
5441
|
};
|
|
5098
5442
|
return {
|
|
5099
5443
|
semgrep,
|
|
@@ -5518,7 +5862,7 @@ function scanServiceDependencies(projects) {
|
|
|
5518
5862
|
}
|
|
5519
5863
|
|
|
5520
5864
|
// src/scanners/architecture.ts
|
|
5521
|
-
import * as
|
|
5865
|
+
import * as path19 from "path";
|
|
5522
5866
|
import * as fs5 from "fs/promises";
|
|
5523
5867
|
var ARCHETYPE_SIGNALS = [
|
|
5524
5868
|
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
@@ -5817,9 +6161,9 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5817
6161
|
const entries = await cache.walkDir(rootDir);
|
|
5818
6162
|
return entries.filter((e) => {
|
|
5819
6163
|
if (!e.isFile) return false;
|
|
5820
|
-
const name =
|
|
6164
|
+
const name = path19.basename(e.absPath);
|
|
5821
6165
|
if (name.startsWith(".") && name !== ".") return false;
|
|
5822
|
-
const ext =
|
|
6166
|
+
const ext = path19.extname(name);
|
|
5823
6167
|
return SOURCE_EXTENSIONS.has(ext);
|
|
5824
6168
|
}).map((e) => e.relPath);
|
|
5825
6169
|
}
|
|
@@ -5833,15 +6177,15 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5833
6177
|
}
|
|
5834
6178
|
for (const entry of entries) {
|
|
5835
6179
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
5836
|
-
const fullPath =
|
|
6180
|
+
const fullPath = path19.join(dir, entry.name);
|
|
5837
6181
|
if (entry.isDirectory()) {
|
|
5838
6182
|
if (!IGNORE_DIRS.has(entry.name)) {
|
|
5839
6183
|
await walk(fullPath);
|
|
5840
6184
|
}
|
|
5841
6185
|
} else if (entry.isFile()) {
|
|
5842
|
-
const ext =
|
|
6186
|
+
const ext = path19.extname(entry.name);
|
|
5843
6187
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
5844
|
-
files.push(
|
|
6188
|
+
files.push(path19.relative(rootDir, fullPath));
|
|
5845
6189
|
}
|
|
5846
6190
|
}
|
|
5847
6191
|
}
|
|
@@ -5865,7 +6209,7 @@ function classifyFile(filePath, archetype) {
|
|
|
5865
6209
|
}
|
|
5866
6210
|
}
|
|
5867
6211
|
if (!bestMatch || bestMatch.confidence < 0.7) {
|
|
5868
|
-
const baseName =
|
|
6212
|
+
const baseName = path19.basename(filePath, path19.extname(filePath));
|
|
5869
6213
|
const cleanBase = baseName.replace(/\.(test|spec)$/, "");
|
|
5870
6214
|
for (const rule of SUFFIX_RULES) {
|
|
5871
6215
|
if (cleanBase.endsWith(rule.suffix)) {
|
|
@@ -5974,7 +6318,7 @@ function generateLayerFlowMermaid(layers) {
|
|
|
5974
6318
|
return lines.join("\n");
|
|
5975
6319
|
}
|
|
5976
6320
|
async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
|
|
5977
|
-
const projectRoot =
|
|
6321
|
+
const projectRoot = path19.resolve(rootDir, project.path || ".");
|
|
5978
6322
|
const allFiles = await walkSourceFiles(projectRoot, cache);
|
|
5979
6323
|
const layerSet = /* @__PURE__ */ new Set();
|
|
5980
6324
|
for (const rel of allFiles) {
|
|
@@ -6100,7 +6444,7 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
|
|
|
6100
6444
|
}
|
|
6101
6445
|
|
|
6102
6446
|
// src/scanners/code-quality.ts
|
|
6103
|
-
import * as
|
|
6447
|
+
import * as path20 from "path";
|
|
6104
6448
|
import * as ts from "typescript";
|
|
6105
6449
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6106
6450
|
var DEFAULT_RESULT = {
|
|
@@ -6131,9 +6475,9 @@ async function scanCodeQuality(rootDir, cache) {
|
|
|
6131
6475
|
continue;
|
|
6132
6476
|
}
|
|
6133
6477
|
if (!raw.trim()) continue;
|
|
6134
|
-
const rel = normalizeModuleId(
|
|
6478
|
+
const rel = normalizeModuleId(path20.relative(rootDir, filePath));
|
|
6135
6479
|
const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
|
|
6136
|
-
const imports = collectLocalImports(source,
|
|
6480
|
+
const imports = collectLocalImports(source, path20.dirname(filePath), rootDir);
|
|
6137
6481
|
depGraph.set(rel, imports);
|
|
6138
6482
|
const fileMetrics = computeFileMetrics(source, raw);
|
|
6139
6483
|
totalFunctions += fileMetrics.functionsAnalyzed;
|
|
@@ -6167,9 +6511,9 @@ async function scanCodeQuality(rootDir, cache) {
|
|
|
6167
6511
|
async function findSourceFiles(rootDir, cache) {
|
|
6168
6512
|
if (cache) {
|
|
6169
6513
|
const entries = await cache.walkDir(rootDir);
|
|
6170
|
-
return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(
|
|
6514
|
+
return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path20.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
|
|
6171
6515
|
}
|
|
6172
|
-
const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(
|
|
6516
|
+
const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path20.extname(name).toLowerCase()));
|
|
6173
6517
|
return files;
|
|
6174
6518
|
}
|
|
6175
6519
|
function collectLocalImports(source, fileDir, rootDir) {
|
|
@@ -6190,8 +6534,8 @@ function collectLocalImports(source, fileDir, rootDir) {
|
|
|
6190
6534
|
}
|
|
6191
6535
|
function resolveLocalImport(specifier, fileDir, rootDir) {
|
|
6192
6536
|
if (!specifier.startsWith(".")) return null;
|
|
6193
|
-
const rawTarget =
|
|
6194
|
-
const normalized =
|
|
6537
|
+
const rawTarget = path20.resolve(fileDir, specifier);
|
|
6538
|
+
const normalized = path20.relative(rootDir, rawTarget).replace(/\\/g, "/");
|
|
6195
6539
|
if (!normalized || normalized.startsWith("..")) return null;
|
|
6196
6540
|
return normalizeModuleId(normalized);
|
|
6197
6541
|
}
|
|
@@ -6332,8 +6676,8 @@ function visitEach(node, cb) {
|
|
|
6332
6676
|
}
|
|
6333
6677
|
|
|
6334
6678
|
// src/scanners/owasp-category-mapping.ts
|
|
6335
|
-
import { spawn as
|
|
6336
|
-
import * as
|
|
6679
|
+
import { spawn as spawn4 } from "child_process";
|
|
6680
|
+
import * as path21 from "path";
|
|
6337
6681
|
var OWASP_CONFIG = "p/owasp-top-ten";
|
|
6338
6682
|
var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6339
6683
|
".js",
|
|
@@ -6358,8 +6702,8 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
6358
6702
|
".env"
|
|
6359
6703
|
]);
|
|
6360
6704
|
async function runSemgrep(args, cwd, stdin) {
|
|
6361
|
-
return new Promise((
|
|
6362
|
-
const child =
|
|
6705
|
+
return new Promise((resolve10, reject) => {
|
|
6706
|
+
const child = spawn4("semgrep", args, {
|
|
6363
6707
|
cwd,
|
|
6364
6708
|
shell: true,
|
|
6365
6709
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6374,7 +6718,7 @@ async function runSemgrep(args, cwd, stdin) {
|
|
|
6374
6718
|
});
|
|
6375
6719
|
child.on("error", reject);
|
|
6376
6720
|
child.on("close", (code) => {
|
|
6377
|
-
|
|
6721
|
+
resolve10({ code: code ?? 1, stdout, stderr });
|
|
6378
6722
|
});
|
|
6379
6723
|
if (stdin !== void 0) child.stdin.write(stdin);
|
|
6380
6724
|
child.stdin.end();
|
|
@@ -6412,7 +6756,7 @@ function parseFindings(results, rootDir) {
|
|
|
6412
6756
|
const metadata = r.extra?.metadata;
|
|
6413
6757
|
return {
|
|
6414
6758
|
ruleId: r.check_id ?? "unknown",
|
|
6415
|
-
path: r.path ?
|
|
6759
|
+
path: r.path ? path21.relative(rootDir, path21.resolve(rootDir, r.path)) : "",
|
|
6416
6760
|
line: r.start?.line ?? 1,
|
|
6417
6761
|
endLine: r.end?.line,
|
|
6418
6762
|
message: r.extra?.message ?? "Potential security issue",
|
|
@@ -6472,7 +6816,7 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
|
|
|
6472
6816
|
}
|
|
6473
6817
|
}
|
|
6474
6818
|
const entries = cache ? await cache.walkDir(rootDir) : [];
|
|
6475
|
-
const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(
|
|
6819
|
+
const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path21.extname(e.name).toLowerCase()));
|
|
6476
6820
|
const findings = [];
|
|
6477
6821
|
const errors = [];
|
|
6478
6822
|
let scannedFiles = 0;
|
|
@@ -6720,7 +7064,7 @@ function isUsefulString(s) {
|
|
|
6720
7064
|
}
|
|
6721
7065
|
|
|
6722
7066
|
// src/utils/tool-installer.ts
|
|
6723
|
-
import { spawn as
|
|
7067
|
+
import { spawn as spawn5 } from "child_process";
|
|
6724
7068
|
import chalk5 from "chalk";
|
|
6725
7069
|
var SECURITY_TOOLS = [
|
|
6726
7070
|
{ name: "semgrep", command: "semgrep", brew: "semgrep", winget: null, scoop: null, pip: "semgrep" },
|
|
@@ -6728,9 +7072,9 @@ var SECURITY_TOOLS = [
|
|
|
6728
7072
|
{ name: "trufflehog", command: "trufflehog", brew: "trufflehog", winget: null, scoop: "trufflehog", pip: null }
|
|
6729
7073
|
];
|
|
6730
7074
|
var IS_WIN = process.platform === "win32";
|
|
6731
|
-
function
|
|
6732
|
-
return new Promise((
|
|
6733
|
-
const child =
|
|
7075
|
+
function runCommand2(cmd, args) {
|
|
7076
|
+
return new Promise((resolve10) => {
|
|
7077
|
+
const child = spawn5(cmd, args, {
|
|
6734
7078
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6735
7079
|
shell: IS_WIN
|
|
6736
7080
|
// required for .cmd/.ps1 wrappers on Windows
|
|
@@ -6743,19 +7087,19 @@ function runCommand(cmd, args) {
|
|
|
6743
7087
|
child.stderr.on("data", (d) => {
|
|
6744
7088
|
stderr += d.toString();
|
|
6745
7089
|
});
|
|
6746
|
-
child.on("error", () =>
|
|
6747
|
-
child.on("close", (code) =>
|
|
7090
|
+
child.on("error", () => resolve10({ exitCode: 127, stdout, stderr }));
|
|
7091
|
+
child.on("close", (code) => resolve10({ exitCode: code ?? 1, stdout, stderr }));
|
|
6748
7092
|
});
|
|
6749
7093
|
}
|
|
6750
7094
|
async function commandExists(command) {
|
|
6751
7095
|
const checker = IS_WIN ? "where" : "which";
|
|
6752
|
-
const { exitCode } = await
|
|
7096
|
+
const { exitCode } = await runCommand2(checker, [command]);
|
|
6753
7097
|
return exitCode === 0;
|
|
6754
7098
|
}
|
|
6755
7099
|
async function tryInstall(tool, strategies, log) {
|
|
6756
7100
|
for (const { pm, args } of strategies) {
|
|
6757
7101
|
log(chalk5.dim(` ${pm} ${args.join(" ")}\u2026`));
|
|
6758
|
-
const { exitCode, stderr } = await
|
|
7102
|
+
const { exitCode, stderr } = await runCommand2(pm, args);
|
|
6759
7103
|
if (exitCode === 0) {
|
|
6760
7104
|
log(` ${chalk5.green("\u2714")} ${tool.name} installed via ${pm}`);
|
|
6761
7105
|
return { ok: true, pm };
|
|
@@ -6840,20 +7184,26 @@ function buildDefs() {
|
|
|
6840
7184
|
function generateWorkspaceRelationshipMermaid(projects) {
|
|
6841
7185
|
const lines = ["flowchart LR"];
|
|
6842
7186
|
const byPath = new Map(projects.map((p) => [p.path, p]));
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
|
|
6846
|
-
lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
|
|
6847
|
-
}
|
|
7187
|
+
const edges = [];
|
|
7188
|
+
const connectedIds = /* @__PURE__ */ new Set();
|
|
6848
7189
|
for (const project of projects) {
|
|
6849
7190
|
const fromId = sanitizeId(project.projectId || project.path || project.name);
|
|
6850
7191
|
for (const ref of project.projectReferences ?? []) {
|
|
6851
7192
|
const target = byPath.get(ref.path);
|
|
6852
7193
|
if (!target) continue;
|
|
6853
7194
|
const toId = sanitizeId(target.projectId || target.path || target.name);
|
|
6854
|
-
|
|
7195
|
+
edges.push(`${fromId} --> ${toId}`);
|
|
7196
|
+
connectedIds.add(fromId);
|
|
7197
|
+
connectedIds.add(toId);
|
|
6855
7198
|
}
|
|
6856
7199
|
}
|
|
7200
|
+
for (const project of projects) {
|
|
7201
|
+
const id = sanitizeId(project.projectId || project.path || project.name);
|
|
7202
|
+
if (!connectedIds.has(id)) continue;
|
|
7203
|
+
lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
|
|
7204
|
+
lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
|
|
7205
|
+
}
|
|
7206
|
+
lines.push(...edges);
|
|
6857
7207
|
lines.push(...buildDefs());
|
|
6858
7208
|
return { mermaid: lines.join("\n") };
|
|
6859
7209
|
}
|
|
@@ -6880,21 +7230,94 @@ function generateProjectRelationshipMermaid(project, projects) {
|
|
|
6880
7230
|
lines.push(...buildDefs());
|
|
6881
7231
|
return { mermaid: lines.join("\n") };
|
|
6882
7232
|
}
|
|
7233
|
+
function generateSolutionRelationshipMermaid(solution, projects) {
|
|
7234
|
+
const lines = ["flowchart TB"];
|
|
7235
|
+
const solutionNodeId = sanitizeId(solution.solutionId || solution.path || solution.name);
|
|
7236
|
+
const solutionScore = solution.drift?.score;
|
|
7237
|
+
const solutionScoreText = typeof solutionScore === "number" ? ` (${solutionScore})` : " (n/a)";
|
|
7238
|
+
lines.push(`${solutionNodeId}["${escapeLabel(`${solution.name}${solutionScoreText}`)}"]`);
|
|
7239
|
+
lines.push(`class ${solutionNodeId} ${scoreClass(solutionScore)}`);
|
|
7240
|
+
const projectByPath = new Map(projects.map((p) => [p.path, p]));
|
|
7241
|
+
for (const projectPath of solution.projectPaths) {
|
|
7242
|
+
const project = projectByPath.get(projectPath);
|
|
7243
|
+
if (!project) continue;
|
|
7244
|
+
const projectNodeId = sanitizeId(project.projectId || project.path || project.name);
|
|
7245
|
+
lines.push(`${projectNodeId}["${escapeLabel(nodeLabel(project))}"]`);
|
|
7246
|
+
lines.push(`class ${projectNodeId} ${scoreClass(project.drift?.score)}`);
|
|
7247
|
+
lines.push(`${solutionNodeId} --> ${projectNodeId}`);
|
|
7248
|
+
for (const ref of project.projectReferences ?? []) {
|
|
7249
|
+
const target = projectByPath.get(ref.path);
|
|
7250
|
+
if (!target) continue;
|
|
7251
|
+
const toId = sanitizeId(target.projectId || target.path || target.name);
|
|
7252
|
+
lines.push(`${projectNodeId} --> ${toId}`);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
lines.push(...buildDefs());
|
|
7256
|
+
return { mermaid: lines.join("\n") };
|
|
7257
|
+
}
|
|
6883
7258
|
|
|
6884
7259
|
// src/commands/scan.ts
|
|
7260
|
+
async function discoverSolutions(rootDir, fileCache) {
|
|
7261
|
+
const solutionFiles = await fileCache.findSolutionFiles(rootDir);
|
|
7262
|
+
const parsed = [];
|
|
7263
|
+
for (const solutionFile of solutionFiles) {
|
|
7264
|
+
try {
|
|
7265
|
+
const content = await fileCache.readTextFile(solutionFile);
|
|
7266
|
+
const dir = path22.dirname(solutionFile);
|
|
7267
|
+
const relSolutionPath = path22.relative(rootDir, solutionFile).replace(/\\/g, "/");
|
|
7268
|
+
const projectPaths = /* @__PURE__ */ new Set();
|
|
7269
|
+
const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.csproj)"/g;
|
|
7270
|
+
let match;
|
|
7271
|
+
while ((match = projectRegex.exec(content)) !== null) {
|
|
7272
|
+
const projectRelative = match[2];
|
|
7273
|
+
const absProjectPath = path22.resolve(dir, projectRelative.replace(/\\/g, "/"));
|
|
7274
|
+
projectPaths.add(path22.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
|
|
7275
|
+
}
|
|
7276
|
+
const solutionName = path22.basename(solutionFile, path22.extname(solutionFile));
|
|
7277
|
+
parsed.push({
|
|
7278
|
+
path: relSolutionPath,
|
|
7279
|
+
name: solutionName,
|
|
7280
|
+
type: "dotnet-sln",
|
|
7281
|
+
projectPaths: [...projectPaths]
|
|
7282
|
+
});
|
|
7283
|
+
} catch {
|
|
7284
|
+
}
|
|
7285
|
+
}
|
|
7286
|
+
return parsed;
|
|
7287
|
+
}
|
|
6885
7288
|
async function runScan(rootDir, opts) {
|
|
6886
7289
|
const scanStart = Date.now();
|
|
6887
7290
|
const config = await loadConfig(rootDir);
|
|
6888
7291
|
const sem = new Semaphore(opts.concurrency);
|
|
6889
|
-
const
|
|
6890
|
-
const
|
|
6891
|
-
const
|
|
6892
|
-
const
|
|
7292
|
+
const packageManifest = opts.packageManifest ? await loadPackageVersionManifest(opts.packageManifest) : void 0;
|
|
7293
|
+
const offlineMode = opts.offline === true;
|
|
7294
|
+
const maxPrivacyMode = opts.maxPrivacy === true;
|
|
7295
|
+
const npmCache = new NpmCache(rootDir, sem, packageManifest, offlineMode);
|
|
7296
|
+
const nugetCache = new NuGetCache(sem, packageManifest, offlineMode);
|
|
7297
|
+
const pypiCache = new PyPICache(sem, packageManifest, offlineMode);
|
|
7298
|
+
const mavenCache = new MavenCache(sem, packageManifest, offlineMode);
|
|
6893
7299
|
const fileCache = new FileCache();
|
|
6894
7300
|
const excludePatterns = config.exclude ?? [];
|
|
6895
7301
|
fileCache.setExcludePatterns(excludePatterns);
|
|
6896
7302
|
fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
|
|
6897
7303
|
const scanners = config.scanners;
|
|
7304
|
+
const scannerPolicy = {
|
|
7305
|
+
platformMatrix: !maxPrivacyMode,
|
|
7306
|
+
toolingInventory: true,
|
|
7307
|
+
serviceDependencies: !maxPrivacyMode,
|
|
7308
|
+
breakingChangeExposure: !maxPrivacyMode,
|
|
7309
|
+
securityPosture: true,
|
|
7310
|
+
securityScanners: !maxPrivacyMode,
|
|
7311
|
+
buildDeploy: !maxPrivacyMode,
|
|
7312
|
+
tsModernity: !maxPrivacyMode,
|
|
7313
|
+
fileHotspots: !maxPrivacyMode,
|
|
7314
|
+
dependencyGraph: true,
|
|
7315
|
+
dependencyRisk: true,
|
|
7316
|
+
architecture: !maxPrivacyMode,
|
|
7317
|
+
codeQuality: !maxPrivacyMode,
|
|
7318
|
+
owaspCategoryMapping: !maxPrivacyMode,
|
|
7319
|
+
uiPurpose: !maxPrivacyMode
|
|
7320
|
+
};
|
|
6898
7321
|
let filesScanned = 0;
|
|
6899
7322
|
const progress = new ScanProgress(rootDir);
|
|
6900
7323
|
const steps = [
|
|
@@ -6906,29 +7329,30 @@ async function runScan(rootDir, opts) {
|
|
|
6906
7329
|
{ id: "dotnet", label: "Scanning .NET projects", weight: 2 },
|
|
6907
7330
|
{ id: "python", label: "Scanning Python projects", weight: 3 },
|
|
6908
7331
|
{ id: "java", label: "Scanning Java projects", weight: 3 },
|
|
7332
|
+
{ id: "polyglot", label: "Scanning additional language projects", weight: 2 },
|
|
6909
7333
|
...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
|
-
|
|
7334
|
+
...scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
7335
|
+
...scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
7336
|
+
...scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
|
|
7337
|
+
...scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
|
|
7338
|
+
...scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
|
|
7339
|
+
...scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
|
|
7340
|
+
...scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
|
|
7341
|
+
...scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
7342
|
+
...scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
7343
|
+
...scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
7344
|
+
...scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
7345
|
+
...scannerPolicy.architecture && scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
|
|
7346
|
+
...scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
|
|
7347
|
+
...scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
|
|
7348
|
+
...!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
|
|
6925
7349
|
] : [],
|
|
6926
7350
|
{ id: "drift", label: "Computing drift score" },
|
|
6927
7351
|
{ id: "findings", label: "Generating findings" }
|
|
6928
7352
|
];
|
|
6929
7353
|
progress.setSteps(steps);
|
|
6930
7354
|
progress.completeStep("config", "loaded");
|
|
6931
|
-
const registryOk = await checkRegistryAccess(rootDir);
|
|
7355
|
+
const registryOk = offlineMode ? true : await checkRegistryAccess(rootDir);
|
|
6932
7356
|
if (!registryOk) {
|
|
6933
7357
|
progress.finish();
|
|
6934
7358
|
const msg = [
|
|
@@ -7002,7 +7426,16 @@ async function runScan(rootDir, opts) {
|
|
|
7002
7426
|
filesScanned += javaProjects.length;
|
|
7003
7427
|
progress.addProjects(javaProjects.length);
|
|
7004
7428
|
progress.completeStep("java", `${javaProjects.length} project${javaProjects.length !== 1 ? "s" : ""}`, javaProjects.length);
|
|
7005
|
-
|
|
7429
|
+
progress.startStep("polyglot");
|
|
7430
|
+
const polyglotProjects = await scanPolyglotProjects(rootDir, fileCache);
|
|
7431
|
+
for (const p of polyglotProjects) {
|
|
7432
|
+
progress.addDependencies(p.dependencies.length);
|
|
7433
|
+
progress.addFrameworks(p.frameworks.length);
|
|
7434
|
+
}
|
|
7435
|
+
filesScanned += polyglotProjects.length;
|
|
7436
|
+
progress.addProjects(polyglotProjects.length);
|
|
7437
|
+
progress.completeStep("polyglot", `${polyglotProjects.length} project${polyglotProjects.length !== 1 ? "s" : ""}`, polyglotProjects.length);
|
|
7438
|
+
const allProjects = [...nodeProjects, ...dotnetProjects, ...pythonProjects, ...javaProjects, ...polyglotProjects];
|
|
7006
7439
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
7007
7440
|
const parsedDsn = dsn ? parseDsn(dsn) : null;
|
|
7008
7441
|
const workspaceId = parsedDsn?.workspaceId;
|
|
@@ -7010,14 +7443,45 @@ async function runScan(rootDir, opts) {
|
|
|
7010
7443
|
project.drift = computeDriftScore([project]);
|
|
7011
7444
|
project.projectId = computeProjectId(project.path, project.name, workspaceId);
|
|
7012
7445
|
}
|
|
7446
|
+
const solutionsManifestPath = path22.join(rootDir, ".vibgrate", "solutions.json");
|
|
7447
|
+
const persistedSolutionIds = /* @__PURE__ */ new Map();
|
|
7448
|
+
if (await pathExists(solutionsManifestPath)) {
|
|
7449
|
+
try {
|
|
7450
|
+
const persisted = await readJsonFile(solutionsManifestPath);
|
|
7451
|
+
for (const solution of persisted.solutions ?? []) {
|
|
7452
|
+
if (solution.path && solution.solutionId) persistedSolutionIds.set(solution.path, solution.solutionId);
|
|
7453
|
+
}
|
|
7454
|
+
} catch {
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
const discoveredSolutions = await discoverSolutions(rootDir, fileCache);
|
|
7458
|
+
const solutions = discoveredSolutions.map((solution) => ({
|
|
7459
|
+
solutionId: persistedSolutionIds.get(solution.path) ?? computeSolutionId(solution.path, solution.name, workspaceId),
|
|
7460
|
+
path: solution.path,
|
|
7461
|
+
name: solution.name,
|
|
7462
|
+
type: solution.type,
|
|
7463
|
+
projectPaths: solution.projectPaths
|
|
7464
|
+
}));
|
|
7465
|
+
const projectsByPath = new Map(allProjects.map((project) => [project.path, project]));
|
|
7466
|
+
for (const solution of solutions) {
|
|
7467
|
+
const includedProjects = solution.projectPaths.map((projectPath) => projectsByPath.get(projectPath)).filter((project) => Boolean(project));
|
|
7468
|
+
solution.drift = includedProjects.length > 0 ? computeDriftScore(includedProjects) : void 0;
|
|
7469
|
+
for (const project of includedProjects) {
|
|
7470
|
+
project.solutionId = solution.solutionId;
|
|
7471
|
+
project.solutionName = solution.name;
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7013
7474
|
for (const project of allProjects) {
|
|
7014
7475
|
project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
|
|
7015
7476
|
}
|
|
7477
|
+
for (const solution of solutions) {
|
|
7478
|
+
solution.relationshipDiagram = generateSolutionRelationshipMermaid(solution, allProjects);
|
|
7479
|
+
}
|
|
7016
7480
|
const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
|
|
7017
7481
|
const extended = {};
|
|
7018
7482
|
if (scanners !== false) {
|
|
7019
7483
|
const scannerTasks = [];
|
|
7020
|
-
if (scanners?.platformMatrix?.enabled !== false) {
|
|
7484
|
+
if (scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false) {
|
|
7021
7485
|
progress.startStep("platform");
|
|
7022
7486
|
scannerTasks.push(
|
|
7023
7487
|
scanPlatformMatrix(rootDir, fileCache).then((result) => {
|
|
@@ -7031,7 +7495,7 @@ async function runScan(rootDir, opts) {
|
|
|
7031
7495
|
})
|
|
7032
7496
|
);
|
|
7033
7497
|
}
|
|
7034
|
-
if (scanners?.toolingInventory?.enabled !== false) {
|
|
7498
|
+
if (scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false) {
|
|
7035
7499
|
progress.startStep("tooling");
|
|
7036
7500
|
scannerTasks.push(
|
|
7037
7501
|
Promise.resolve().then(() => {
|
|
@@ -7041,7 +7505,7 @@ async function runScan(rootDir, opts) {
|
|
|
7041
7505
|
})
|
|
7042
7506
|
);
|
|
7043
7507
|
}
|
|
7044
|
-
if (scanners?.serviceDependencies?.enabled !== false) {
|
|
7508
|
+
if (scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false) {
|
|
7045
7509
|
progress.startStep("services");
|
|
7046
7510
|
scannerTasks.push(
|
|
7047
7511
|
Promise.resolve().then(() => {
|
|
@@ -7051,7 +7515,7 @@ async function runScan(rootDir, opts) {
|
|
|
7051
7515
|
})
|
|
7052
7516
|
);
|
|
7053
7517
|
}
|
|
7054
|
-
if (scanners?.breakingChangeExposure?.enabled !== false) {
|
|
7518
|
+
if (scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false) {
|
|
7055
7519
|
progress.startStep("breaking");
|
|
7056
7520
|
scannerTasks.push(
|
|
7057
7521
|
Promise.resolve().then(() => {
|
|
@@ -7066,7 +7530,7 @@ async function runScan(rootDir, opts) {
|
|
|
7066
7530
|
})
|
|
7067
7531
|
);
|
|
7068
7532
|
}
|
|
7069
|
-
if (scanners?.securityPosture?.enabled !== false) {
|
|
7533
|
+
if (scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false) {
|
|
7070
7534
|
progress.startStep("security");
|
|
7071
7535
|
scannerTasks.push(
|
|
7072
7536
|
scanSecurityPosture(rootDir, fileCache).then((result) => {
|
|
@@ -7076,7 +7540,7 @@ async function runScan(rootDir, opts) {
|
|
|
7076
7540
|
})
|
|
7077
7541
|
);
|
|
7078
7542
|
}
|
|
7079
|
-
if (scanners?.securityScanners?.enabled !== false) {
|
|
7543
|
+
if (scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false) {
|
|
7080
7544
|
if (opts.installTools) {
|
|
7081
7545
|
const installResult = await installMissingTools();
|
|
7082
7546
|
if (installResult.installed.length > 0) {
|
|
@@ -7099,7 +7563,7 @@ async function runScan(rootDir, opts) {
|
|
|
7099
7563
|
})
|
|
7100
7564
|
);
|
|
7101
7565
|
}
|
|
7102
|
-
if (scanners?.buildDeploy?.enabled !== false) {
|
|
7566
|
+
if (scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false) {
|
|
7103
7567
|
progress.startStep("build");
|
|
7104
7568
|
scannerTasks.push(
|
|
7105
7569
|
scanBuildDeploy(rootDir, fileCache).then((result) => {
|
|
@@ -7111,7 +7575,7 @@ async function runScan(rootDir, opts) {
|
|
|
7111
7575
|
})
|
|
7112
7576
|
);
|
|
7113
7577
|
}
|
|
7114
|
-
if (scanners?.tsModernity?.enabled !== false) {
|
|
7578
|
+
if (scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false) {
|
|
7115
7579
|
progress.startStep("ts");
|
|
7116
7580
|
scannerTasks.push(
|
|
7117
7581
|
scanTsModernity(rootDir, fileCache).then((result) => {
|
|
@@ -7124,7 +7588,7 @@ async function runScan(rootDir, opts) {
|
|
|
7124
7588
|
})
|
|
7125
7589
|
);
|
|
7126
7590
|
}
|
|
7127
|
-
if (scanners?.fileHotspots?.enabled !== false) {
|
|
7591
|
+
if (scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false) {
|
|
7128
7592
|
progress.startStep("hotspots");
|
|
7129
7593
|
scannerTasks.push(
|
|
7130
7594
|
scanFileHotspots(rootDir, fileCache).then((result) => {
|
|
@@ -7133,7 +7597,7 @@ async function runScan(rootDir, opts) {
|
|
|
7133
7597
|
})
|
|
7134
7598
|
);
|
|
7135
7599
|
}
|
|
7136
|
-
if (scanners?.dependencyGraph?.enabled !== false) {
|
|
7600
|
+
if (scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false) {
|
|
7137
7601
|
progress.startStep("depgraph");
|
|
7138
7602
|
scannerTasks.push(
|
|
7139
7603
|
scanDependencyGraph(rootDir, fileCache).then((result) => {
|
|
@@ -7143,7 +7607,7 @@ async function runScan(rootDir, opts) {
|
|
|
7143
7607
|
})
|
|
7144
7608
|
);
|
|
7145
7609
|
}
|
|
7146
|
-
if (scanners?.codeQuality?.enabled !== false) {
|
|
7610
|
+
if (scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false) {
|
|
7147
7611
|
progress.startStep("codequality");
|
|
7148
7612
|
scannerTasks.push(
|
|
7149
7613
|
scanCodeQuality(rootDir, fileCache).then((result) => {
|
|
@@ -7156,7 +7620,7 @@ async function runScan(rootDir, opts) {
|
|
|
7156
7620
|
})
|
|
7157
7621
|
);
|
|
7158
7622
|
}
|
|
7159
|
-
if (scanners?.dependencyRisk?.enabled !== false) {
|
|
7623
|
+
if (scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false) {
|
|
7160
7624
|
progress.startStep("deprisk");
|
|
7161
7625
|
scannerTasks.push(
|
|
7162
7626
|
Promise.resolve().then(() => {
|
|
@@ -7170,7 +7634,7 @@ async function runScan(rootDir, opts) {
|
|
|
7170
7634
|
);
|
|
7171
7635
|
}
|
|
7172
7636
|
await Promise.all(scannerTasks);
|
|
7173
|
-
if (scanners?.owaspCategoryMapping?.enabled !== false) {
|
|
7637
|
+
if (scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false) {
|
|
7174
7638
|
progress.startStep("owasp");
|
|
7175
7639
|
extended.owaspCategoryMapping = await scanOwaspCategoryMapping(
|
|
7176
7640
|
rootDir,
|
|
@@ -7189,14 +7653,14 @@ async function runScan(rootDir, opts) {
|
|
|
7189
7653
|
);
|
|
7190
7654
|
}
|
|
7191
7655
|
}
|
|
7192
|
-
if (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) {
|
|
7656
|
+
if (!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true)) {
|
|
7193
7657
|
progress.startStep("uipurpose");
|
|
7194
7658
|
extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
|
|
7195
7659
|
const up = extended.uiPurpose;
|
|
7196
7660
|
const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
|
|
7197
7661
|
progress.completeStep("uipurpose", summary, up.topEvidence.length);
|
|
7198
7662
|
}
|
|
7199
|
-
if (scanners?.architecture?.enabled !== false) {
|
|
7663
|
+
if (scannerPolicy.architecture && scanners?.architecture?.enabled !== false) {
|
|
7200
7664
|
progress.startStep("architecture");
|
|
7201
7665
|
extended.architecture = await scanArchitecture(
|
|
7202
7666
|
rootDir,
|
|
@@ -7284,13 +7748,16 @@ async function runScan(rootDir, opts) {
|
|
|
7284
7748
|
if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
|
|
7285
7749
|
if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
|
|
7286
7750
|
const durationMs = Date.now() - scanStart;
|
|
7751
|
+
const repository = await buildRepositoryInfo(rootDir, vcs.remoteUrl, extended.buildDeploy?.ci);
|
|
7287
7752
|
const artifact = {
|
|
7288
7753
|
schemaVersion: "1.0",
|
|
7289
7754
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7290
7755
|
vibgrateVersion: VERSION,
|
|
7291
|
-
rootPath:
|
|
7756
|
+
rootPath: path22.basename(rootDir),
|
|
7292
7757
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
7758
|
+
repository,
|
|
7293
7759
|
projects: allProjects,
|
|
7760
|
+
...solutions.length > 0 ? { solutions } : {},
|
|
7294
7761
|
drift,
|
|
7295
7762
|
findings,
|
|
7296
7763
|
...Object.keys(extended).length > 0 ? { extended } : {},
|
|
@@ -7300,7 +7767,7 @@ async function runScan(rootDir, opts) {
|
|
|
7300
7767
|
relationshipDiagram
|
|
7301
7768
|
};
|
|
7302
7769
|
if (opts.baseline) {
|
|
7303
|
-
const baselinePath =
|
|
7770
|
+
const baselinePath = path22.resolve(opts.baseline);
|
|
7304
7771
|
if (await pathExists(baselinePath)) {
|
|
7305
7772
|
try {
|
|
7306
7773
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -7311,9 +7778,21 @@ async function runScan(rootDir, opts) {
|
|
|
7311
7778
|
}
|
|
7312
7779
|
}
|
|
7313
7780
|
}
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7781
|
+
if (!opts.noLocalArtifacts && !maxPrivacyMode) {
|
|
7782
|
+
const vibgrateDir = path22.join(rootDir, ".vibgrate");
|
|
7783
|
+
await ensureDir(vibgrateDir);
|
|
7784
|
+
await writeJsonFile(path22.join(vibgrateDir, "scan_result.json"), artifact);
|
|
7785
|
+
await writeJsonFile(path22.join(vibgrateDir, "solutions.json"), {
|
|
7786
|
+
scannedAt: artifact.timestamp,
|
|
7787
|
+
solutions: solutions.map((solution) => ({
|
|
7788
|
+
solutionId: solution.solutionId,
|
|
7789
|
+
name: solution.name,
|
|
7790
|
+
path: solution.path,
|
|
7791
|
+
type: solution.type,
|
|
7792
|
+
projectPaths: solution.projectPaths
|
|
7793
|
+
}))
|
|
7794
|
+
});
|
|
7795
|
+
}
|
|
7317
7796
|
await saveScanHistory(rootDir, {
|
|
7318
7797
|
timestamp: artifact.timestamp,
|
|
7319
7798
|
totalDurationMs: durationMs,
|
|
@@ -7321,29 +7800,33 @@ async function runScan(rootDir, opts) {
|
|
|
7321
7800
|
totalDirs: treeCount.totalDirs,
|
|
7322
7801
|
steps: progress.getStepTimings()
|
|
7323
7802
|
});
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7803
|
+
if (!opts.noLocalArtifacts && !maxPrivacyMode) {
|
|
7804
|
+
for (const project of allProjects) {
|
|
7805
|
+
if (project.drift && project.path) {
|
|
7806
|
+
const projectDir = path22.resolve(rootDir, project.path);
|
|
7807
|
+
const projectVibgrateDir = path22.join(projectDir, ".vibgrate");
|
|
7808
|
+
await ensureDir(projectVibgrateDir);
|
|
7809
|
+
await writeJsonFile(path22.join(projectVibgrateDir, "project_score.json"), {
|
|
7810
|
+
projectId: project.projectId,
|
|
7811
|
+
name: project.name,
|
|
7812
|
+
type: project.type,
|
|
7813
|
+
path: project.path,
|
|
7814
|
+
score: project.drift.score,
|
|
7815
|
+
riskLevel: project.drift.riskLevel,
|
|
7816
|
+
components: project.drift.components,
|
|
7817
|
+
measured: project.drift.measured,
|
|
7818
|
+
scannedAt: artifact.timestamp,
|
|
7819
|
+
vibgrateVersion: VERSION,
|
|
7820
|
+
solutionId: project.solutionId,
|
|
7821
|
+
solutionName: project.solutionName
|
|
7822
|
+
});
|
|
7823
|
+
}
|
|
7341
7824
|
}
|
|
7342
7825
|
}
|
|
7343
7826
|
if (opts.format === "json") {
|
|
7344
7827
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
7345
7828
|
if (opts.out) {
|
|
7346
|
-
await writeTextFile(
|
|
7829
|
+
await writeTextFile(path22.resolve(opts.out), jsonStr);
|
|
7347
7830
|
console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
7348
7831
|
} else {
|
|
7349
7832
|
console.log(jsonStr);
|
|
@@ -7352,7 +7835,7 @@ async function runScan(rootDir, opts) {
|
|
|
7352
7835
|
const sarif = formatSarif(artifact);
|
|
7353
7836
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
7354
7837
|
if (opts.out) {
|
|
7355
|
-
await writeTextFile(
|
|
7838
|
+
await writeTextFile(path22.resolve(opts.out), sarifStr);
|
|
7356
7839
|
console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
7357
7840
|
} else {
|
|
7358
7841
|
console.log(sarifStr);
|
|
@@ -7361,11 +7844,34 @@ async function runScan(rootDir, opts) {
|
|
|
7361
7844
|
const text = formatText(artifact);
|
|
7362
7845
|
console.log(text);
|
|
7363
7846
|
if (opts.out) {
|
|
7364
|
-
await writeTextFile(
|
|
7847
|
+
await writeTextFile(path22.resolve(opts.out), text);
|
|
7365
7848
|
}
|
|
7366
7849
|
}
|
|
7367
7850
|
return artifact;
|
|
7368
7851
|
}
|
|
7852
|
+
async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
|
|
7853
|
+
const packageJsonPath = path22.join(rootDir, "package.json");
|
|
7854
|
+
let name = path22.basename(rootDir);
|
|
7855
|
+
let version;
|
|
7856
|
+
if (await pathExists(packageJsonPath)) {
|
|
7857
|
+
try {
|
|
7858
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
7859
|
+
if (typeof packageJson.name === "string" && packageJson.name.trim()) {
|
|
7860
|
+
name = packageJson.name.trim();
|
|
7861
|
+
}
|
|
7862
|
+
if (typeof packageJson.version === "string" && packageJson.version.trim()) {
|
|
7863
|
+
version = packageJson.version.trim();
|
|
7864
|
+
}
|
|
7865
|
+
} catch {
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
return {
|
|
7869
|
+
name,
|
|
7870
|
+
...version ? { version } : {},
|
|
7871
|
+
...ciSystems && ciSystems.length > 0 ? { pipeline: ciSystems.join(",") } : {},
|
|
7872
|
+
...remoteUrl ? { remoteUrl } : {}
|
|
7873
|
+
};
|
|
7874
|
+
}
|
|
7369
7875
|
async function autoPush(artifact, rootDir, opts) {
|
|
7370
7876
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
7371
7877
|
if (!dsn) {
|
|
@@ -7436,8 +7942,8 @@ function parseNonNegativeNumber(value, label) {
|
|
|
7436
7942
|
}
|
|
7437
7943
|
return parsed;
|
|
7438
7944
|
}
|
|
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 =
|
|
7945
|
+
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) => {
|
|
7946
|
+
const rootDir = path22.resolve(targetPath);
|
|
7441
7947
|
if (!await pathExists(rootDir)) {
|
|
7442
7948
|
console.error(chalk6.red(`Path does not exist: ${rootDir}`));
|
|
7443
7949
|
process.exit(1);
|
|
@@ -7455,6 +7961,10 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
7455
7961
|
strict: opts.strict,
|
|
7456
7962
|
installTools: opts.installTools,
|
|
7457
7963
|
uiPurpose: opts.uiPurpose,
|
|
7964
|
+
noLocalArtifacts: opts.noLocalArtifacts,
|
|
7965
|
+
maxPrivacy: opts.maxPrivacy,
|
|
7966
|
+
offline: opts.offline,
|
|
7967
|
+
packageManifest: opts.packageManifest,
|
|
7458
7968
|
driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
|
|
7459
7969
|
driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
|
|
7460
7970
|
};
|
|
@@ -7495,7 +8005,7 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
|
|
|
7495
8005
|
}
|
|
7496
8006
|
}
|
|
7497
8007
|
const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
|
|
7498
|
-
if (opts.push || hasDsn) {
|
|
8008
|
+
if (!scanOpts.offline && (opts.push || hasDsn)) {
|
|
7499
8009
|
await autoPush(artifact, rootDir, scanOpts);
|
|
7500
8010
|
}
|
|
7501
8011
|
});
|