@vibgrate/cli 1.0.45 → 1.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +293 -95
- package/README.md +212 -206
- package/dist/{baseline-FDWMBM2O.js → baseline-K7V6GAP3.js} +2 -2
- package/dist/{chunk-LO66M6OC.js → chunk-NASGRGXK.js} +1 -1
- package/dist/{chunk-GN3IWKSY.js → chunk-PTMLMDZU.js} +20 -0
- package/dist/{chunk-YFJC5JSQ.js → chunk-UVFIFNYG.js} +966 -305
- package/dist/cli.js +190 -10
- package/dist/index.d.ts +69 -0
- package/dist/index.js +2 -2
- 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";
|
|
@@ -315,6 +319,27 @@ function formatText(artifact) {
|
|
|
315
319
|
if (artifact.extended?.architecture) {
|
|
316
320
|
lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
|
|
317
321
|
}
|
|
322
|
+
if (artifact.relationshipDiagram?.mermaid) {
|
|
323
|
+
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"));
|
|
324
|
+
lines.push(chalk.bold.cyan("\u2551 Project Relationship Diagram \u2551"));
|
|
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"));
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push(chalk.bold(" Mermaid"));
|
|
328
|
+
lines.push(chalk.dim(artifact.relationshipDiagram.mermaid));
|
|
329
|
+
lines.push("");
|
|
330
|
+
}
|
|
331
|
+
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
332
|
+
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
333
|
+
lines.push(chalk.bold.cyan("\u2551 Solution Drift Summary \u2551"));
|
|
334
|
+
lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
335
|
+
lines.push("");
|
|
336
|
+
for (const solution of artifact.solutions) {
|
|
337
|
+
const solScore = solution.drift?.score;
|
|
338
|
+
const color = typeof solScore === "number" ? solScore >= 70 ? chalk.green : solScore >= 40 ? chalk.yellow : chalk.red : chalk.dim;
|
|
339
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk.dim("n/a")}`);
|
|
340
|
+
}
|
|
341
|
+
lines.push("");
|
|
342
|
+
}
|
|
318
343
|
const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
|
|
319
344
|
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
320
345
|
lines.push(chalk.bold.cyan("\u2551 Drift Score Summary \u2551"));
|
|
@@ -469,6 +494,26 @@ function formatExtended(ext) {
|
|
|
469
494
|
lines.push("");
|
|
470
495
|
}
|
|
471
496
|
}
|
|
497
|
+
if (ext.uiPurpose) {
|
|
498
|
+
const up = ext.uiPurpose;
|
|
499
|
+
lines.push(chalk.bold.underline(" Product Purpose Signals"));
|
|
500
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk.dim("unknown")}`);
|
|
501
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
502
|
+
const top = up.topEvidence.slice(0, 8);
|
|
503
|
+
if (top.length > 0) {
|
|
504
|
+
lines.push(" Top Signals:");
|
|
505
|
+
for (const item of top) {
|
|
506
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk.dim(`(${item.file})`)}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (up.unknownSignals.length > 0) {
|
|
510
|
+
lines.push(" Unknowns:");
|
|
511
|
+
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
512
|
+
lines.push(` - ${chalk.yellow(u)}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
lines.push("");
|
|
516
|
+
}
|
|
472
517
|
if (ext.owaspCategoryMapping) {
|
|
473
518
|
const ow = ext.owaspCategoryMapping;
|
|
474
519
|
lines.push(chalk.bold.underline(" OWASP Category Mapping"));
|
|
@@ -565,125 +610,22 @@ function formatExtended(ext) {
|
|
|
565
610
|
}
|
|
566
611
|
return lines;
|
|
567
612
|
}
|
|
568
|
-
var LAYER_LABELS = {
|
|
569
|
-
"presentation": "Presentation",
|
|
570
|
-
"routing": "Routing",
|
|
571
|
-
"middleware": "Middleware",
|
|
572
|
-
"services": "Services",
|
|
573
|
-
"domain": "Domain",
|
|
574
|
-
"data-access": "Data Access",
|
|
575
|
-
"infrastructure": "Infrastructure",
|
|
576
|
-
"config": "Config",
|
|
577
|
-
"shared": "Shared",
|
|
578
|
-
"testing": "Testing"
|
|
579
|
-
};
|
|
580
|
-
var LAYER_ICONS = {
|
|
581
|
-
"presentation": "\u{1F5A5}",
|
|
582
|
-
"routing": "\u{1F500}",
|
|
583
|
-
"middleware": "\u{1F517}",
|
|
584
|
-
"services": "\u2699",
|
|
585
|
-
"domain": "\u{1F48E}",
|
|
586
|
-
"data-access": "\u{1F5C4}",
|
|
587
|
-
"infrastructure": "\u{1F3D7}",
|
|
588
|
-
"config": "\u2699",
|
|
589
|
-
"shared": "\u{1F4E6}",
|
|
590
|
-
"testing": "\u{1F9EA}"
|
|
591
|
-
};
|
|
592
613
|
function formatArchitectureDiagram(arch) {
|
|
593
614
|
const lines = [];
|
|
594
615
|
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"));
|
|
595
616
|
lines.push(chalk.bold.cyan("\u2551 Architecture Layers \u2551"));
|
|
596
617
|
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"));
|
|
597
618
|
lines.push("");
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
lines.push(chalk.bold(" Archetype: ") + chalk.cyan.bold(archetypeDisplay) + chalk.dim(` (${confPct}% confidence)`));
|
|
601
|
-
lines.push(chalk.dim(` ${arch.totalClassified} files classified \xB7 ${arch.unclassified} unclassified`));
|
|
619
|
+
lines.push(chalk.bold(" Archetype: ") + `${arch.archetype}` + chalk.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
620
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
602
621
|
lines.push("");
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
lines.push("");
|
|
608
|
-
return lines;
|
|
609
|
-
}
|
|
610
|
-
for (let i = 0; i < visibleLayers.length; i++) {
|
|
611
|
-
const layer = visibleLayers[i];
|
|
612
|
-
const icon = LAYER_ICONS[layer.layer] ?? "\xB7";
|
|
613
|
-
const label = LAYER_LABELS[layer.layer] ?? layer.layer;
|
|
614
|
-
const hasScore = layer.packages.length > 0;
|
|
615
|
-
const ds = layer.driftScore;
|
|
616
|
-
const scoreColor = !hasScore ? chalk.dim : ds >= 70 ? chalk.green : ds >= 40 ? chalk.yellow : chalk.red;
|
|
617
|
-
const riskBadgeStr = layer.riskLevel === "low" ? chalk.bgGreen.black(" LOW ") : layer.riskLevel === "moderate" ? chalk.bgYellow.black(" MOD ") : chalk.bgRed.white(" HIGH ");
|
|
618
|
-
if (i === 0) {
|
|
619
|
-
lines.push(chalk.cyan(` \u250C${"\u2500".repeat(boxWidth)}\u2510`));
|
|
620
|
-
}
|
|
621
|
-
const nameStr = `${icon} ${label}`;
|
|
622
|
-
const scoreStr = hasScore ? `${layer.driftScore}/100` : "n/a";
|
|
623
|
-
const fileSuffix = `${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""}`;
|
|
624
|
-
const leftContent = ` ${nameStr}`;
|
|
625
|
-
const rightContent = `${fileSuffix} ${scoreStr} `;
|
|
626
|
-
const leftLen = nameStr.length + 2;
|
|
627
|
-
const rightLen = rightContent.length;
|
|
628
|
-
const padLen = Math.max(1, boxWidth - leftLen - rightLen);
|
|
629
|
-
lines.push(
|
|
630
|
-
chalk.cyan(" \u2502") + ` ${icon} ${chalk.bold(label)}` + " ".repeat(padLen) + chalk.dim(fileSuffix) + " " + scoreColor.bold(scoreStr) + " " + chalk.cyan("\u2502")
|
|
631
|
-
);
|
|
632
|
-
const barWidth = boxWidth - 8;
|
|
633
|
-
if (hasScore) {
|
|
634
|
-
const filled = Math.round(layer.driftScore / 100 * barWidth);
|
|
635
|
-
const empty = barWidth - filled;
|
|
636
|
-
lines.push(
|
|
637
|
-
chalk.cyan(" \u2502") + " " + scoreColor("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + " " + chalk.cyan("\u2502")
|
|
638
|
-
);
|
|
639
|
-
} else {
|
|
640
|
-
lines.push(
|
|
641
|
-
chalk.cyan(" \u2502") + " " + chalk.dim("\xB7".repeat(barWidth)) + " " + chalk.cyan("\u2502")
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
if (layer.techStack.length > 0) {
|
|
645
|
-
const techNames = layer.techStack.slice(0, 6).map((t) => t.name);
|
|
646
|
-
const moreCount = layer.techStack.length > 6 ? ` +${layer.techStack.length - 6}` : "";
|
|
647
|
-
const techLine = `Tech: ${techNames.join(", ")}${moreCount}`;
|
|
648
|
-
const truncated = techLine.length > boxWidth - 6 ? techLine.slice(0, boxWidth - 9) + "..." : techLine;
|
|
649
|
-
const techPad = Math.max(0, boxWidth - truncated.length - 4);
|
|
650
|
-
lines.push(
|
|
651
|
-
chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(techPad) + chalk.cyan("\u2502")
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
if (layer.services.length > 0) {
|
|
655
|
-
const svcNames = layer.services.slice(0, 5).map((s) => s.name);
|
|
656
|
-
const moreCount = layer.services.length > 5 ? ` +${layer.services.length - 5}` : "";
|
|
657
|
-
const svcLine = `Services: ${svcNames.join(", ")}${moreCount}`;
|
|
658
|
-
const truncated = svcLine.length > boxWidth - 6 ? svcLine.slice(0, boxWidth - 9) + "..." : svcLine;
|
|
659
|
-
const svcPad = Math.max(0, boxWidth - truncated.length - 4);
|
|
660
|
-
lines.push(
|
|
661
|
-
chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(svcPad) + chalk.cyan("\u2502")
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
const driftPkgs = layer.packages.filter((p) => p.majorsBehind !== null && p.majorsBehind > 0);
|
|
665
|
-
if (driftPkgs.length > 0) {
|
|
666
|
-
const worst = driftPkgs.sort((a, b) => (b.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
|
|
667
|
-
const shown = worst.slice(0, 3);
|
|
668
|
-
const pkgStrs = shown.map((p) => {
|
|
669
|
-
const color = (p.majorsBehind ?? 0) >= 2 ? chalk.red : chalk.yellow;
|
|
670
|
-
return color(`${p.name} -${p.majorsBehind}`);
|
|
671
|
-
});
|
|
672
|
-
const moreCount = worst.length > 3 ? chalk.dim(` +${worst.length - 3}`) : "";
|
|
673
|
-
const pkgLine = pkgStrs.join(chalk.dim(", ")) + moreCount;
|
|
674
|
-
const roughLen = shown.map((p) => `${p.name} -${p.majorsBehind}`).join(", ").length + (worst.length > 3 ? ` +${worst.length - 3}`.length : 0);
|
|
675
|
-
const pkgPad = Math.max(0, boxWidth - roughLen - 4);
|
|
676
|
-
lines.push(
|
|
677
|
-
chalk.cyan(" \u2502") + " " + pkgLine + " ".repeat(pkgPad) + chalk.cyan("\u2502")
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
if (i < visibleLayers.length - 1) {
|
|
681
|
-
lines.push(chalk.cyan(` \u251C${"\u2500".repeat(boxWidth)}\u2524`));
|
|
682
|
-
} else {
|
|
683
|
-
lines.push(chalk.cyan(` \u2514${"\u2500".repeat(boxWidth)}\u2518`));
|
|
622
|
+
if (arch.layers.length > 0) {
|
|
623
|
+
for (const layer of arch.layers) {
|
|
624
|
+
const risk = layer.riskLevel === "low" ? chalk.green("low") : layer.riskLevel === "moderate" ? chalk.yellow("moderate") : chalk.red("high");
|
|
625
|
+
lines.push(` ${chalk.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
684
626
|
}
|
|
627
|
+
lines.push("");
|
|
685
628
|
}
|
|
686
|
-
lines.push("");
|
|
687
629
|
return lines;
|
|
688
630
|
}
|
|
689
631
|
function generatePriorityActions(artifact) {
|
|
@@ -1158,19 +1100,19 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1158
1100
|
});
|
|
1159
1101
|
|
|
1160
1102
|
// src/commands/scan.ts
|
|
1161
|
-
import * as
|
|
1103
|
+
import * as path21 from "path";
|
|
1162
1104
|
import { Command as Command3 } from "commander";
|
|
1163
1105
|
import chalk6 from "chalk";
|
|
1164
1106
|
|
|
1165
1107
|
// src/scanners/node-scanner.ts
|
|
1166
|
-
import * as
|
|
1108
|
+
import * as path4 from "path";
|
|
1167
1109
|
import * as semver2 from "semver";
|
|
1168
1110
|
|
|
1169
1111
|
// src/utils/timeout.ts
|
|
1170
1112
|
async function withTimeout(promise, ms) {
|
|
1171
1113
|
let timer;
|
|
1172
|
-
const timeout = new Promise((
|
|
1173
|
-
timer = setTimeout(() =>
|
|
1114
|
+
const timeout = new Promise((resolve10) => {
|
|
1115
|
+
timer = setTimeout(() => resolve10({ ok: false }), ms);
|
|
1174
1116
|
});
|
|
1175
1117
|
try {
|
|
1176
1118
|
const result = await Promise.race([
|
|
@@ -1184,8 +1126,78 @@ async function withTimeout(promise, ms) {
|
|
|
1184
1126
|
}
|
|
1185
1127
|
|
|
1186
1128
|
// src/scanners/npm-cache.ts
|
|
1187
|
-
import { spawn } from "child_process";
|
|
1129
|
+
import { spawn as spawn2 } from "child_process";
|
|
1188
1130
|
import * as semver from "semver";
|
|
1131
|
+
|
|
1132
|
+
// src/package-version-manifest.ts
|
|
1133
|
+
import { mkdtemp, readFile, rm } from "fs/promises";
|
|
1134
|
+
import * as path3 from "path";
|
|
1135
|
+
import * as os from "os";
|
|
1136
|
+
import { spawn } from "child_process";
|
|
1137
|
+
function runCommand(cmd, args) {
|
|
1138
|
+
return new Promise((resolve10, reject) => {
|
|
1139
|
+
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1140
|
+
let out = "";
|
|
1141
|
+
let err = "";
|
|
1142
|
+
child.stdout.on("data", (d) => out += String(d));
|
|
1143
|
+
child.stderr.on("data", (d) => err += String(d));
|
|
1144
|
+
child.on("error", reject);
|
|
1145
|
+
child.on("close", (code) => {
|
|
1146
|
+
if (code !== 0) {
|
|
1147
|
+
reject(new Error(`${cmd} ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
resolve10(out);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
async function parseManifestText(text, source) {
|
|
1155
|
+
try {
|
|
1156
|
+
return JSON.parse(text);
|
|
1157
|
+
} catch {
|
|
1158
|
+
throw new Error(`Invalid JSON in package version manifest: ${source}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async function loadManifestFromZip(zipPath) {
|
|
1162
|
+
const tmpDir = await mkdtemp(path3.join(os.tmpdir(), "vibgrate-manifest-"));
|
|
1163
|
+
try {
|
|
1164
|
+
await runCommand("unzip", ["-qq", zipPath, "-d", tmpDir]);
|
|
1165
|
+
const candidates = [
|
|
1166
|
+
path3.join(tmpDir, "package-versions.json"),
|
|
1167
|
+
path3.join(tmpDir, "manifest.json"),
|
|
1168
|
+
path3.join(tmpDir, "index.json")
|
|
1169
|
+
];
|
|
1170
|
+
for (const candidate of candidates) {
|
|
1171
|
+
try {
|
|
1172
|
+
const text = await readFile(candidate, "utf8");
|
|
1173
|
+
return await parseManifestText(text, candidate);
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
throw new Error("Zip must contain package-versions.json, manifest.json, or index.json");
|
|
1178
|
+
} finally {
|
|
1179
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function loadPackageVersionManifest(filePath) {
|
|
1183
|
+
const resolved = path3.resolve(filePath);
|
|
1184
|
+
if (resolved.toLowerCase().endsWith(".zip")) {
|
|
1185
|
+
return loadManifestFromZip(resolved);
|
|
1186
|
+
}
|
|
1187
|
+
const text = await readFile(resolved, "utf8");
|
|
1188
|
+
return parseManifestText(text, resolved);
|
|
1189
|
+
}
|
|
1190
|
+
function getManifestEntry(manifest, ecosystem, packageName) {
|
|
1191
|
+
if (!manifest) return void 0;
|
|
1192
|
+
const table = manifest[ecosystem];
|
|
1193
|
+
if (!table) return void 0;
|
|
1194
|
+
if (ecosystem === "nuget") {
|
|
1195
|
+
return table[packageName.toLowerCase()] ?? table[packageName];
|
|
1196
|
+
}
|
|
1197
|
+
return table[packageName];
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/scanners/npm-cache.ts
|
|
1189
1201
|
function stableOnly(versions) {
|
|
1190
1202
|
return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
|
|
1191
1203
|
}
|
|
@@ -1195,8 +1207,8 @@ function maxStable(versions) {
|
|
|
1195
1207
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
1196
1208
|
}
|
|
1197
1209
|
async function npmViewJson(args, cwd) {
|
|
1198
|
-
return new Promise((
|
|
1199
|
-
const child =
|
|
1210
|
+
return new Promise((resolve10, reject) => {
|
|
1211
|
+
const child = spawn2("npm", ["view", ...args, "--json"], {
|
|
1200
1212
|
cwd,
|
|
1201
1213
|
shell: true,
|
|
1202
1214
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1213,27 +1225,42 @@ async function npmViewJson(args, cwd) {
|
|
|
1213
1225
|
}
|
|
1214
1226
|
const trimmed = out.trim();
|
|
1215
1227
|
if (!trimmed) {
|
|
1216
|
-
|
|
1228
|
+
resolve10(null);
|
|
1217
1229
|
return;
|
|
1218
1230
|
}
|
|
1219
1231
|
try {
|
|
1220
|
-
|
|
1232
|
+
resolve10(JSON.parse(trimmed));
|
|
1221
1233
|
} catch {
|
|
1222
|
-
|
|
1234
|
+
resolve10(trimmed.replace(/^"|"$/g, ""));
|
|
1223
1235
|
}
|
|
1224
1236
|
});
|
|
1225
1237
|
});
|
|
1226
1238
|
}
|
|
1227
1239
|
var NpmCache = class {
|
|
1228
|
-
constructor(cwd, sem) {
|
|
1240
|
+
constructor(cwd, sem, manifest, offline = false) {
|
|
1229
1241
|
this.cwd = cwd;
|
|
1230
1242
|
this.sem = sem;
|
|
1243
|
+
this.manifest = manifest;
|
|
1244
|
+
this.offline = offline;
|
|
1231
1245
|
}
|
|
1232
1246
|
meta = /* @__PURE__ */ new Map();
|
|
1233
1247
|
get(pkg2) {
|
|
1234
1248
|
const existing = this.meta.get(pkg2);
|
|
1235
1249
|
if (existing) return existing;
|
|
1236
1250
|
const p = this.sem.run(async () => {
|
|
1251
|
+
const manifestEntry = getManifestEntry(this.manifest, "npm", pkg2);
|
|
1252
|
+
if (manifestEntry) {
|
|
1253
|
+
const stable2 = stableOnly(manifestEntry.versions ?? []);
|
|
1254
|
+
const latestStableOverall2 = maxStable(stable2);
|
|
1255
|
+
return {
|
|
1256
|
+
latest: manifestEntry.latest ?? latestStableOverall2,
|
|
1257
|
+
stableVersions: stable2,
|
|
1258
|
+
latestStableOverall: latestStableOverall2
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
if (this.offline) {
|
|
1262
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
1263
|
+
}
|
|
1237
1264
|
let latest = null;
|
|
1238
1265
|
let versions = [];
|
|
1239
1266
|
try {
|
|
@@ -1387,7 +1414,7 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1387
1414
|
results.push(result.value);
|
|
1388
1415
|
packageNameToPath.set(result.value.name, result.value.path);
|
|
1389
1416
|
} else {
|
|
1390
|
-
const relPath =
|
|
1417
|
+
const relPath = path4.relative(rootDir, path4.dirname(pjPath));
|
|
1391
1418
|
if (cache) {
|
|
1392
1419
|
cache.addStuckPath(relPath || ".");
|
|
1393
1420
|
}
|
|
@@ -1418,8 +1445,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1418
1445
|
}
|
|
1419
1446
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
1420
1447
|
const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
|
|
1421
|
-
const absProjectPath =
|
|
1422
|
-
const projectPath =
|
|
1448
|
+
const absProjectPath = path4.dirname(packageJsonPath);
|
|
1449
|
+
const projectPath = path4.relative(rootDir, absProjectPath) || ".";
|
|
1423
1450
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
1424
1451
|
let runtimeLatest;
|
|
1425
1452
|
let runtimeMajorsBehind;
|
|
@@ -1506,7 +1533,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1506
1533
|
return {
|
|
1507
1534
|
type: "node",
|
|
1508
1535
|
path: projectPath,
|
|
1509
|
-
name: pj.name ??
|
|
1536
|
+
name: pj.name ?? path4.basename(absProjectPath),
|
|
1510
1537
|
runtime: nodeEngine,
|
|
1511
1538
|
runtimeLatest,
|
|
1512
1539
|
runtimeMajorsBehind,
|
|
@@ -1518,7 +1545,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1518
1545
|
}
|
|
1519
1546
|
|
|
1520
1547
|
// src/scanners/dotnet-scanner.ts
|
|
1521
|
-
import * as
|
|
1548
|
+
import * as path5 from "path";
|
|
1522
1549
|
import * as semver3 from "semver";
|
|
1523
1550
|
import { XMLParser } from "fast-xml-parser";
|
|
1524
1551
|
var parser = new XMLParser({
|
|
@@ -1720,7 +1747,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1720
1747
|
const parsed = parser.parse(xml);
|
|
1721
1748
|
const project = parsed?.Project;
|
|
1722
1749
|
if (!project) {
|
|
1723
|
-
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName:
|
|
1750
|
+
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path5.basename(filePath, ".csproj") };
|
|
1724
1751
|
}
|
|
1725
1752
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1726
1753
|
const targetFrameworks = [];
|
|
@@ -1757,7 +1784,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1757
1784
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1758
1785
|
packageReferences,
|
|
1759
1786
|
projectReferences,
|
|
1760
|
-
projectName:
|
|
1787
|
+
projectName: path5.basename(filePath, ".csproj")
|
|
1761
1788
|
};
|
|
1762
1789
|
}
|
|
1763
1790
|
async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
@@ -1767,12 +1794,12 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1767
1794
|
for (const slnPath of slnFiles) {
|
|
1768
1795
|
try {
|
|
1769
1796
|
const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
|
|
1770
|
-
const slnDir =
|
|
1797
|
+
const slnDir = path5.dirname(slnPath);
|
|
1771
1798
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1772
1799
|
let match;
|
|
1773
1800
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1774
1801
|
if (match[1]) {
|
|
1775
|
-
const csprojPath =
|
|
1802
|
+
const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1776
1803
|
slnCsprojPaths.add(csprojPath);
|
|
1777
1804
|
}
|
|
1778
1805
|
}
|
|
@@ -1789,7 +1816,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1789
1816
|
if (result.ok) {
|
|
1790
1817
|
results.push(result.value);
|
|
1791
1818
|
} else {
|
|
1792
|
-
const relPath =
|
|
1819
|
+
const relPath = path5.relative(rootDir, path5.dirname(csprojPath));
|
|
1793
1820
|
if (cache) {
|
|
1794
1821
|
cache.addStuckPath(relPath || ".");
|
|
1795
1822
|
}
|
|
@@ -1805,7 +1832,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
|
1805
1832
|
async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
1806
1833
|
const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
|
|
1807
1834
|
const data = parseCsproj(xml, csprojPath);
|
|
1808
|
-
const csprojDir =
|
|
1835
|
+
const csprojDir = path5.dirname(csprojPath);
|
|
1809
1836
|
const primaryTfm = data.targetFrameworks[0];
|
|
1810
1837
|
let runtimeMajorsBehind;
|
|
1811
1838
|
let targetFramework = primaryTfm;
|
|
@@ -1883,9 +1910,9 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1883
1910
|
}
|
|
1884
1911
|
}
|
|
1885
1912
|
const projectReferences = data.projectReferences.map((refPath) => {
|
|
1886
|
-
const absRefPath =
|
|
1887
|
-
const relRefPath =
|
|
1888
|
-
const refName =
|
|
1913
|
+
const absRefPath = path5.resolve(csprojDir, refPath);
|
|
1914
|
+
const relRefPath = path5.relative(rootDir, path5.dirname(absRefPath));
|
|
1915
|
+
const refName = path5.basename(absRefPath, ".csproj");
|
|
1889
1916
|
return {
|
|
1890
1917
|
path: relRefPath || ".",
|
|
1891
1918
|
name: refName,
|
|
@@ -1906,7 +1933,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1906
1933
|
const buckets = bucketsMut;
|
|
1907
1934
|
return {
|
|
1908
1935
|
type: "dotnet",
|
|
1909
|
-
path:
|
|
1936
|
+
path: path5.relative(rootDir, csprojDir) || ".",
|
|
1910
1937
|
name: data.projectName,
|
|
1911
1938
|
targetFramework,
|
|
1912
1939
|
runtime: primaryTfm,
|
|
@@ -1921,7 +1948,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
|
1921
1948
|
}
|
|
1922
1949
|
|
|
1923
1950
|
// src/scanners/python-scanner.ts
|
|
1924
|
-
import * as
|
|
1951
|
+
import * as path6 from "path";
|
|
1925
1952
|
import * as semver4 from "semver";
|
|
1926
1953
|
var KNOWN_PYTHON_FRAMEWORKS = {
|
|
1927
1954
|
// ── Web Frameworks ──
|
|
@@ -2162,7 +2189,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
|
|
|
2162
2189
|
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
|
|
2163
2190
|
const projectDirs = /* @__PURE__ */ new Map();
|
|
2164
2191
|
for (const f of manifestFiles) {
|
|
2165
|
-
const dir =
|
|
2192
|
+
const dir = path6.dirname(f);
|
|
2166
2193
|
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2167
2194
|
projectDirs.get(dir).push(f);
|
|
2168
2195
|
}
|
|
@@ -2175,7 +2202,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
|
|
|
2175
2202
|
if (result.ok) {
|
|
2176
2203
|
results.push(result.value);
|
|
2177
2204
|
} else {
|
|
2178
|
-
const relPath =
|
|
2205
|
+
const relPath = path6.relative(rootDir, dir);
|
|
2179
2206
|
if (cache) cache.addStuckPath(relPath || ".");
|
|
2180
2207
|
console.error(`Timeout scanning Python project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2181
2208
|
}
|
|
@@ -2191,12 +2218,12 @@ async function findPythonManifests(rootDir) {
|
|
|
2191
2218
|
return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
|
|
2192
2219
|
}
|
|
2193
2220
|
async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
|
|
2194
|
-
const relDir =
|
|
2195
|
-
let projectName =
|
|
2221
|
+
const relDir = path6.relative(rootDir, dir) || ".";
|
|
2222
|
+
let projectName = path6.basename(dir === rootDir ? rootDir : dir);
|
|
2196
2223
|
let pythonVersion;
|
|
2197
2224
|
const allDeps = /* @__PURE__ */ new Map();
|
|
2198
2225
|
for (const f of manifestFiles) {
|
|
2199
|
-
const fileName =
|
|
2226
|
+
const fileName = path6.basename(f);
|
|
2200
2227
|
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2201
2228
|
if (fileName === "pyproject.toml") {
|
|
2202
2229
|
const parsed = parsePyprojectToml(content);
|
|
@@ -2313,7 +2340,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
|
|
|
2313
2340
|
}
|
|
2314
2341
|
|
|
2315
2342
|
// src/scanners/java-scanner.ts
|
|
2316
|
-
import * as
|
|
2343
|
+
import * as path7 from "path";
|
|
2317
2344
|
import * as semver5 from "semver";
|
|
2318
2345
|
import { XMLParser as XMLParser2 } from "fast-xml-parser";
|
|
2319
2346
|
var parser2 = new XMLParser2({
|
|
@@ -2419,7 +2446,7 @@ function parsePom(xml, filePath) {
|
|
|
2419
2446
|
const project = parsed?.project;
|
|
2420
2447
|
if (!project) {
|
|
2421
2448
|
return {
|
|
2422
|
-
artifactId:
|
|
2449
|
+
artifactId: path7.basename(path7.dirname(filePath)),
|
|
2423
2450
|
dependencies: [],
|
|
2424
2451
|
modules: [],
|
|
2425
2452
|
properties: {}
|
|
@@ -2479,7 +2506,7 @@ function parsePom(xml, filePath) {
|
|
|
2479
2506
|
}
|
|
2480
2507
|
return {
|
|
2481
2508
|
groupId: project.groupId ? String(project.groupId) : parent?.groupId,
|
|
2482
|
-
artifactId: String(project.artifactId ??
|
|
2509
|
+
artifactId: String(project.artifactId ?? path7.basename(path7.dirname(filePath))),
|
|
2483
2510
|
version: project.version ? String(project.version) : parent?.version,
|
|
2484
2511
|
packaging: project.packaging ? String(project.packaging) : void 0,
|
|
2485
2512
|
javaVersion,
|
|
@@ -2494,7 +2521,7 @@ function resolveProperty(value, properties) {
|
|
|
2494
2521
|
}
|
|
2495
2522
|
function parseGradleBuild(content, filePath) {
|
|
2496
2523
|
const deps = [];
|
|
2497
|
-
const projectName =
|
|
2524
|
+
const projectName = path7.basename(path7.dirname(filePath));
|
|
2498
2525
|
let javaVersion;
|
|
2499
2526
|
const compatMatch = content.match(/(?:sourceCompatibility|targetCompatibility|javaVersion)\s*[=:]\s*['"]?(?:JavaVersion\.VERSION_)?(\d+)['"]?/);
|
|
2500
2527
|
if (compatMatch) javaVersion = compatMatch[1];
|
|
@@ -2557,7 +2584,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
|
|
|
2557
2584
|
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
|
|
2558
2585
|
const projectDirs = /* @__PURE__ */ new Map();
|
|
2559
2586
|
for (const f of manifestFiles) {
|
|
2560
|
-
const dir =
|
|
2587
|
+
const dir = path7.dirname(f);
|
|
2561
2588
|
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2562
2589
|
projectDirs.get(dir).push(f);
|
|
2563
2590
|
}
|
|
@@ -2570,7 +2597,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
|
|
|
2570
2597
|
if (result.ok) {
|
|
2571
2598
|
results.push(result.value);
|
|
2572
2599
|
} else {
|
|
2573
|
-
const relPath =
|
|
2600
|
+
const relPath = path7.relative(rootDir, dir);
|
|
2574
2601
|
if (cache) cache.addStuckPath(relPath || ".");
|
|
2575
2602
|
console.error(`Timeout scanning Java project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2576
2603
|
}
|
|
@@ -2590,13 +2617,13 @@ async function findJavaManifests(rootDir) {
|
|
|
2590
2617
|
return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
|
|
2591
2618
|
}
|
|
2592
2619
|
async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
|
|
2593
|
-
const relDir =
|
|
2594
|
-
let projectName =
|
|
2620
|
+
const relDir = path7.relative(rootDir, dir) || ".";
|
|
2621
|
+
let projectName = path7.basename(dir === rootDir ? rootDir : dir);
|
|
2595
2622
|
let javaVersion;
|
|
2596
2623
|
const allDeps = /* @__PURE__ */ new Map();
|
|
2597
2624
|
const projectReferences = [];
|
|
2598
2625
|
for (const f of manifestFiles) {
|
|
2599
|
-
const fileName =
|
|
2626
|
+
const fileName = path7.basename(f);
|
|
2600
2627
|
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2601
2628
|
if (fileName === "pom.xml") {
|
|
2602
2629
|
const pom = parsePom(content, f);
|
|
@@ -2610,7 +2637,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
|
|
|
2610
2637
|
}
|
|
2611
2638
|
for (const mod of pom.modules) {
|
|
2612
2639
|
projectReferences.push({
|
|
2613
|
-
path:
|
|
2640
|
+
path: path7.join(relDir, mod),
|
|
2614
2641
|
name: mod,
|
|
2615
2642
|
refType: "project"
|
|
2616
2643
|
});
|
|
@@ -2716,8 +2743,10 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
|
|
|
2716
2743
|
// src/scanners/nuget-cache.ts
|
|
2717
2744
|
import * as semver6 from "semver";
|
|
2718
2745
|
var NuGetCache = class {
|
|
2719
|
-
constructor(sem) {
|
|
2746
|
+
constructor(sem, manifest, offline = false) {
|
|
2720
2747
|
this.sem = sem;
|
|
2748
|
+
this.manifest = manifest;
|
|
2749
|
+
this.offline = offline;
|
|
2721
2750
|
}
|
|
2722
2751
|
meta = /* @__PURE__ */ new Map();
|
|
2723
2752
|
baseUrl = "https://api.nuget.org/v3-flatcontainer";
|
|
@@ -2725,6 +2754,23 @@ var NuGetCache = class {
|
|
|
2725
2754
|
const existing = this.meta.get(pkg2);
|
|
2726
2755
|
if (existing) return existing;
|
|
2727
2756
|
const p = this.sem.run(async () => {
|
|
2757
|
+
const manifestEntry = getManifestEntry(this.manifest, "nuget", pkg2);
|
|
2758
|
+
if (manifestEntry) {
|
|
2759
|
+
const stableVersions = (manifestEntry.versions ?? []).filter((v) => {
|
|
2760
|
+
const parsed = semver6.valid(v);
|
|
2761
|
+
return parsed && semver6.prerelease(v) === null;
|
|
2762
|
+
});
|
|
2763
|
+
const sorted = [...stableVersions].sort(semver6.rcompare);
|
|
2764
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2765
|
+
return {
|
|
2766
|
+
latest: manifestEntry.latest ?? latestStableOverall,
|
|
2767
|
+
stableVersions,
|
|
2768
|
+
latestStableOverall
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
if (this.offline) {
|
|
2772
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2773
|
+
}
|
|
2728
2774
|
try {
|
|
2729
2775
|
const url = `${this.baseUrl}/${pkg2.toLowerCase()}/index.json`;
|
|
2730
2776
|
const response = await fetch(url, {
|
|
@@ -2767,14 +2813,34 @@ function pep440ToSemver2(ver) {
|
|
|
2767
2813
|
return semver7.valid(v);
|
|
2768
2814
|
}
|
|
2769
2815
|
var PyPICache = class {
|
|
2770
|
-
constructor(sem) {
|
|
2816
|
+
constructor(sem, manifest, offline = false) {
|
|
2771
2817
|
this.sem = sem;
|
|
2818
|
+
this.manifest = manifest;
|
|
2819
|
+
this.offline = offline;
|
|
2772
2820
|
}
|
|
2773
2821
|
meta = /* @__PURE__ */ new Map();
|
|
2774
2822
|
get(pkg2) {
|
|
2775
2823
|
const existing = this.meta.get(pkg2);
|
|
2776
2824
|
if (existing) return existing;
|
|
2777
2825
|
const p = this.sem.run(async () => {
|
|
2826
|
+
const manifestEntry = getManifestEntry(this.manifest, "pypi", pkg2);
|
|
2827
|
+
if (manifestEntry) {
|
|
2828
|
+
const stableVersions = [];
|
|
2829
|
+
for (const ver of manifestEntry.versions ?? []) {
|
|
2830
|
+
const sv = pep440ToSemver2(ver);
|
|
2831
|
+
if (sv) stableVersions.push(sv);
|
|
2832
|
+
}
|
|
2833
|
+
const sorted = [...stableVersions].sort(semver7.rcompare);
|
|
2834
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2835
|
+
return {
|
|
2836
|
+
latest: manifestEntry.latest ? pep440ToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
|
|
2837
|
+
stableVersions,
|
|
2838
|
+
latestStableOverall
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
if (this.offline) {
|
|
2842
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2843
|
+
}
|
|
2778
2844
|
try {
|
|
2779
2845
|
const url = `https://pypi.org/pypi/${encodeURIComponent(pkg2)}/json`;
|
|
2780
2846
|
const response = await fetch(url, {
|
|
@@ -2821,8 +2887,10 @@ function mavenToSemver2(ver) {
|
|
|
2821
2887
|
return semver8.valid(v);
|
|
2822
2888
|
}
|
|
2823
2889
|
var MavenCache = class {
|
|
2824
|
-
constructor(sem) {
|
|
2890
|
+
constructor(sem, manifest, offline = false) {
|
|
2825
2891
|
this.sem = sem;
|
|
2892
|
+
this.manifest = manifest;
|
|
2893
|
+
this.offline = offline;
|
|
2826
2894
|
}
|
|
2827
2895
|
meta = /* @__PURE__ */ new Map();
|
|
2828
2896
|
/**
|
|
@@ -2835,6 +2903,25 @@ var MavenCache = class {
|
|
|
2835
2903
|
const existing = this.meta.get(key);
|
|
2836
2904
|
if (existing) return existing;
|
|
2837
2905
|
const p = this.sem.run(async () => {
|
|
2906
|
+
const key2 = `${groupId}:${artifactId}`;
|
|
2907
|
+
const manifestEntry = getManifestEntry(this.manifest, "maven", key2);
|
|
2908
|
+
if (manifestEntry) {
|
|
2909
|
+
const stableVersions = [];
|
|
2910
|
+
for (const ver of manifestEntry.versions ?? []) {
|
|
2911
|
+
const sv = mavenToSemver2(ver);
|
|
2912
|
+
if (sv) stableVersions.push(sv);
|
|
2913
|
+
}
|
|
2914
|
+
const sorted = [...stableVersions].sort(semver8.rcompare);
|
|
2915
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2916
|
+
return {
|
|
2917
|
+
latest: manifestEntry.latest ? mavenToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
|
|
2918
|
+
stableVersions,
|
|
2919
|
+
latestStableOverall
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
if (this.offline) {
|
|
2923
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2924
|
+
}
|
|
2838
2925
|
try {
|
|
2839
2926
|
const url = `https://search.maven.org/solrsearch/select?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22&core=gav&rows=100&wt=json`;
|
|
2840
2927
|
const response = await fetch(url, {
|
|
@@ -2869,7 +2956,7 @@ var MavenCache = class {
|
|
|
2869
2956
|
};
|
|
2870
2957
|
|
|
2871
2958
|
// src/config.ts
|
|
2872
|
-
import * as
|
|
2959
|
+
import * as path8 from "path";
|
|
2873
2960
|
import * as fs from "fs/promises";
|
|
2874
2961
|
var CONFIG_FILES = [
|
|
2875
2962
|
"vibgrate.config.ts",
|
|
@@ -2895,7 +2982,7 @@ var DEFAULT_CONFIG = {
|
|
|
2895
2982
|
async function loadConfig(rootDir) {
|
|
2896
2983
|
let config = DEFAULT_CONFIG;
|
|
2897
2984
|
for (const file of CONFIG_FILES) {
|
|
2898
|
-
const configPath =
|
|
2985
|
+
const configPath = path8.join(rootDir, file);
|
|
2899
2986
|
if (await pathExists(configPath)) {
|
|
2900
2987
|
if (file.endsWith(".json")) {
|
|
2901
2988
|
const txt = await readTextFile(configPath);
|
|
@@ -2910,7 +2997,7 @@ async function loadConfig(rootDir) {
|
|
|
2910
2997
|
}
|
|
2911
2998
|
}
|
|
2912
2999
|
}
|
|
2913
|
-
const sidecarPath =
|
|
3000
|
+
const sidecarPath = path8.join(rootDir, ".vibgrate", "auto-excludes.json");
|
|
2914
3001
|
if (await pathExists(sidecarPath)) {
|
|
2915
3002
|
try {
|
|
2916
3003
|
const txt = await readTextFile(sidecarPath);
|
|
@@ -2925,7 +3012,7 @@ async function loadConfig(rootDir) {
|
|
|
2925
3012
|
return config;
|
|
2926
3013
|
}
|
|
2927
3014
|
async function writeDefaultConfig(rootDir) {
|
|
2928
|
-
const configPath =
|
|
3015
|
+
const configPath = path8.join(rootDir, "vibgrate.config.ts");
|
|
2929
3016
|
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
2930
3017
|
|
|
2931
3018
|
const config: VibgrateConfig = {
|
|
@@ -2951,7 +3038,7 @@ export default config;
|
|
|
2951
3038
|
}
|
|
2952
3039
|
async function appendExcludePatterns(rootDir, newPatterns) {
|
|
2953
3040
|
if (newPatterns.length === 0) return false;
|
|
2954
|
-
const jsonPath =
|
|
3041
|
+
const jsonPath = path8.join(rootDir, "vibgrate.config.json");
|
|
2955
3042
|
if (await pathExists(jsonPath)) {
|
|
2956
3043
|
try {
|
|
2957
3044
|
const txt = await readTextFile(jsonPath);
|
|
@@ -2964,8 +3051,8 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2964
3051
|
} catch {
|
|
2965
3052
|
}
|
|
2966
3053
|
}
|
|
2967
|
-
const vibgrateDir =
|
|
2968
|
-
const sidecarPath =
|
|
3054
|
+
const vibgrateDir = path8.join(rootDir, ".vibgrate");
|
|
3055
|
+
const sidecarPath = path8.join(vibgrateDir, "auto-excludes.json");
|
|
2969
3056
|
let existing = [];
|
|
2970
3057
|
if (await pathExists(sidecarPath)) {
|
|
2971
3058
|
try {
|
|
@@ -2986,7 +3073,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2986
3073
|
}
|
|
2987
3074
|
|
|
2988
3075
|
// src/utils/vcs.ts
|
|
2989
|
-
import * as
|
|
3076
|
+
import * as path9 from "path";
|
|
2990
3077
|
import * as fs2 from "fs/promises";
|
|
2991
3078
|
async function detectVcs(rootDir) {
|
|
2992
3079
|
try {
|
|
@@ -3000,7 +3087,7 @@ async function detectGit(rootDir) {
|
|
|
3000
3087
|
if (!gitDir) {
|
|
3001
3088
|
return { type: "unknown" };
|
|
3002
3089
|
}
|
|
3003
|
-
const headPath =
|
|
3090
|
+
const headPath = path9.join(gitDir, "HEAD");
|
|
3004
3091
|
let headContent;
|
|
3005
3092
|
try {
|
|
3006
3093
|
headContent = (await fs2.readFile(headPath, "utf8")).trim();
|
|
@@ -3016,18 +3103,20 @@ async function detectGit(rootDir) {
|
|
|
3016
3103
|
} else if (/^[0-9a-f]{40}$/i.test(headContent)) {
|
|
3017
3104
|
sha = headContent;
|
|
3018
3105
|
}
|
|
3106
|
+
const remoteUrl = await readGitRemoteUrl(gitDir);
|
|
3019
3107
|
return {
|
|
3020
3108
|
type: "git",
|
|
3021
3109
|
sha: sha ?? void 0,
|
|
3022
3110
|
shortSha: sha ? sha.slice(0, 7) : void 0,
|
|
3023
|
-
branch: branch ?? void 0
|
|
3111
|
+
branch: branch ?? void 0,
|
|
3112
|
+
remoteUrl
|
|
3024
3113
|
};
|
|
3025
3114
|
}
|
|
3026
3115
|
async function findGitDir(startDir) {
|
|
3027
|
-
let dir =
|
|
3028
|
-
const root =
|
|
3116
|
+
let dir = path9.resolve(startDir);
|
|
3117
|
+
const root = path9.parse(dir).root;
|
|
3029
3118
|
while (dir !== root) {
|
|
3030
|
-
const gitPath =
|
|
3119
|
+
const gitPath = path9.join(dir, ".git");
|
|
3031
3120
|
try {
|
|
3032
3121
|
const stat3 = await fs2.stat(gitPath);
|
|
3033
3122
|
if (stat3.isDirectory()) {
|
|
@@ -3036,18 +3125,18 @@ async function findGitDir(startDir) {
|
|
|
3036
3125
|
if (stat3.isFile()) {
|
|
3037
3126
|
const content = (await fs2.readFile(gitPath, "utf8")).trim();
|
|
3038
3127
|
if (content.startsWith("gitdir: ")) {
|
|
3039
|
-
const resolved =
|
|
3128
|
+
const resolved = path9.resolve(dir, content.slice(8));
|
|
3040
3129
|
return resolved;
|
|
3041
3130
|
}
|
|
3042
3131
|
}
|
|
3043
3132
|
} catch {
|
|
3044
3133
|
}
|
|
3045
|
-
dir =
|
|
3134
|
+
dir = path9.dirname(dir);
|
|
3046
3135
|
}
|
|
3047
3136
|
return null;
|
|
3048
3137
|
}
|
|
3049
3138
|
async function resolveRef(gitDir, refPath) {
|
|
3050
|
-
const loosePath =
|
|
3139
|
+
const loosePath = path9.join(gitDir, refPath);
|
|
3051
3140
|
try {
|
|
3052
3141
|
const sha = (await fs2.readFile(loosePath, "utf8")).trim();
|
|
3053
3142
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
@@ -3055,7 +3144,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
3055
3144
|
}
|
|
3056
3145
|
} catch {
|
|
3057
3146
|
}
|
|
3058
|
-
const packedPath =
|
|
3147
|
+
const packedPath = path9.join(gitDir, "packed-refs");
|
|
3059
3148
|
try {
|
|
3060
3149
|
const packed = await fs2.readFile(packedPath, "utf8");
|
|
3061
3150
|
for (const line of packed.split("\n")) {
|
|
@@ -3069,6 +3158,39 @@ async function resolveRef(gitDir, refPath) {
|
|
|
3069
3158
|
}
|
|
3070
3159
|
return void 0;
|
|
3071
3160
|
}
|
|
3161
|
+
async function readGitRemoteUrl(gitDir) {
|
|
3162
|
+
const configPath = await resolveGitConfigPath(gitDir);
|
|
3163
|
+
if (!configPath) return void 0;
|
|
3164
|
+
try {
|
|
3165
|
+
const config = await fs2.readFile(configPath, "utf8");
|
|
3166
|
+
const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
|
|
3167
|
+
if (!originBlock) return void 0;
|
|
3168
|
+
const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
|
|
3169
|
+
return urlMatch?.[1]?.trim();
|
|
3170
|
+
} catch {
|
|
3171
|
+
return void 0;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
async function resolveGitConfigPath(gitDir) {
|
|
3175
|
+
const directConfig = path9.join(gitDir, "config");
|
|
3176
|
+
try {
|
|
3177
|
+
const stat3 = await fs2.stat(directConfig);
|
|
3178
|
+
if (stat3.isFile()) return directConfig;
|
|
3179
|
+
} catch {
|
|
3180
|
+
}
|
|
3181
|
+
const commonDirFile = path9.join(gitDir, "commondir");
|
|
3182
|
+
try {
|
|
3183
|
+
const commonDir = (await fs2.readFile(commonDirFile, "utf8")).trim();
|
|
3184
|
+
if (!commonDir) return void 0;
|
|
3185
|
+
const resolvedCommonDir = path9.resolve(gitDir, commonDir);
|
|
3186
|
+
const commonConfig = path9.join(resolvedCommonDir, "config");
|
|
3187
|
+
const stat3 = await fs2.stat(commonConfig);
|
|
3188
|
+
if (stat3.isFile()) return commonConfig;
|
|
3189
|
+
} catch {
|
|
3190
|
+
return void 0;
|
|
3191
|
+
}
|
|
3192
|
+
return void 0;
|
|
3193
|
+
}
|
|
3072
3194
|
|
|
3073
3195
|
// src/ui/progress.ts
|
|
3074
3196
|
import chalk4 from "chalk";
|
|
@@ -3452,11 +3574,11 @@ var ScanProgress = class {
|
|
|
3452
3574
|
|
|
3453
3575
|
// src/ui/scan-history.ts
|
|
3454
3576
|
import * as fs3 from "fs/promises";
|
|
3455
|
-
import * as
|
|
3577
|
+
import * as path10 from "path";
|
|
3456
3578
|
var HISTORY_FILENAME = "scan_history.json";
|
|
3457
3579
|
var MAX_RECORDS = 10;
|
|
3458
3580
|
async function loadScanHistory(rootDir) {
|
|
3459
|
-
const filePath =
|
|
3581
|
+
const filePath = path10.join(rootDir, ".vibgrate", HISTORY_FILENAME);
|
|
3460
3582
|
try {
|
|
3461
3583
|
const txt = await fs3.readFile(filePath, "utf8");
|
|
3462
3584
|
const data = JSON.parse(txt);
|
|
@@ -3469,8 +3591,8 @@ async function loadScanHistory(rootDir) {
|
|
|
3469
3591
|
}
|
|
3470
3592
|
}
|
|
3471
3593
|
async function saveScanHistory(rootDir, record) {
|
|
3472
|
-
const dir =
|
|
3473
|
-
const filePath =
|
|
3594
|
+
const dir = path10.join(rootDir, ".vibgrate");
|
|
3595
|
+
const filePath = path10.join(dir, HISTORY_FILENAME);
|
|
3474
3596
|
let history;
|
|
3475
3597
|
const existing = await loadScanHistory(rootDir);
|
|
3476
3598
|
if (existing) {
|
|
@@ -3534,7 +3656,7 @@ function estimateStepDurations(history, currentFileCount) {
|
|
|
3534
3656
|
}
|
|
3535
3657
|
|
|
3536
3658
|
// src/scanners/platform-matrix.ts
|
|
3537
|
-
import * as
|
|
3659
|
+
import * as path11 from "path";
|
|
3538
3660
|
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
3539
3661
|
// Image / media processing
|
|
3540
3662
|
"sharp",
|
|
@@ -3814,7 +3936,7 @@ async function scanPlatformMatrix(rootDir, cache) {
|
|
|
3814
3936
|
}
|
|
3815
3937
|
result.dockerBaseImages = [...baseImages].sort();
|
|
3816
3938
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
3817
|
-
const exists = cache ? await cache.pathExists(
|
|
3939
|
+
const exists = cache ? await cache.pathExists(path11.join(rootDir, file)) : await pathExists(path11.join(rootDir, file));
|
|
3818
3940
|
if (exists) {
|
|
3819
3941
|
result.nodeVersionFiles.push(file);
|
|
3820
3942
|
}
|
|
@@ -3891,7 +4013,7 @@ function scanDependencyRisk(projects) {
|
|
|
3891
4013
|
}
|
|
3892
4014
|
|
|
3893
4015
|
// src/scanners/dependency-graph.ts
|
|
3894
|
-
import * as
|
|
4016
|
+
import * as path12 from "path";
|
|
3895
4017
|
function parsePnpmLock(content) {
|
|
3896
4018
|
const entries = [];
|
|
3897
4019
|
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
@@ -3950,9 +4072,9 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
3950
4072
|
phantomDependencies: []
|
|
3951
4073
|
};
|
|
3952
4074
|
let entries = [];
|
|
3953
|
-
const pnpmLock =
|
|
3954
|
-
const npmLock =
|
|
3955
|
-
const yarnLock =
|
|
4075
|
+
const pnpmLock = path12.join(rootDir, "pnpm-lock.yaml");
|
|
4076
|
+
const npmLock = path12.join(rootDir, "package-lock.json");
|
|
4077
|
+
const yarnLock = path12.join(rootDir, "yarn.lock");
|
|
3956
4078
|
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
3957
4079
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3958
4080
|
if (await _pathExists(pnpmLock)) {
|
|
@@ -3999,7 +4121,7 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
3999
4121
|
for (const pjPath of pkgFiles) {
|
|
4000
4122
|
try {
|
|
4001
4123
|
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
4002
|
-
const relPath =
|
|
4124
|
+
const relPath = path12.relative(rootDir, pjPath);
|
|
4003
4125
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
4004
4126
|
const deps = pj[section];
|
|
4005
4127
|
if (!deps) continue;
|
|
@@ -4345,7 +4467,7 @@ function scanToolingInventory(projects) {
|
|
|
4345
4467
|
}
|
|
4346
4468
|
|
|
4347
4469
|
// src/scanners/build-deploy.ts
|
|
4348
|
-
import * as
|
|
4470
|
+
import * as path13 from "path";
|
|
4349
4471
|
var CI_FILES = {
|
|
4350
4472
|
".github/workflows": "github-actions",
|
|
4351
4473
|
".gitlab-ci.yml": "gitlab-ci",
|
|
@@ -4398,17 +4520,17 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4398
4520
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
4399
4521
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
4400
4522
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
4401
|
-
const fullPath =
|
|
4523
|
+
const fullPath = path13.join(rootDir, file);
|
|
4402
4524
|
if (await _pathExists(fullPath)) {
|
|
4403
4525
|
ciSystems.add(system);
|
|
4404
4526
|
}
|
|
4405
4527
|
}
|
|
4406
|
-
const ghWorkflowDir =
|
|
4528
|
+
const ghWorkflowDir = path13.join(rootDir, ".github", "workflows");
|
|
4407
4529
|
if (await _pathExists(ghWorkflowDir)) {
|
|
4408
4530
|
try {
|
|
4409
4531
|
if (cache) {
|
|
4410
4532
|
const entries = await cache.walkDir(rootDir);
|
|
4411
|
-
const ghPrefix =
|
|
4533
|
+
const ghPrefix = path13.relative(rootDir, ghWorkflowDir) + path13.sep;
|
|
4412
4534
|
result.ciWorkflowCount = entries.filter(
|
|
4413
4535
|
(e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
|
|
4414
4536
|
).length;
|
|
@@ -4459,11 +4581,11 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4459
4581
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
4460
4582
|
);
|
|
4461
4583
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
4462
|
-
if (await _pathExists(
|
|
4584
|
+
if (await _pathExists(path13.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
4463
4585
|
result.iac = [...iacSystems].sort();
|
|
4464
4586
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
4465
4587
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
4466
|
-
if (await _pathExists(
|
|
4588
|
+
if (await _pathExists(path13.join(rootDir, file))) releaseTools.add(tool);
|
|
4467
4589
|
}
|
|
4468
4590
|
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
4469
4591
|
for (const pjPath of pkgFiles) {
|
|
@@ -4488,19 +4610,19 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
4488
4610
|
};
|
|
4489
4611
|
const managers = /* @__PURE__ */ new Set();
|
|
4490
4612
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
4491
|
-
if (await _pathExists(
|
|
4613
|
+
if (await _pathExists(path13.join(rootDir, file))) managers.add(manager);
|
|
4492
4614
|
}
|
|
4493
4615
|
result.packageManagers = [...managers].sort();
|
|
4494
4616
|
const monoTools = /* @__PURE__ */ new Set();
|
|
4495
4617
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
4496
|
-
if (await _pathExists(
|
|
4618
|
+
if (await _pathExists(path13.join(rootDir, file))) monoTools.add(tool);
|
|
4497
4619
|
}
|
|
4498
4620
|
result.monorepoTools = [...monoTools].sort();
|
|
4499
4621
|
return result;
|
|
4500
4622
|
}
|
|
4501
4623
|
|
|
4502
4624
|
// src/scanners/ts-modernity.ts
|
|
4503
|
-
import * as
|
|
4625
|
+
import * as path14 from "path";
|
|
4504
4626
|
async function scanTsModernity(rootDir, cache) {
|
|
4505
4627
|
const result = {
|
|
4506
4628
|
typescriptVersion: null,
|
|
@@ -4538,7 +4660,7 @@ async function scanTsModernity(rootDir, cache) {
|
|
|
4538
4660
|
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
4539
4661
|
else if (hasEsm) result.moduleType = "esm";
|
|
4540
4662
|
else if (hasCjs) result.moduleType = "cjs";
|
|
4541
|
-
let tsConfigPath =
|
|
4663
|
+
let tsConfigPath = path14.join(rootDir, "tsconfig.json");
|
|
4542
4664
|
const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
|
|
4543
4665
|
if (!tsConfigExists) {
|
|
4544
4666
|
const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
@@ -4885,7 +5007,7 @@ function scanBreakingChangeExposure(projects) {
|
|
|
4885
5007
|
|
|
4886
5008
|
// src/scanners/file-hotspots.ts
|
|
4887
5009
|
import * as fs4 from "fs/promises";
|
|
4888
|
-
import * as
|
|
5010
|
+
import * as path15 from "path";
|
|
4889
5011
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4890
5012
|
"node_modules",
|
|
4891
5013
|
".git",
|
|
@@ -4930,9 +5052,9 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4930
5052
|
const entries = await cache.walkDir(rootDir);
|
|
4931
5053
|
for (const entry of entries) {
|
|
4932
5054
|
if (!entry.isFile) continue;
|
|
4933
|
-
const ext =
|
|
5055
|
+
const ext = path15.extname(entry.name).toLowerCase();
|
|
4934
5056
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4935
|
-
const depth = entry.relPath.split(
|
|
5057
|
+
const depth = entry.relPath.split(path15.sep).length - 1;
|
|
4936
5058
|
if (depth > maxDepth) maxDepth = depth;
|
|
4937
5059
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4938
5060
|
try {
|
|
@@ -4961,15 +5083,15 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4961
5083
|
for (const e of entries) {
|
|
4962
5084
|
if (e.isDirectory) {
|
|
4963
5085
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
4964
|
-
await walk(
|
|
5086
|
+
await walk(path15.join(dir, e.name), depth + 1);
|
|
4965
5087
|
} else if (e.isFile) {
|
|
4966
|
-
const ext =
|
|
5088
|
+
const ext = path15.extname(e.name).toLowerCase();
|
|
4967
5089
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4968
5090
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4969
5091
|
try {
|
|
4970
|
-
const stat3 = await fs4.stat(
|
|
5092
|
+
const stat3 = await fs4.stat(path15.join(dir, e.name));
|
|
4971
5093
|
allFiles.push({
|
|
4972
|
-
path:
|
|
5094
|
+
path: path15.relative(rootDir, path15.join(dir, e.name)),
|
|
4973
5095
|
bytes: stat3.size
|
|
4974
5096
|
});
|
|
4975
5097
|
} catch {
|
|
@@ -4992,7 +5114,7 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4992
5114
|
}
|
|
4993
5115
|
|
|
4994
5116
|
// src/scanners/security-posture.ts
|
|
4995
|
-
import * as
|
|
5117
|
+
import * as path16 from "path";
|
|
4996
5118
|
var LOCKFILES = {
|
|
4997
5119
|
"pnpm-lock.yaml": "pnpm",
|
|
4998
5120
|
"package-lock.json": "npm",
|
|
@@ -5013,14 +5135,14 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
5013
5135
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
5014
5136
|
const foundLockfiles = [];
|
|
5015
5137
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
5016
|
-
if (await _pathExists(
|
|
5138
|
+
if (await _pathExists(path16.join(rootDir, file))) {
|
|
5017
5139
|
foundLockfiles.push(type);
|
|
5018
5140
|
}
|
|
5019
5141
|
}
|
|
5020
5142
|
result.lockfilePresent = foundLockfiles.length > 0;
|
|
5021
5143
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
5022
5144
|
result.lockfileTypes = foundLockfiles.sort();
|
|
5023
|
-
const gitignorePath =
|
|
5145
|
+
const gitignorePath = path16.join(rootDir, ".gitignore");
|
|
5024
5146
|
if (await _pathExists(gitignorePath)) {
|
|
5025
5147
|
try {
|
|
5026
5148
|
const content = await _readTextFile(gitignorePath);
|
|
@@ -5035,7 +5157,7 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
5035
5157
|
}
|
|
5036
5158
|
}
|
|
5037
5159
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
5038
|
-
if (await _pathExists(
|
|
5160
|
+
if (await _pathExists(path16.join(rootDir, envFile))) {
|
|
5039
5161
|
if (!result.gitignoreCoversEnv) {
|
|
5040
5162
|
result.envFilesTracked = true;
|
|
5041
5163
|
break;
|
|
@@ -5046,8 +5168,8 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
5046
5168
|
}
|
|
5047
5169
|
|
|
5048
5170
|
// src/scanners/security-scanners.ts
|
|
5049
|
-
import { spawn as
|
|
5050
|
-
import * as
|
|
5171
|
+
import { spawn as spawn3 } from "child_process";
|
|
5172
|
+
import * as path17 from "path";
|
|
5051
5173
|
var TOOL_MATRIX = [
|
|
5052
5174
|
{ key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
|
|
5053
5175
|
{ key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
|
|
@@ -5060,8 +5182,8 @@ var SECRET_HEURISTICS = [
|
|
|
5060
5182
|
{ detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
5061
5183
|
{ detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
|
|
5062
5184
|
];
|
|
5063
|
-
var defaultRunner = (command, args) => new Promise((
|
|
5064
|
-
const child =
|
|
5185
|
+
var defaultRunner = (command, args) => new Promise((resolve10, reject) => {
|
|
5186
|
+
const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5065
5187
|
let stdout = "";
|
|
5066
5188
|
let stderr = "";
|
|
5067
5189
|
child.stdout.on("data", (d) => {
|
|
@@ -5072,7 +5194,7 @@ var defaultRunner = (command, args) => new Promise((resolve8, reject) => {
|
|
|
5072
5194
|
});
|
|
5073
5195
|
child.on("error", reject);
|
|
5074
5196
|
child.on("close", (code) => {
|
|
5075
|
-
|
|
5197
|
+
resolve10({ stdout, stderr, exitCode: code ?? 1 });
|
|
5076
5198
|
});
|
|
5077
5199
|
});
|
|
5078
5200
|
function compareSemver(a, b) {
|
|
@@ -5142,7 +5264,7 @@ async function detectSecretHeuristics(rootDir, cache) {
|
|
|
5142
5264
|
const findings = [];
|
|
5143
5265
|
for (const entry of entries) {
|
|
5144
5266
|
if (!entry.isFile) continue;
|
|
5145
|
-
const ext =
|
|
5267
|
+
const ext = path17.extname(entry.name).toLowerCase();
|
|
5146
5268
|
if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
|
|
5147
5269
|
const content = await cache.readTextFile(entry.absPath);
|
|
5148
5270
|
if (!content || content.length > 3e5) continue;
|
|
@@ -5165,9 +5287,9 @@ async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
|
|
|
5165
5287
|
const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
|
|
5166
5288
|
const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
|
|
5167
5289
|
const configFiles = {
|
|
5168
|
-
semgrep: await cache.pathExists(
|
|
5169
|
-
gitleaks: await cache.pathExists(
|
|
5170
|
-
trufflehog: await cache.pathExists(
|
|
5290
|
+
semgrep: await cache.pathExists(path17.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path17.join(rootDir, ".semgrep.yaml")),
|
|
5291
|
+
gitleaks: await cache.pathExists(path17.join(rootDir, ".gitleaks.toml")),
|
|
5292
|
+
trufflehog: await cache.pathExists(path17.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path17.join(rootDir, ".trufflehog.yaml"))
|
|
5171
5293
|
};
|
|
5172
5294
|
return {
|
|
5173
5295
|
semgrep,
|
|
@@ -5592,7 +5714,7 @@ function scanServiceDependencies(projects) {
|
|
|
5592
5714
|
}
|
|
5593
5715
|
|
|
5594
5716
|
// src/scanners/architecture.ts
|
|
5595
|
-
import * as
|
|
5717
|
+
import * as path18 from "path";
|
|
5596
5718
|
import * as fs5 from "fs/promises";
|
|
5597
5719
|
var ARCHETYPE_SIGNALS = [
|
|
5598
5720
|
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
@@ -5891,9 +6013,9 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5891
6013
|
const entries = await cache.walkDir(rootDir);
|
|
5892
6014
|
return entries.filter((e) => {
|
|
5893
6015
|
if (!e.isFile) return false;
|
|
5894
|
-
const name =
|
|
6016
|
+
const name = path18.basename(e.absPath);
|
|
5895
6017
|
if (name.startsWith(".") && name !== ".") return false;
|
|
5896
|
-
const ext =
|
|
6018
|
+
const ext = path18.extname(name);
|
|
5897
6019
|
return SOURCE_EXTENSIONS.has(ext);
|
|
5898
6020
|
}).map((e) => e.relPath);
|
|
5899
6021
|
}
|
|
@@ -5907,15 +6029,15 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5907
6029
|
}
|
|
5908
6030
|
for (const entry of entries) {
|
|
5909
6031
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
5910
|
-
const fullPath =
|
|
6032
|
+
const fullPath = path18.join(dir, entry.name);
|
|
5911
6033
|
if (entry.isDirectory()) {
|
|
5912
6034
|
if (!IGNORE_DIRS.has(entry.name)) {
|
|
5913
6035
|
await walk(fullPath);
|
|
5914
6036
|
}
|
|
5915
6037
|
} else if (entry.isFile()) {
|
|
5916
|
-
const ext =
|
|
6038
|
+
const ext = path18.extname(entry.name);
|
|
5917
6039
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
5918
|
-
files.push(
|
|
6040
|
+
files.push(path18.relative(rootDir, fullPath));
|
|
5919
6041
|
}
|
|
5920
6042
|
}
|
|
5921
6043
|
}
|
|
@@ -5939,7 +6061,7 @@ function classifyFile(filePath, archetype) {
|
|
|
5939
6061
|
}
|
|
5940
6062
|
}
|
|
5941
6063
|
if (!bestMatch || bestMatch.confidence < 0.7) {
|
|
5942
|
-
const baseName =
|
|
6064
|
+
const baseName = path18.basename(filePath, path18.extname(filePath));
|
|
5943
6065
|
const cleanBase = baseName.replace(/\.(test|spec)$/, "");
|
|
5944
6066
|
for (const rule of SUFFIX_RULES) {
|
|
5945
6067
|
if (cleanBase.endsWith(rule.suffix)) {
|
|
@@ -6022,6 +6144,56 @@ function mapToolingToLayers(tooling, services, depsByLayer) {
|
|
|
6022
6144
|
}
|
|
6023
6145
|
return { layerTooling, layerServices };
|
|
6024
6146
|
}
|
|
6147
|
+
function generateLayerFlowMermaid(layers) {
|
|
6148
|
+
const labels = {
|
|
6149
|
+
presentation: "Presentation",
|
|
6150
|
+
routing: "Routing",
|
|
6151
|
+
middleware: "Middleware",
|
|
6152
|
+
services: "Services",
|
|
6153
|
+
domain: "Domain",
|
|
6154
|
+
"data-access": "Data Access",
|
|
6155
|
+
infrastructure: "Infrastructure",
|
|
6156
|
+
config: "Config",
|
|
6157
|
+
shared: "Shared",
|
|
6158
|
+
testing: "Testing"
|
|
6159
|
+
};
|
|
6160
|
+
if (layers.length === 0) {
|
|
6161
|
+
return 'flowchart TD\n APP["Project"]';
|
|
6162
|
+
}
|
|
6163
|
+
const ordered = [...layers];
|
|
6164
|
+
const lines = ["flowchart TD"];
|
|
6165
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
6166
|
+
const layer = ordered[i];
|
|
6167
|
+
lines.push(` L${i}["${labels[layer]}"]`);
|
|
6168
|
+
if (i > 0) lines.push(` L${i - 1} --> L${i}`);
|
|
6169
|
+
}
|
|
6170
|
+
return lines.join("\n");
|
|
6171
|
+
}
|
|
6172
|
+
async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
|
|
6173
|
+
const projectRoot = path18.resolve(rootDir, project.path || ".");
|
|
6174
|
+
const allFiles = await walkSourceFiles(projectRoot, cache);
|
|
6175
|
+
const layerSet = /* @__PURE__ */ new Set();
|
|
6176
|
+
for (const rel of allFiles) {
|
|
6177
|
+
const classification = classifyFile(rel, archetype);
|
|
6178
|
+
if (classification) {
|
|
6179
|
+
layerSet.add(classification.layer);
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
6182
|
+
const layerOrder = [
|
|
6183
|
+
"presentation",
|
|
6184
|
+
"routing",
|
|
6185
|
+
"middleware",
|
|
6186
|
+
"services",
|
|
6187
|
+
"domain",
|
|
6188
|
+
"data-access",
|
|
6189
|
+
"infrastructure",
|
|
6190
|
+
"config",
|
|
6191
|
+
"shared",
|
|
6192
|
+
"testing"
|
|
6193
|
+
];
|
|
6194
|
+
const orderedLayers = layerOrder.filter((l) => layerSet.has(l));
|
|
6195
|
+
return generateLayerFlowMermaid(orderedLayers);
|
|
6196
|
+
}
|
|
6025
6197
|
async function scanArchitecture(rootDir, projects, tooling, services, cache) {
|
|
6026
6198
|
const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
|
|
6027
6199
|
const sourceFiles = await walkSourceFiles(rootDir, cache);
|
|
@@ -6124,7 +6296,7 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
|
|
|
6124
6296
|
}
|
|
6125
6297
|
|
|
6126
6298
|
// src/scanners/code-quality.ts
|
|
6127
|
-
import * as
|
|
6299
|
+
import * as path19 from "path";
|
|
6128
6300
|
import * as ts from "typescript";
|
|
6129
6301
|
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6130
6302
|
var DEFAULT_RESULT = {
|
|
@@ -6155,9 +6327,9 @@ async function scanCodeQuality(rootDir, cache) {
|
|
|
6155
6327
|
continue;
|
|
6156
6328
|
}
|
|
6157
6329
|
if (!raw.trim()) continue;
|
|
6158
|
-
const rel = normalizeModuleId(
|
|
6330
|
+
const rel = normalizeModuleId(path19.relative(rootDir, filePath));
|
|
6159
6331
|
const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
|
|
6160
|
-
const imports = collectLocalImports(source,
|
|
6332
|
+
const imports = collectLocalImports(source, path19.dirname(filePath), rootDir);
|
|
6161
6333
|
depGraph.set(rel, imports);
|
|
6162
6334
|
const fileMetrics = computeFileMetrics(source, raw);
|
|
6163
6335
|
totalFunctions += fileMetrics.functionsAnalyzed;
|
|
@@ -6191,9 +6363,9 @@ async function scanCodeQuality(rootDir, cache) {
|
|
|
6191
6363
|
async function findSourceFiles(rootDir, cache) {
|
|
6192
6364
|
if (cache) {
|
|
6193
6365
|
const entries = await cache.walkDir(rootDir);
|
|
6194
|
-
return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(
|
|
6366
|
+
return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path19.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
|
|
6195
6367
|
}
|
|
6196
|
-
const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(
|
|
6368
|
+
const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path19.extname(name).toLowerCase()));
|
|
6197
6369
|
return files;
|
|
6198
6370
|
}
|
|
6199
6371
|
function collectLocalImports(source, fileDir, rootDir) {
|
|
@@ -6214,8 +6386,8 @@ function collectLocalImports(source, fileDir, rootDir) {
|
|
|
6214
6386
|
}
|
|
6215
6387
|
function resolveLocalImport(specifier, fileDir, rootDir) {
|
|
6216
6388
|
if (!specifier.startsWith(".")) return null;
|
|
6217
|
-
const rawTarget =
|
|
6218
|
-
const normalized =
|
|
6389
|
+
const rawTarget = path19.resolve(fileDir, specifier);
|
|
6390
|
+
const normalized = path19.relative(rootDir, rawTarget).replace(/\\/g, "/");
|
|
6219
6391
|
if (!normalized || normalized.startsWith("..")) return null;
|
|
6220
6392
|
return normalizeModuleId(normalized);
|
|
6221
6393
|
}
|
|
@@ -6356,8 +6528,8 @@ function visitEach(node, cb) {
|
|
|
6356
6528
|
}
|
|
6357
6529
|
|
|
6358
6530
|
// src/scanners/owasp-category-mapping.ts
|
|
6359
|
-
import { spawn as
|
|
6360
|
-
import * as
|
|
6531
|
+
import { spawn as spawn4 } from "child_process";
|
|
6532
|
+
import * as path20 from "path";
|
|
6361
6533
|
var OWASP_CONFIG = "p/owasp-top-ten";
|
|
6362
6534
|
var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6363
6535
|
".js",
|
|
@@ -6382,8 +6554,8 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
6382
6554
|
".env"
|
|
6383
6555
|
]);
|
|
6384
6556
|
async function runSemgrep(args, cwd, stdin) {
|
|
6385
|
-
return new Promise((
|
|
6386
|
-
const child =
|
|
6557
|
+
return new Promise((resolve10, reject) => {
|
|
6558
|
+
const child = spawn4("semgrep", args, {
|
|
6387
6559
|
cwd,
|
|
6388
6560
|
shell: true,
|
|
6389
6561
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6398,7 +6570,7 @@ async function runSemgrep(args, cwd, stdin) {
|
|
|
6398
6570
|
});
|
|
6399
6571
|
child.on("error", reject);
|
|
6400
6572
|
child.on("close", (code) => {
|
|
6401
|
-
|
|
6573
|
+
resolve10({ code: code ?? 1, stdout, stderr });
|
|
6402
6574
|
});
|
|
6403
6575
|
if (stdin !== void 0) child.stdin.write(stdin);
|
|
6404
6576
|
child.stdin.end();
|
|
@@ -6436,7 +6608,7 @@ function parseFindings(results, rootDir) {
|
|
|
6436
6608
|
const metadata = r.extra?.metadata;
|
|
6437
6609
|
return {
|
|
6438
6610
|
ruleId: r.check_id ?? "unknown",
|
|
6439
|
-
path: r.path ?
|
|
6611
|
+
path: r.path ? path20.relative(rootDir, path20.resolve(rootDir, r.path)) : "",
|
|
6440
6612
|
line: r.start?.line ?? 1,
|
|
6441
6613
|
endLine: r.end?.line,
|
|
6442
6614
|
message: r.extra?.message ?? "Potential security issue",
|
|
@@ -6496,7 +6668,7 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
|
|
|
6496
6668
|
}
|
|
6497
6669
|
}
|
|
6498
6670
|
const entries = cache ? await cache.walkDir(rootDir) : [];
|
|
6499
|
-
const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(
|
|
6671
|
+
const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path20.extname(e.name).toLowerCase()));
|
|
6500
6672
|
const findings = [];
|
|
6501
6673
|
const errors = [];
|
|
6502
6674
|
let scannedFiles = 0;
|
|
@@ -6528,8 +6700,223 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
|
|
|
6528
6700
|
};
|
|
6529
6701
|
}
|
|
6530
6702
|
|
|
6703
|
+
// src/scanners/ui-purpose.ts
|
|
6704
|
+
var UI_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6705
|
+
".tsx",
|
|
6706
|
+
".jsx",
|
|
6707
|
+
".ts",
|
|
6708
|
+
".js",
|
|
6709
|
+
".vue",
|
|
6710
|
+
".svelte",
|
|
6711
|
+
".html",
|
|
6712
|
+
".mdx",
|
|
6713
|
+
".json",
|
|
6714
|
+
".yml",
|
|
6715
|
+
".yaml"
|
|
6716
|
+
]);
|
|
6717
|
+
var HIGH_SIGNAL_DEPENDENCIES = /* @__PURE__ */ new Set([
|
|
6718
|
+
"stripe",
|
|
6719
|
+
"posthog-js",
|
|
6720
|
+
"posthog-node",
|
|
6721
|
+
"@sentry/node",
|
|
6722
|
+
"@sentry/browser",
|
|
6723
|
+
"@sentry/react",
|
|
6724
|
+
"next-auth",
|
|
6725
|
+
"firebase",
|
|
6726
|
+
"auth0",
|
|
6727
|
+
"@auth0/auth0-react",
|
|
6728
|
+
"@supabase/supabase-js",
|
|
6729
|
+
"supabase",
|
|
6730
|
+
"clerk",
|
|
6731
|
+
"@clerk/clerk-js"
|
|
6732
|
+
]);
|
|
6733
|
+
var GENERIC_LOW_SIGNAL = /* @__PURE__ */ new Set(["welcome", "home", "click here", "learn more", "submit", "cancel"]);
|
|
6734
|
+
async function scanUiPurpose(rootDir, fileCache, maxItems = 300) {
|
|
6735
|
+
const entries = await fileCache.walkDir(rootDir);
|
|
6736
|
+
const files = entries.filter((e) => e.isFile);
|
|
6737
|
+
const packageJsonEntry = files.find((e) => e.relPath === "package.json");
|
|
6738
|
+
let packageJson = {};
|
|
6739
|
+
if (packageJsonEntry) {
|
|
6740
|
+
try {
|
|
6741
|
+
packageJson = JSON.parse(await fileCache.readTextFile(packageJsonEntry.absPath));
|
|
6742
|
+
} catch {
|
|
6743
|
+
packageJson = {};
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
const frameworks = detectFrameworks(packageJson);
|
|
6747
|
+
const items = [];
|
|
6748
|
+
for (const entry of files) {
|
|
6749
|
+
const ext = extension(entry.relPath);
|
|
6750
|
+
if (!UI_EXTENSIONS.has(ext)) continue;
|
|
6751
|
+
if (!isLikelyUiPath(entry.relPath)) {
|
|
6752
|
+
const routeHints2 = extractRouteHints(entry.relPath, frameworks);
|
|
6753
|
+
if (routeHints2.length > 0) {
|
|
6754
|
+
items.push(...routeHints2.map((r) => ({ ...r, file: entry.relPath })));
|
|
6755
|
+
}
|
|
6756
|
+
continue;
|
|
6757
|
+
}
|
|
6758
|
+
const routeHints = extractRouteHints(entry.relPath, frameworks);
|
|
6759
|
+
if (routeHints.length > 0) {
|
|
6760
|
+
items.push(...routeHints.map((r) => ({ ...r, file: entry.relPath })));
|
|
6761
|
+
}
|
|
6762
|
+
const src = await fileCache.readTextFile(entry.absPath);
|
|
6763
|
+
if (!src || src.length > 512e3) continue;
|
|
6764
|
+
const strings = extractUiStrings(src);
|
|
6765
|
+
for (const text of strings) {
|
|
6766
|
+
const kind = classifyString(text);
|
|
6767
|
+
const weight = scoreString(kind, text);
|
|
6768
|
+
items.push({ kind, value: text, file: entry.relPath, weight });
|
|
6769
|
+
}
|
|
6770
|
+
if (/(featureFlag|FEATURE_FLAG|launchDarkly|isFeatureEnabled|flags\.)/.test(src)) {
|
|
6771
|
+
items.push({ kind: "feature_flag", value: `flags in ${entry.relPath}`, file: entry.relPath, weight: 2 });
|
|
6772
|
+
}
|
|
6773
|
+
}
|
|
6774
|
+
const deps = getDependencies(packageJson);
|
|
6775
|
+
for (const [name, version] of deps) {
|
|
6776
|
+
if (HIGH_SIGNAL_DEPENDENCIES.has(name)) {
|
|
6777
|
+
items.push({ kind: "dependency", value: `${name}@${version}`, file: "package.json", weight: 4 });
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
6780
|
+
const deduped = dedupeByKindValue(items).sort((a, b) => b.weight - a.weight || a.value.localeCompare(b.value));
|
|
6781
|
+
const cappedItems = deduped.slice(0, maxItems);
|
|
6782
|
+
const unknownSignals = buildUnknowns(cappedItems);
|
|
6783
|
+
return {
|
|
6784
|
+
enabled: true,
|
|
6785
|
+
detectedFrameworks: frameworks,
|
|
6786
|
+
evidenceCount: deduped.length,
|
|
6787
|
+
capped: deduped.length > cappedItems.length,
|
|
6788
|
+
topEvidence: cappedItems,
|
|
6789
|
+
unknownSignals
|
|
6790
|
+
};
|
|
6791
|
+
}
|
|
6792
|
+
function extension(relPath) {
|
|
6793
|
+
const i = relPath.lastIndexOf(".");
|
|
6794
|
+
return i === -1 ? "" : relPath.slice(i).toLowerCase();
|
|
6795
|
+
}
|
|
6796
|
+
function isLikelyUiPath(relPath) {
|
|
6797
|
+
const lower = relPath.toLowerCase();
|
|
6798
|
+
return lower.startsWith("pages/") || lower.includes("/pages/") || lower.startsWith("app/") || lower.includes("/app/") || lower.startsWith("components/") || lower.includes("/components/") || lower.startsWith("ui/") || lower.includes("/ui/") || lower.startsWith("views/") || lower.includes("/views/") || lower.includes("/routes") || lower.startsWith("routes") || lower.includes("/router") || lower.startsWith("router") || lower.startsWith("locales/") || lower.includes("/locales/") || lower.startsWith("i18n/") || lower.includes("/i18n/") || lower.endsWith(".html") || lower.endsWith(".mdx");
|
|
6799
|
+
}
|
|
6800
|
+
function detectFrameworks(pkgJson) {
|
|
6801
|
+
const deps = {
|
|
6802
|
+
...asRecord(pkgJson.dependencies),
|
|
6803
|
+
...asRecord(pkgJson.devDependencies)
|
|
6804
|
+
};
|
|
6805
|
+
const hits = [];
|
|
6806
|
+
if (deps.next) hits.push("nextjs");
|
|
6807
|
+
if (deps.nuxt || deps.nuxt3) hits.push("nuxt");
|
|
6808
|
+
if (deps.react) hits.push("react");
|
|
6809
|
+
if (deps.vue) hits.push("vue");
|
|
6810
|
+
if (deps.svelte) hits.push("svelte");
|
|
6811
|
+
if (deps["@angular/core"]) hits.push("angular");
|
|
6812
|
+
return Array.from(new Set(hits));
|
|
6813
|
+
}
|
|
6814
|
+
function extractRouteHints(relPath, frameworks) {
|
|
6815
|
+
const items = [];
|
|
6816
|
+
if (frameworks.includes("nextjs")) {
|
|
6817
|
+
const m = relPath.match(/^(?:src\/)?(pages|app)\/(.+)\.(tsx|jsx|ts|js)$/);
|
|
6818
|
+
if (m) {
|
|
6819
|
+
let route = "/" + m[2].replace(/(^|\/)page$/i, "").replace(/(^|\/)route$/i, "").replace(/index$/i, "").replace(/\[(?:\.\.\.)?(.+?)\]/g, ":$1").replace(/\/+/g, "/");
|
|
6820
|
+
if (route !== "/" && route.endsWith("/")) route = route.slice(0, -1);
|
|
6821
|
+
items.push({ kind: "route", value: route || "/", weight: 5 });
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
if (/(^|\/)(routes?|router)\.(ts|js|json)$/.test(relPath) || relPath.includes("/router/")) {
|
|
6825
|
+
items.push({ kind: "route", value: `route config: ${relPath}`, weight: 3 });
|
|
6826
|
+
}
|
|
6827
|
+
return items;
|
|
6828
|
+
}
|
|
6829
|
+
function extractUiStrings(src) {
|
|
6830
|
+
const out = [];
|
|
6831
|
+
const textNodeRegex = />\s*([A-Za-z0-9][^<>]{2,120}?)\s*</g;
|
|
6832
|
+
for (const m of src.matchAll(textNodeRegex)) {
|
|
6833
|
+
const s = normaliseText(m[1] ?? "");
|
|
6834
|
+
if (isUsefulString(s)) out.push(s);
|
|
6835
|
+
}
|
|
6836
|
+
const titleRegex = /<title>\s*([^<]{2,120})\s*<\/title>|title\s*[:=]\s*["'`](.{2,120}?)["'`]/g;
|
|
6837
|
+
for (const m of src.matchAll(titleRegex)) {
|
|
6838
|
+
const s = normaliseText((m[1] ?? m[2] ?? "").trim());
|
|
6839
|
+
if (isUsefulString(s)) out.push(s);
|
|
6840
|
+
}
|
|
6841
|
+
const attrRegex = /(?:aria-label|label|placeholder|alt)\s*=\s*["'`](.{2,120}?)["'`]/g;
|
|
6842
|
+
for (const m of src.matchAll(attrRegex)) {
|
|
6843
|
+
const s = normaliseText(m[1] ?? "");
|
|
6844
|
+
if (isUsefulString(s)) out.push(s);
|
|
6845
|
+
}
|
|
6846
|
+
const jsonValueRegex = /:\s*["'`](.{2,140}?)["'`]\s*[,\n]/g;
|
|
6847
|
+
for (const m of src.matchAll(jsonValueRegex)) {
|
|
6848
|
+
const s = normaliseText(m[1] ?? "");
|
|
6849
|
+
if (isUsefulString(s)) out.push(s);
|
|
6850
|
+
}
|
|
6851
|
+
return out;
|
|
6852
|
+
}
|
|
6853
|
+
function classifyString(s) {
|
|
6854
|
+
const lower = s.toLowerCase();
|
|
6855
|
+
if (/(pricing|plan|billing|subscription|trial|credit)/.test(lower)) return "copy";
|
|
6856
|
+
if (/(sign in|sign up|log in|register|invite|workspace|organization|sso|oauth)/.test(lower)) return "copy";
|
|
6857
|
+
if (/(dashboard|reports|settings|integrations|users|roles|permissions)/.test(lower)) return "heading";
|
|
6858
|
+
if (/(get started|start|scan|generate|export|run|deploy|upgrade|analy[sz]e)/.test(lower)) return "cta";
|
|
6859
|
+
if (/(overview|features|about|documentation|docs)/.test(lower)) return "title";
|
|
6860
|
+
if (/(menu|navigation|sidebar|breadcrumb)/.test(lower)) return "nav";
|
|
6861
|
+
return "copy";
|
|
6862
|
+
}
|
|
6863
|
+
function scoreString(kind, value) {
|
|
6864
|
+
const lower = value.toLowerCase();
|
|
6865
|
+
if (GENERIC_LOW_SIGNAL.has(lower)) return 0;
|
|
6866
|
+
let score = 1;
|
|
6867
|
+
if (kind === "route") score += 4;
|
|
6868
|
+
if (kind === "nav") score += 3;
|
|
6869
|
+
if (kind === "title") score += 2;
|
|
6870
|
+
if (kind === "heading") score += 2;
|
|
6871
|
+
if (kind === "cta") score += 2;
|
|
6872
|
+
if (/(pricing|billing|subscription|auth|security|integration)/.test(lower)) score += 2;
|
|
6873
|
+
if (/(dashboard|report|scan|workspace|project|repository)/.test(lower)) score += 1;
|
|
6874
|
+
return score;
|
|
6875
|
+
}
|
|
6876
|
+
function dedupeByKindValue(items) {
|
|
6877
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6878
|
+
for (const item of items) {
|
|
6879
|
+
const key = `${item.kind}::${item.value.toLowerCase()}`;
|
|
6880
|
+
const prev = seen.get(key);
|
|
6881
|
+
if (!prev || item.weight > prev.weight) {
|
|
6882
|
+
seen.set(key, item);
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
return Array.from(seen.values()).filter((i) => i.weight > 0);
|
|
6886
|
+
}
|
|
6887
|
+
function buildUnknowns(items) {
|
|
6888
|
+
const unknowns = [];
|
|
6889
|
+
const hasPricing = items.some((i) => /pricing|billing|subscription|trial|credit/i.test(i.value));
|
|
6890
|
+
const hasAuth = items.some((i) => /sign in|sign up|login|auth|sso|oauth|invite/i.test(i.value));
|
|
6891
|
+
const hasIntegrations = items.some((i) => /integration|webhook|api key|connector/i.test(i.value));
|
|
6892
|
+
const hasRoutes = items.some((i) => i.kind === "route");
|
|
6893
|
+
if (!hasPricing) unknowns.push("No pricing or billing evidence found.");
|
|
6894
|
+
if (!hasAuth) unknowns.push("No authentication or user access flow evidence found.");
|
|
6895
|
+
if (!hasIntegrations) unknowns.push("No integrations/connectors evidence found.");
|
|
6896
|
+
if (!hasRoutes) unknowns.push("No route structure evidence found.");
|
|
6897
|
+
return unknowns;
|
|
6898
|
+
}
|
|
6899
|
+
function asRecord(value) {
|
|
6900
|
+
if (!value || typeof value !== "object") return {};
|
|
6901
|
+
return value;
|
|
6902
|
+
}
|
|
6903
|
+
function getDependencies(pkgJson) {
|
|
6904
|
+
return Object.entries(asRecord(pkgJson.dependencies));
|
|
6905
|
+
}
|
|
6906
|
+
function normaliseText(s) {
|
|
6907
|
+
return s.replace(/\s+/g, " ").trim();
|
|
6908
|
+
}
|
|
6909
|
+
function isUsefulString(s) {
|
|
6910
|
+
if (!s || s.length < 3 || s.length > 160) return false;
|
|
6911
|
+
if (/^[0-9_./-]+$/.test(s)) return false;
|
|
6912
|
+
if (/^(true|false|null|undefined)$/i.test(s)) return false;
|
|
6913
|
+
if (/[<>{}]/.test(s)) return false;
|
|
6914
|
+
if (/function\s*\(|=>|console\.|import\s+|export\s+/.test(s)) return false;
|
|
6915
|
+
return true;
|
|
6916
|
+
}
|
|
6917
|
+
|
|
6531
6918
|
// src/utils/tool-installer.ts
|
|
6532
|
-
import { spawn as
|
|
6919
|
+
import { spawn as spawn5 } from "child_process";
|
|
6533
6920
|
import chalk5 from "chalk";
|
|
6534
6921
|
var SECURITY_TOOLS = [
|
|
6535
6922
|
{ name: "semgrep", command: "semgrep", brew: "semgrep", winget: null, scoop: null, pip: "semgrep" },
|
|
@@ -6537,9 +6924,9 @@ var SECURITY_TOOLS = [
|
|
|
6537
6924
|
{ name: "trufflehog", command: "trufflehog", brew: "trufflehog", winget: null, scoop: "trufflehog", pip: null }
|
|
6538
6925
|
];
|
|
6539
6926
|
var IS_WIN = process.platform === "win32";
|
|
6540
|
-
function
|
|
6541
|
-
return new Promise((
|
|
6542
|
-
const child =
|
|
6927
|
+
function runCommand2(cmd, args) {
|
|
6928
|
+
return new Promise((resolve10) => {
|
|
6929
|
+
const child = spawn5(cmd, args, {
|
|
6543
6930
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6544
6931
|
shell: IS_WIN
|
|
6545
6932
|
// required for .cmd/.ps1 wrappers on Windows
|
|
@@ -6552,19 +6939,19 @@ function runCommand(cmd, args) {
|
|
|
6552
6939
|
child.stderr.on("data", (d) => {
|
|
6553
6940
|
stderr += d.toString();
|
|
6554
6941
|
});
|
|
6555
|
-
child.on("error", () =>
|
|
6556
|
-
child.on("close", (code) =>
|
|
6942
|
+
child.on("error", () => resolve10({ exitCode: 127, stdout, stderr }));
|
|
6943
|
+
child.on("close", (code) => resolve10({ exitCode: code ?? 1, stdout, stderr }));
|
|
6557
6944
|
});
|
|
6558
6945
|
}
|
|
6559
6946
|
async function commandExists(command) {
|
|
6560
6947
|
const checker = IS_WIN ? "where" : "which";
|
|
6561
|
-
const { exitCode } = await
|
|
6948
|
+
const { exitCode } = await runCommand2(checker, [command]);
|
|
6562
6949
|
return exitCode === 0;
|
|
6563
6950
|
}
|
|
6564
6951
|
async function tryInstall(tool, strategies, log) {
|
|
6565
6952
|
for (const { pm, args } of strategies) {
|
|
6566
6953
|
log(chalk5.dim(` ${pm} ${args.join(" ")}\u2026`));
|
|
6567
|
-
const { exitCode, stderr } = await
|
|
6954
|
+
const { exitCode, stderr } = await runCommand2(pm, args);
|
|
6568
6955
|
if (exitCode === 0) {
|
|
6569
6956
|
log(` ${chalk5.green("\u2714")} ${tool.name} installed via ${pm}`);
|
|
6570
6957
|
return { ok: true, pm };
|
|
@@ -6620,20 +7007,163 @@ async function installMissingTools(log = (m) => process.stderr.write(m + "\n"))
|
|
|
6620
7007
|
return result;
|
|
6621
7008
|
}
|
|
6622
7009
|
|
|
7010
|
+
// src/utils/mermaid.ts
|
|
7011
|
+
function sanitizeId(input) {
|
|
7012
|
+
return input.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
7013
|
+
}
|
|
7014
|
+
function escapeLabel(input) {
|
|
7015
|
+
return input.replace(/"/g, '\\"');
|
|
7016
|
+
}
|
|
7017
|
+
function scoreClass(score) {
|
|
7018
|
+
if (score === void 0 || Number.isNaN(score)) return "scoreUnknown";
|
|
7019
|
+
if (score >= 70) return "scoreHigh";
|
|
7020
|
+
if (score >= 40) return "scoreModerate";
|
|
7021
|
+
return "scoreLow";
|
|
7022
|
+
}
|
|
7023
|
+
function nodeLabel(project) {
|
|
7024
|
+
const score = project.drift?.score;
|
|
7025
|
+
const scoreText = typeof score === "number" ? ` (${score})` : " (n/a)";
|
|
7026
|
+
return `${project.name}${scoreText}`;
|
|
7027
|
+
}
|
|
7028
|
+
function buildDefs() {
|
|
7029
|
+
return [
|
|
7030
|
+
"classDef scoreHigh fill:#064e3b,stroke:#10b981,color:#d1fae5,stroke-width:2px",
|
|
7031
|
+
"classDef scoreModerate fill:#78350f,stroke:#f59e0b,color:#fef3c7,stroke-width:2px",
|
|
7032
|
+
"classDef scoreLow fill:#7f1d1d,stroke:#ef4444,color:#fee2e2,stroke-width:2px",
|
|
7033
|
+
"classDef scoreUnknown fill:#334155,stroke:#94a3b8,color:#e2e8f0,stroke-width:2px"
|
|
7034
|
+
];
|
|
7035
|
+
}
|
|
7036
|
+
function generateWorkspaceRelationshipMermaid(projects) {
|
|
7037
|
+
const lines = ["flowchart LR"];
|
|
7038
|
+
const byPath = new Map(projects.map((p) => [p.path, p]));
|
|
7039
|
+
for (const project of projects) {
|
|
7040
|
+
const id = sanitizeId(project.projectId || project.path || project.name);
|
|
7041
|
+
lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
|
|
7042
|
+
lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
|
|
7043
|
+
}
|
|
7044
|
+
for (const project of projects) {
|
|
7045
|
+
const fromId = sanitizeId(project.projectId || project.path || project.name);
|
|
7046
|
+
for (const ref of project.projectReferences ?? []) {
|
|
7047
|
+
const target = byPath.get(ref.path);
|
|
7048
|
+
if (!target) continue;
|
|
7049
|
+
const toId = sanitizeId(target.projectId || target.path || target.name);
|
|
7050
|
+
lines.push(`${fromId} --> ${toId}`);
|
|
7051
|
+
}
|
|
7052
|
+
}
|
|
7053
|
+
lines.push(...buildDefs());
|
|
7054
|
+
return { mermaid: lines.join("\n") };
|
|
7055
|
+
}
|
|
7056
|
+
function generateProjectRelationshipMermaid(project, projects) {
|
|
7057
|
+
const lines = ["flowchart LR"];
|
|
7058
|
+
const byPath = new Map(projects.map((p) => [p.path, p]));
|
|
7059
|
+
const parents = projects.filter((p) => p.projectReferences?.some((r) => r.path === project.path));
|
|
7060
|
+
const children = (project.projectReferences ?? []).map((r) => byPath.get(r.path)).filter((p) => Boolean(p));
|
|
7061
|
+
const centerId = sanitizeId(project.projectId || project.path || project.name);
|
|
7062
|
+
lines.push(`${centerId}["${escapeLabel(nodeLabel(project))}"]`);
|
|
7063
|
+
lines.push(`class ${centerId} ${scoreClass(project.drift?.score)}`);
|
|
7064
|
+
for (const parent of parents) {
|
|
7065
|
+
const id = sanitizeId(parent.projectId || parent.path || parent.name);
|
|
7066
|
+
lines.push(`${id}["${escapeLabel(nodeLabel(parent))}"]`);
|
|
7067
|
+
lines.push(`class ${id} ${scoreClass(parent.drift?.score)}`);
|
|
7068
|
+
lines.push(`${id} --> ${centerId}`);
|
|
7069
|
+
}
|
|
7070
|
+
for (const child of children) {
|
|
7071
|
+
const id = sanitizeId(child.projectId || child.path || child.name);
|
|
7072
|
+
lines.push(`${id}["${escapeLabel(nodeLabel(child))}"]`);
|
|
7073
|
+
lines.push(`class ${id} ${scoreClass(child.drift?.score)}`);
|
|
7074
|
+
lines.push(`${centerId} --> ${id}`);
|
|
7075
|
+
}
|
|
7076
|
+
lines.push(...buildDefs());
|
|
7077
|
+
return { mermaid: lines.join("\n") };
|
|
7078
|
+
}
|
|
7079
|
+
function generateSolutionRelationshipMermaid(solution, projects) {
|
|
7080
|
+
const lines = ["flowchart TB"];
|
|
7081
|
+
const solutionNodeId = sanitizeId(solution.solutionId || solution.path || solution.name);
|
|
7082
|
+
const solutionScore = solution.drift?.score;
|
|
7083
|
+
const solutionScoreText = typeof solutionScore === "number" ? ` (${solutionScore})` : " (n/a)";
|
|
7084
|
+
lines.push(`${solutionNodeId}["${escapeLabel(`${solution.name}${solutionScoreText}`)}"]`);
|
|
7085
|
+
lines.push(`class ${solutionNodeId} ${scoreClass(solutionScore)}`);
|
|
7086
|
+
const projectByPath = new Map(projects.map((p) => [p.path, p]));
|
|
7087
|
+
for (const projectPath of solution.projectPaths) {
|
|
7088
|
+
const project = projectByPath.get(projectPath);
|
|
7089
|
+
if (!project) continue;
|
|
7090
|
+
const projectNodeId = sanitizeId(project.projectId || project.path || project.name);
|
|
7091
|
+
lines.push(`${projectNodeId}["${escapeLabel(nodeLabel(project))}"]`);
|
|
7092
|
+
lines.push(`class ${projectNodeId} ${scoreClass(project.drift?.score)}`);
|
|
7093
|
+
lines.push(`${solutionNodeId} --> ${projectNodeId}`);
|
|
7094
|
+
for (const ref of project.projectReferences ?? []) {
|
|
7095
|
+
const target = projectByPath.get(ref.path);
|
|
7096
|
+
if (!target) continue;
|
|
7097
|
+
const toId = sanitizeId(target.projectId || target.path || target.name);
|
|
7098
|
+
lines.push(`${projectNodeId} --> ${toId}`);
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
lines.push(...buildDefs());
|
|
7102
|
+
return { mermaid: lines.join("\n") };
|
|
7103
|
+
}
|
|
7104
|
+
|
|
6623
7105
|
// src/commands/scan.ts
|
|
7106
|
+
async function discoverSolutions(rootDir, fileCache) {
|
|
7107
|
+
const solutionFiles = await fileCache.findSolutionFiles(rootDir);
|
|
7108
|
+
const parsed = [];
|
|
7109
|
+
for (const solutionFile of solutionFiles) {
|
|
7110
|
+
try {
|
|
7111
|
+
const content = await fileCache.readTextFile(solutionFile);
|
|
7112
|
+
const dir = path21.dirname(solutionFile);
|
|
7113
|
+
const relSolutionPath = path21.relative(rootDir, solutionFile).replace(/\\/g, "/");
|
|
7114
|
+
const projectPaths = /* @__PURE__ */ new Set();
|
|
7115
|
+
const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.csproj)"/g;
|
|
7116
|
+
let match;
|
|
7117
|
+
while ((match = projectRegex.exec(content)) !== null) {
|
|
7118
|
+
const projectRelative = match[2];
|
|
7119
|
+
const absProjectPath = path21.resolve(dir, projectRelative.replace(/\\/g, "/"));
|
|
7120
|
+
projectPaths.add(path21.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
|
|
7121
|
+
}
|
|
7122
|
+
const solutionName = path21.basename(solutionFile, path21.extname(solutionFile));
|
|
7123
|
+
parsed.push({
|
|
7124
|
+
path: relSolutionPath,
|
|
7125
|
+
name: solutionName,
|
|
7126
|
+
type: "dotnet-sln",
|
|
7127
|
+
projectPaths: [...projectPaths]
|
|
7128
|
+
});
|
|
7129
|
+
} catch {
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
return parsed;
|
|
7133
|
+
}
|
|
6624
7134
|
async function runScan(rootDir, opts) {
|
|
6625
7135
|
const scanStart = Date.now();
|
|
6626
7136
|
const config = await loadConfig(rootDir);
|
|
6627
7137
|
const sem = new Semaphore(opts.concurrency);
|
|
6628
|
-
const
|
|
6629
|
-
const
|
|
6630
|
-
const
|
|
6631
|
-
const
|
|
7138
|
+
const packageManifest = opts.packageManifest ? await loadPackageVersionManifest(opts.packageManifest) : void 0;
|
|
7139
|
+
const offlineMode = opts.offline === true;
|
|
7140
|
+
const maxPrivacyMode = opts.maxPrivacy === true;
|
|
7141
|
+
const npmCache = new NpmCache(rootDir, sem, packageManifest, offlineMode);
|
|
7142
|
+
const nugetCache = new NuGetCache(sem, packageManifest, offlineMode);
|
|
7143
|
+
const pypiCache = new PyPICache(sem, packageManifest, offlineMode);
|
|
7144
|
+
const mavenCache = new MavenCache(sem, packageManifest, offlineMode);
|
|
6632
7145
|
const fileCache = new FileCache();
|
|
6633
7146
|
const excludePatterns = config.exclude ?? [];
|
|
6634
7147
|
fileCache.setExcludePatterns(excludePatterns);
|
|
6635
7148
|
fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
|
|
6636
7149
|
const scanners = config.scanners;
|
|
7150
|
+
const scannerPolicy = {
|
|
7151
|
+
platformMatrix: !maxPrivacyMode,
|
|
7152
|
+
toolingInventory: true,
|
|
7153
|
+
serviceDependencies: !maxPrivacyMode,
|
|
7154
|
+
breakingChangeExposure: !maxPrivacyMode,
|
|
7155
|
+
securityPosture: true,
|
|
7156
|
+
securityScanners: !maxPrivacyMode,
|
|
7157
|
+
buildDeploy: !maxPrivacyMode,
|
|
7158
|
+
tsModernity: !maxPrivacyMode,
|
|
7159
|
+
fileHotspots: !maxPrivacyMode,
|
|
7160
|
+
dependencyGraph: true,
|
|
7161
|
+
dependencyRisk: true,
|
|
7162
|
+
architecture: !maxPrivacyMode,
|
|
7163
|
+
codeQuality: !maxPrivacyMode,
|
|
7164
|
+
owaspCategoryMapping: !maxPrivacyMode,
|
|
7165
|
+
uiPurpose: !maxPrivacyMode
|
|
7166
|
+
};
|
|
6637
7167
|
let filesScanned = 0;
|
|
6638
7168
|
const progress = new ScanProgress(rootDir);
|
|
6639
7169
|
const steps = [
|
|
@@ -6646,27 +7176,28 @@ async function runScan(rootDir, opts) {
|
|
|
6646
7176
|
{ id: "python", label: "Scanning Python projects", weight: 3 },
|
|
6647
7177
|
{ id: "java", label: "Scanning Java projects", weight: 3 },
|
|
6648
7178
|
...scanners !== false ? [
|
|
6649
|
-
...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
6650
|
-
...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
6651
|
-
...scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
|
|
6652
|
-
...scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
|
|
6653
|
-
...scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
|
|
6654
|
-
...scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
|
|
6655
|
-
...scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
|
|
6656
|
-
...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
6657
|
-
...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
6658
|
-
...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
6659
|
-
...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
6660
|
-
...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
|
|
6661
|
-
...scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
|
|
6662
|
-
...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : []
|
|
7179
|
+
...scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
7180
|
+
...scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
7181
|
+
...scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
|
|
7182
|
+
...scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
|
|
7183
|
+
...scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
|
|
7184
|
+
...scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
|
|
7185
|
+
...scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
|
|
7186
|
+
...scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
7187
|
+
...scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
7188
|
+
...scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
7189
|
+
...scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
7190
|
+
...scannerPolicy.architecture && scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
|
|
7191
|
+
...scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
|
|
7192
|
+
...scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
|
|
7193
|
+
...!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
|
|
6663
7194
|
] : [],
|
|
6664
7195
|
{ id: "drift", label: "Computing drift score" },
|
|
6665
7196
|
{ id: "findings", label: "Generating findings" }
|
|
6666
7197
|
];
|
|
6667
7198
|
progress.setSteps(steps);
|
|
6668
7199
|
progress.completeStep("config", "loaded");
|
|
6669
|
-
const registryOk = await checkRegistryAccess(rootDir);
|
|
7200
|
+
const registryOk = offlineMode ? true : await checkRegistryAccess(rootDir);
|
|
6670
7201
|
if (!registryOk) {
|
|
6671
7202
|
progress.finish();
|
|
6672
7203
|
const msg = [
|
|
@@ -6748,10 +7279,45 @@ async function runScan(rootDir, opts) {
|
|
|
6748
7279
|
project.drift = computeDriftScore([project]);
|
|
6749
7280
|
project.projectId = computeProjectId(project.path, project.name, workspaceId);
|
|
6750
7281
|
}
|
|
7282
|
+
const solutionsManifestPath = path21.join(rootDir, ".vibgrate", "solutions.json");
|
|
7283
|
+
const persistedSolutionIds = /* @__PURE__ */ new Map();
|
|
7284
|
+
if (await pathExists(solutionsManifestPath)) {
|
|
7285
|
+
try {
|
|
7286
|
+
const persisted = await readJsonFile(solutionsManifestPath);
|
|
7287
|
+
for (const solution of persisted.solutions ?? []) {
|
|
7288
|
+
if (solution.path && solution.solutionId) persistedSolutionIds.set(solution.path, solution.solutionId);
|
|
7289
|
+
}
|
|
7290
|
+
} catch {
|
|
7291
|
+
}
|
|
7292
|
+
}
|
|
7293
|
+
const discoveredSolutions = await discoverSolutions(rootDir, fileCache);
|
|
7294
|
+
const solutions = discoveredSolutions.map((solution) => ({
|
|
7295
|
+
solutionId: persistedSolutionIds.get(solution.path) ?? computeSolutionId(solution.path, solution.name, workspaceId),
|
|
7296
|
+
path: solution.path,
|
|
7297
|
+
name: solution.name,
|
|
7298
|
+
type: solution.type,
|
|
7299
|
+
projectPaths: solution.projectPaths
|
|
7300
|
+
}));
|
|
7301
|
+
const projectsByPath = new Map(allProjects.map((project) => [project.path, project]));
|
|
7302
|
+
for (const solution of solutions) {
|
|
7303
|
+
const includedProjects = solution.projectPaths.map((projectPath) => projectsByPath.get(projectPath)).filter((project) => Boolean(project));
|
|
7304
|
+
solution.drift = includedProjects.length > 0 ? computeDriftScore(includedProjects) : void 0;
|
|
7305
|
+
for (const project of includedProjects) {
|
|
7306
|
+
project.solutionId = solution.solutionId;
|
|
7307
|
+
project.solutionName = solution.name;
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
for (const project of allProjects) {
|
|
7311
|
+
project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
|
|
7312
|
+
}
|
|
7313
|
+
for (const solution of solutions) {
|
|
7314
|
+
solution.relationshipDiagram = generateSolutionRelationshipMermaid(solution, allProjects);
|
|
7315
|
+
}
|
|
7316
|
+
const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
|
|
6751
7317
|
const extended = {};
|
|
6752
7318
|
if (scanners !== false) {
|
|
6753
7319
|
const scannerTasks = [];
|
|
6754
|
-
if (scanners?.platformMatrix?.enabled !== false) {
|
|
7320
|
+
if (scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false) {
|
|
6755
7321
|
progress.startStep("platform");
|
|
6756
7322
|
scannerTasks.push(
|
|
6757
7323
|
scanPlatformMatrix(rootDir, fileCache).then((result) => {
|
|
@@ -6765,7 +7331,7 @@ async function runScan(rootDir, opts) {
|
|
|
6765
7331
|
})
|
|
6766
7332
|
);
|
|
6767
7333
|
}
|
|
6768
|
-
if (scanners?.toolingInventory?.enabled !== false) {
|
|
7334
|
+
if (scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false) {
|
|
6769
7335
|
progress.startStep("tooling");
|
|
6770
7336
|
scannerTasks.push(
|
|
6771
7337
|
Promise.resolve().then(() => {
|
|
@@ -6775,7 +7341,7 @@ async function runScan(rootDir, opts) {
|
|
|
6775
7341
|
})
|
|
6776
7342
|
);
|
|
6777
7343
|
}
|
|
6778
|
-
if (scanners?.serviceDependencies?.enabled !== false) {
|
|
7344
|
+
if (scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false) {
|
|
6779
7345
|
progress.startStep("services");
|
|
6780
7346
|
scannerTasks.push(
|
|
6781
7347
|
Promise.resolve().then(() => {
|
|
@@ -6785,7 +7351,7 @@ async function runScan(rootDir, opts) {
|
|
|
6785
7351
|
})
|
|
6786
7352
|
);
|
|
6787
7353
|
}
|
|
6788
|
-
if (scanners?.breakingChangeExposure?.enabled !== false) {
|
|
7354
|
+
if (scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false) {
|
|
6789
7355
|
progress.startStep("breaking");
|
|
6790
7356
|
scannerTasks.push(
|
|
6791
7357
|
Promise.resolve().then(() => {
|
|
@@ -6800,7 +7366,7 @@ async function runScan(rootDir, opts) {
|
|
|
6800
7366
|
})
|
|
6801
7367
|
);
|
|
6802
7368
|
}
|
|
6803
|
-
if (scanners?.securityPosture?.enabled !== false) {
|
|
7369
|
+
if (scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false) {
|
|
6804
7370
|
progress.startStep("security");
|
|
6805
7371
|
scannerTasks.push(
|
|
6806
7372
|
scanSecurityPosture(rootDir, fileCache).then((result) => {
|
|
@@ -6810,7 +7376,7 @@ async function runScan(rootDir, opts) {
|
|
|
6810
7376
|
})
|
|
6811
7377
|
);
|
|
6812
7378
|
}
|
|
6813
|
-
if (scanners?.securityScanners?.enabled !== false) {
|
|
7379
|
+
if (scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false) {
|
|
6814
7380
|
if (opts.installTools) {
|
|
6815
7381
|
const installResult = await installMissingTools();
|
|
6816
7382
|
if (installResult.installed.length > 0) {
|
|
@@ -6833,7 +7399,7 @@ async function runScan(rootDir, opts) {
|
|
|
6833
7399
|
})
|
|
6834
7400
|
);
|
|
6835
7401
|
}
|
|
6836
|
-
if (scanners?.buildDeploy?.enabled !== false) {
|
|
7402
|
+
if (scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false) {
|
|
6837
7403
|
progress.startStep("build");
|
|
6838
7404
|
scannerTasks.push(
|
|
6839
7405
|
scanBuildDeploy(rootDir, fileCache).then((result) => {
|
|
@@ -6845,7 +7411,7 @@ async function runScan(rootDir, opts) {
|
|
|
6845
7411
|
})
|
|
6846
7412
|
);
|
|
6847
7413
|
}
|
|
6848
|
-
if (scanners?.tsModernity?.enabled !== false) {
|
|
7414
|
+
if (scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false) {
|
|
6849
7415
|
progress.startStep("ts");
|
|
6850
7416
|
scannerTasks.push(
|
|
6851
7417
|
scanTsModernity(rootDir, fileCache).then((result) => {
|
|
@@ -6858,7 +7424,7 @@ async function runScan(rootDir, opts) {
|
|
|
6858
7424
|
})
|
|
6859
7425
|
);
|
|
6860
7426
|
}
|
|
6861
|
-
if (scanners?.fileHotspots?.enabled !== false) {
|
|
7427
|
+
if (scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false) {
|
|
6862
7428
|
progress.startStep("hotspots");
|
|
6863
7429
|
scannerTasks.push(
|
|
6864
7430
|
scanFileHotspots(rootDir, fileCache).then((result) => {
|
|
@@ -6867,7 +7433,7 @@ async function runScan(rootDir, opts) {
|
|
|
6867
7433
|
})
|
|
6868
7434
|
);
|
|
6869
7435
|
}
|
|
6870
|
-
if (scanners?.dependencyGraph?.enabled !== false) {
|
|
7436
|
+
if (scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false) {
|
|
6871
7437
|
progress.startStep("depgraph");
|
|
6872
7438
|
scannerTasks.push(
|
|
6873
7439
|
scanDependencyGraph(rootDir, fileCache).then((result) => {
|
|
@@ -6877,7 +7443,7 @@ async function runScan(rootDir, opts) {
|
|
|
6877
7443
|
})
|
|
6878
7444
|
);
|
|
6879
7445
|
}
|
|
6880
|
-
if (scanners?.codeQuality?.enabled !== false) {
|
|
7446
|
+
if (scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false) {
|
|
6881
7447
|
progress.startStep("codequality");
|
|
6882
7448
|
scannerTasks.push(
|
|
6883
7449
|
scanCodeQuality(rootDir, fileCache).then((result) => {
|
|
@@ -6890,7 +7456,7 @@ async function runScan(rootDir, opts) {
|
|
|
6890
7456
|
})
|
|
6891
7457
|
);
|
|
6892
7458
|
}
|
|
6893
|
-
if (scanners?.dependencyRisk?.enabled !== false) {
|
|
7459
|
+
if (scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false) {
|
|
6894
7460
|
progress.startStep("deprisk");
|
|
6895
7461
|
scannerTasks.push(
|
|
6896
7462
|
Promise.resolve().then(() => {
|
|
@@ -6904,7 +7470,7 @@ async function runScan(rootDir, opts) {
|
|
|
6904
7470
|
);
|
|
6905
7471
|
}
|
|
6906
7472
|
await Promise.all(scannerTasks);
|
|
6907
|
-
if (scanners?.owaspCategoryMapping?.enabled !== false) {
|
|
7473
|
+
if (scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false) {
|
|
6908
7474
|
progress.startStep("owasp");
|
|
6909
7475
|
extended.owaspCategoryMapping = await scanOwaspCategoryMapping(
|
|
6910
7476
|
rootDir,
|
|
@@ -6923,7 +7489,14 @@ async function runScan(rootDir, opts) {
|
|
|
6923
7489
|
);
|
|
6924
7490
|
}
|
|
6925
7491
|
}
|
|
6926
|
-
if (scanners?.
|
|
7492
|
+
if (!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true)) {
|
|
7493
|
+
progress.startStep("uipurpose");
|
|
7494
|
+
extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
|
|
7495
|
+
const up = extended.uiPurpose;
|
|
7496
|
+
const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
|
|
7497
|
+
progress.completeStep("uipurpose", summary, up.topEvidence.length);
|
|
7498
|
+
}
|
|
7499
|
+
if (scannerPolicy.architecture && scanners?.architecture?.enabled !== false) {
|
|
6927
7500
|
progress.startStep("architecture");
|
|
6928
7501
|
extended.architecture = await scanArchitecture(
|
|
6929
7502
|
rootDir,
|
|
@@ -6934,6 +7507,14 @@ async function runScan(rootDir, opts) {
|
|
|
6934
7507
|
);
|
|
6935
7508
|
const arch = extended.architecture;
|
|
6936
7509
|
const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
|
|
7510
|
+
await Promise.all(allProjects.map(async (project) => {
|
|
7511
|
+
project.architectureMermaid = await buildProjectArchitectureMermaid(
|
|
7512
|
+
rootDir,
|
|
7513
|
+
project,
|
|
7514
|
+
arch.archetype,
|
|
7515
|
+
fileCache
|
|
7516
|
+
);
|
|
7517
|
+
}));
|
|
6937
7518
|
progress.completeStep(
|
|
6938
7519
|
"architecture",
|
|
6939
7520
|
`${arch.archetype} \xB7 ${layerCount} layer${layerCount !== 1 ? "s" : ""} \xB7 ${arch.totalClassified} files`,
|
|
@@ -7001,23 +7582,28 @@ async function runScan(rootDir, opts) {
|
|
|
7001
7582
|
}
|
|
7002
7583
|
if (extended.codeQuality) filesScanned += extended.codeQuality.filesAnalyzed;
|
|
7003
7584
|
if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
|
|
7585
|
+
if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
|
|
7004
7586
|
const durationMs = Date.now() - scanStart;
|
|
7587
|
+
const repository = await buildRepositoryInfo(rootDir, vcs.remoteUrl, extended.buildDeploy?.ci);
|
|
7005
7588
|
const artifact = {
|
|
7006
7589
|
schemaVersion: "1.0",
|
|
7007
7590
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7008
7591
|
vibgrateVersion: VERSION,
|
|
7009
|
-
rootPath:
|
|
7592
|
+
rootPath: path21.basename(rootDir),
|
|
7010
7593
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
7594
|
+
repository,
|
|
7011
7595
|
projects: allProjects,
|
|
7596
|
+
...solutions.length > 0 ? { solutions } : {},
|
|
7012
7597
|
drift,
|
|
7013
7598
|
findings,
|
|
7014
7599
|
...Object.keys(extended).length > 0 ? { extended } : {},
|
|
7015
7600
|
durationMs,
|
|
7016
7601
|
filesScanned,
|
|
7017
|
-
treeSummary: treeCount
|
|
7602
|
+
treeSummary: treeCount,
|
|
7603
|
+
relationshipDiagram
|
|
7018
7604
|
};
|
|
7019
7605
|
if (opts.baseline) {
|
|
7020
|
-
const baselinePath =
|
|
7606
|
+
const baselinePath = path21.resolve(opts.baseline);
|
|
7021
7607
|
if (await pathExists(baselinePath)) {
|
|
7022
7608
|
try {
|
|
7023
7609
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -7028,9 +7614,21 @@ async function runScan(rootDir, opts) {
|
|
|
7028
7614
|
}
|
|
7029
7615
|
}
|
|
7030
7616
|
}
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7617
|
+
if (!opts.noLocalArtifacts && !maxPrivacyMode) {
|
|
7618
|
+
const vibgrateDir = path21.join(rootDir, ".vibgrate");
|
|
7619
|
+
await ensureDir(vibgrateDir);
|
|
7620
|
+
await writeJsonFile(path21.join(vibgrateDir, "scan_result.json"), artifact);
|
|
7621
|
+
await writeJsonFile(path21.join(vibgrateDir, "solutions.json"), {
|
|
7622
|
+
scannedAt: artifact.timestamp,
|
|
7623
|
+
solutions: solutions.map((solution) => ({
|
|
7624
|
+
solutionId: solution.solutionId,
|
|
7625
|
+
name: solution.name,
|
|
7626
|
+
path: solution.path,
|
|
7627
|
+
type: solution.type,
|
|
7628
|
+
projectPaths: solution.projectPaths
|
|
7629
|
+
}))
|
|
7630
|
+
});
|
|
7631
|
+
}
|
|
7034
7632
|
await saveScanHistory(rootDir, {
|
|
7035
7633
|
timestamp: artifact.timestamp,
|
|
7036
7634
|
totalDurationMs: durationMs,
|
|
@@ -7038,29 +7636,33 @@ async function runScan(rootDir, opts) {
|
|
|
7038
7636
|
totalDirs: treeCount.totalDirs,
|
|
7039
7637
|
steps: progress.getStepTimings()
|
|
7040
7638
|
});
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7639
|
+
if (!opts.noLocalArtifacts && !maxPrivacyMode) {
|
|
7640
|
+
for (const project of allProjects) {
|
|
7641
|
+
if (project.drift && project.path) {
|
|
7642
|
+
const projectDir = path21.resolve(rootDir, project.path);
|
|
7643
|
+
const projectVibgrateDir = path21.join(projectDir, ".vibgrate");
|
|
7644
|
+
await ensureDir(projectVibgrateDir);
|
|
7645
|
+
await writeJsonFile(path21.join(projectVibgrateDir, "project_score.json"), {
|
|
7646
|
+
projectId: project.projectId,
|
|
7647
|
+
name: project.name,
|
|
7648
|
+
type: project.type,
|
|
7649
|
+
path: project.path,
|
|
7650
|
+
score: project.drift.score,
|
|
7651
|
+
riskLevel: project.drift.riskLevel,
|
|
7652
|
+
components: project.drift.components,
|
|
7653
|
+
measured: project.drift.measured,
|
|
7654
|
+
scannedAt: artifact.timestamp,
|
|
7655
|
+
vibgrateVersion: VERSION,
|
|
7656
|
+
solutionId: project.solutionId,
|
|
7657
|
+
solutionName: project.solutionName
|
|
7658
|
+
});
|
|
7659
|
+
}
|
|
7058
7660
|
}
|
|
7059
7661
|
}
|
|
7060
7662
|
if (opts.format === "json") {
|
|
7061
7663
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
7062
7664
|
if (opts.out) {
|
|
7063
|
-
await writeTextFile(
|
|
7665
|
+
await writeTextFile(path21.resolve(opts.out), jsonStr);
|
|
7064
7666
|
console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
7065
7667
|
} else {
|
|
7066
7668
|
console.log(jsonStr);
|
|
@@ -7069,7 +7671,7 @@ async function runScan(rootDir, opts) {
|
|
|
7069
7671
|
const sarif = formatSarif(artifact);
|
|
7070
7672
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
7071
7673
|
if (opts.out) {
|
|
7072
|
-
await writeTextFile(
|
|
7674
|
+
await writeTextFile(path21.resolve(opts.out), sarifStr);
|
|
7073
7675
|
console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
7074
7676
|
} else {
|
|
7075
7677
|
console.log(sarifStr);
|
|
@@ -7078,11 +7680,34 @@ async function runScan(rootDir, opts) {
|
|
|
7078
7680
|
const text = formatText(artifact);
|
|
7079
7681
|
console.log(text);
|
|
7080
7682
|
if (opts.out) {
|
|
7081
|
-
await writeTextFile(
|
|
7683
|
+
await writeTextFile(path21.resolve(opts.out), text);
|
|
7082
7684
|
}
|
|
7083
7685
|
}
|
|
7084
7686
|
return artifact;
|
|
7085
7687
|
}
|
|
7688
|
+
async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
|
|
7689
|
+
const packageJsonPath = path21.join(rootDir, "package.json");
|
|
7690
|
+
let name = path21.basename(rootDir);
|
|
7691
|
+
let version;
|
|
7692
|
+
if (await pathExists(packageJsonPath)) {
|
|
7693
|
+
try {
|
|
7694
|
+
const packageJson = await readJsonFile(packageJsonPath);
|
|
7695
|
+
if (typeof packageJson.name === "string" && packageJson.name.trim()) {
|
|
7696
|
+
name = packageJson.name.trim();
|
|
7697
|
+
}
|
|
7698
|
+
if (typeof packageJson.version === "string" && packageJson.version.trim()) {
|
|
7699
|
+
version = packageJson.version.trim();
|
|
7700
|
+
}
|
|
7701
|
+
} catch {
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
return {
|
|
7705
|
+
name,
|
|
7706
|
+
...version ? { version } : {},
|
|
7707
|
+
...ciSystems && ciSystems.length > 0 ? { pipeline: ciSystems.join(",") } : {},
|
|
7708
|
+
...remoteUrl ? { remoteUrl } : {}
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7086
7711
|
async function autoPush(artifact, rootDir, opts) {
|
|
7087
7712
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
7088
7713
|
if (!dsn) {
|
|
@@ -7145,8 +7770,16 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
7145
7770
|
if (opts.strict) process.exit(1);
|
|
7146
7771
|
}
|
|
7147
7772
|
}
|
|
7148
|
-
|
|
7149
|
-
|
|
7773
|
+
function parseNonNegativeNumber(value, label) {
|
|
7774
|
+
if (value === void 0) return void 0;
|
|
7775
|
+
const parsed = Number(value);
|
|
7776
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
7777
|
+
throw new Error(`${label} must be a non-negative number.`);
|
|
7778
|
+
}
|
|
7779
|
+
return parsed;
|
|
7780
|
+
}
|
|
7781
|
+
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
|
|
7782
|
+
const rootDir = path21.resolve(targetPath);
|
|
7150
7783
|
if (!await pathExists(rootDir)) {
|
|
7151
7784
|
console.error(chalk6.red(`Path does not exist: ${rootDir}`));
|
|
7152
7785
|
process.exit(1);
|
|
@@ -7162,7 +7795,14 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
7162
7795
|
dsn: opts.dsn,
|
|
7163
7796
|
region: opts.region,
|
|
7164
7797
|
strict: opts.strict,
|
|
7165
|
-
installTools: opts.installTools
|
|
7798
|
+
installTools: opts.installTools,
|
|
7799
|
+
uiPurpose: opts.uiPurpose,
|
|
7800
|
+
noLocalArtifacts: opts.noLocalArtifacts,
|
|
7801
|
+
maxPrivacy: opts.maxPrivacy,
|
|
7802
|
+
offline: opts.offline,
|
|
7803
|
+
packageManifest: opts.packageManifest,
|
|
7804
|
+
driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
|
|
7805
|
+
driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
|
|
7166
7806
|
};
|
|
7167
7807
|
const artifact = await runScan(rootDir, scanOpts);
|
|
7168
7808
|
if (opts.failOn) {
|
|
@@ -7179,8 +7819,29 @@ Failing: findings detected at warn level or above.`));
|
|
|
7179
7819
|
process.exit(2);
|
|
7180
7820
|
}
|
|
7181
7821
|
}
|
|
7822
|
+
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
7823
|
+
console.error(chalk6.red(`
|
|
7824
|
+
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
7825
|
+
process.exit(2);
|
|
7826
|
+
}
|
|
7827
|
+
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
7828
|
+
if (artifact.delta === void 0) {
|
|
7829
|
+
console.error(chalk6.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
7830
|
+
process.exit(2);
|
|
7831
|
+
}
|
|
7832
|
+
if (artifact.delta > 0) {
|
|
7833
|
+
const baselineScore = artifact.drift.score - artifact.delta;
|
|
7834
|
+
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
7835
|
+
const worseningPercent = artifact.delta / denominator * 100;
|
|
7836
|
+
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
7837
|
+
console.error(chalk6.red(`
|
|
7838
|
+
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
7839
|
+
process.exit(2);
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7182
7843
|
const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
|
|
7183
|
-
if (opts.push || hasDsn) {
|
|
7844
|
+
if (!scanOpts.offline && (opts.push || hasDsn)) {
|
|
7184
7845
|
await autoPush(artifact, rootDir, scanOpts);
|
|
7185
7846
|
}
|
|
7186
7847
|
});
|