@vibgrate/cli 1.0.44 → 1.0.46
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 +202 -89
- package/README.md +150 -209
- package/dist/{baseline-FDWMBM2O.js → baseline-AWRL3ITR.js} +2 -2
- package/dist/{chunk-YFJC5JSQ.js → chunk-HEILEAVO.js} +442 -127
- package/dist/{chunk-GN3IWKSY.js → chunk-PTMLMDZU.js} +20 -0
- package/dist/{chunk-LO66M6OC.js → chunk-SKROLJET.js} +1 -1
- package/dist/cli.js +190 -10
- package/dist/index.d.ts +32 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -315,6 +315,15 @@ function formatText(artifact) {
|
|
|
315
315
|
if (artifact.extended?.architecture) {
|
|
316
316
|
lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
|
|
317
317
|
}
|
|
318
|
+
if (artifact.relationshipDiagram?.mermaid) {
|
|
319
|
+
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
|
+
lines.push(chalk.bold.cyan("\u2551 Project Relationship Diagram \u2551"));
|
|
321
|
+
lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
322
|
+
lines.push("");
|
|
323
|
+
lines.push(chalk.bold(" Mermaid"));
|
|
324
|
+
lines.push(chalk.dim(artifact.relationshipDiagram.mermaid));
|
|
325
|
+
lines.push("");
|
|
326
|
+
}
|
|
318
327
|
const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
|
|
319
328
|
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
329
|
lines.push(chalk.bold.cyan("\u2551 Drift Score Summary \u2551"));
|
|
@@ -469,6 +478,26 @@ function formatExtended(ext) {
|
|
|
469
478
|
lines.push("");
|
|
470
479
|
}
|
|
471
480
|
}
|
|
481
|
+
if (ext.uiPurpose) {
|
|
482
|
+
const up = ext.uiPurpose;
|
|
483
|
+
lines.push(chalk.bold.underline(" Product Purpose Signals"));
|
|
484
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk.dim("unknown")}`);
|
|
485
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
486
|
+
const top = up.topEvidence.slice(0, 8);
|
|
487
|
+
if (top.length > 0) {
|
|
488
|
+
lines.push(" Top Signals:");
|
|
489
|
+
for (const item of top) {
|
|
490
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk.dim(`(${item.file})`)}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (up.unknownSignals.length > 0) {
|
|
494
|
+
lines.push(" Unknowns:");
|
|
495
|
+
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
496
|
+
lines.push(` - ${chalk.yellow(u)}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
lines.push("");
|
|
500
|
+
}
|
|
472
501
|
if (ext.owaspCategoryMapping) {
|
|
473
502
|
const ow = ext.owaspCategoryMapping;
|
|
474
503
|
lines.push(chalk.bold.underline(" OWASP Category Mapping"));
|
|
@@ -565,125 +594,22 @@ function formatExtended(ext) {
|
|
|
565
594
|
}
|
|
566
595
|
return lines;
|
|
567
596
|
}
|
|
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
597
|
function formatArchitectureDiagram(arch) {
|
|
593
598
|
const lines = [];
|
|
594
599
|
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
600
|
lines.push(chalk.bold.cyan("\u2551 Architecture Layers \u2551"));
|
|
596
601
|
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
602
|
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`));
|
|
603
|
+
lines.push(chalk.bold(" Archetype: ") + `${arch.archetype}` + chalk.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
604
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
602
605
|
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`));
|
|
606
|
+
if (arch.layers.length > 0) {
|
|
607
|
+
for (const layer of arch.layers) {
|
|
608
|
+
const risk = layer.riskLevel === "low" ? chalk.green("low") : layer.riskLevel === "moderate" ? chalk.yellow("moderate") : chalk.red("high");
|
|
609
|
+
lines.push(` ${chalk.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
684
610
|
}
|
|
611
|
+
lines.push("");
|
|
685
612
|
}
|
|
686
|
-
lines.push("");
|
|
687
613
|
return lines;
|
|
688
614
|
}
|
|
689
615
|
function generatePriorityActions(artifact) {
|
|
@@ -1169,8 +1095,8 @@ import * as semver2 from "semver";
|
|
|
1169
1095
|
// src/utils/timeout.ts
|
|
1170
1096
|
async function withTimeout(promise, ms) {
|
|
1171
1097
|
let timer;
|
|
1172
|
-
const timeout = new Promise((
|
|
1173
|
-
timer = setTimeout(() =>
|
|
1098
|
+
const timeout = new Promise((resolve9) => {
|
|
1099
|
+
timer = setTimeout(() => resolve9({ ok: false }), ms);
|
|
1174
1100
|
});
|
|
1175
1101
|
try {
|
|
1176
1102
|
const result = await Promise.race([
|
|
@@ -1195,7 +1121,7 @@ function maxStable(versions) {
|
|
|
1195
1121
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
1196
1122
|
}
|
|
1197
1123
|
async function npmViewJson(args, cwd) {
|
|
1198
|
-
return new Promise((
|
|
1124
|
+
return new Promise((resolve9, reject) => {
|
|
1199
1125
|
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
1200
1126
|
cwd,
|
|
1201
1127
|
shell: true,
|
|
@@ -1213,13 +1139,13 @@ async function npmViewJson(args, cwd) {
|
|
|
1213
1139
|
}
|
|
1214
1140
|
const trimmed = out.trim();
|
|
1215
1141
|
if (!trimmed) {
|
|
1216
|
-
|
|
1142
|
+
resolve9(null);
|
|
1217
1143
|
return;
|
|
1218
1144
|
}
|
|
1219
1145
|
try {
|
|
1220
|
-
|
|
1146
|
+
resolve9(JSON.parse(trimmed));
|
|
1221
1147
|
} catch {
|
|
1222
|
-
|
|
1148
|
+
resolve9(trimmed.replace(/^"|"$/g, ""));
|
|
1223
1149
|
}
|
|
1224
1150
|
});
|
|
1225
1151
|
});
|
|
@@ -5060,7 +4986,7 @@ var SECRET_HEURISTICS = [
|
|
|
5060
4986
|
{ detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
5061
4987
|
{ detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
|
|
5062
4988
|
];
|
|
5063
|
-
var defaultRunner = (command, args) => new Promise((
|
|
4989
|
+
var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
|
|
5064
4990
|
const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
5065
4991
|
let stdout = "";
|
|
5066
4992
|
let stderr = "";
|
|
@@ -5072,7 +4998,7 @@ var defaultRunner = (command, args) => new Promise((resolve8, reject) => {
|
|
|
5072
4998
|
});
|
|
5073
4999
|
child.on("error", reject);
|
|
5074
5000
|
child.on("close", (code) => {
|
|
5075
|
-
|
|
5001
|
+
resolve9({ stdout, stderr, exitCode: code ?? 1 });
|
|
5076
5002
|
});
|
|
5077
5003
|
});
|
|
5078
5004
|
function compareSemver(a, b) {
|
|
@@ -6022,6 +5948,56 @@ function mapToolingToLayers(tooling, services, depsByLayer) {
|
|
|
6022
5948
|
}
|
|
6023
5949
|
return { layerTooling, layerServices };
|
|
6024
5950
|
}
|
|
5951
|
+
function generateLayerFlowMermaid(layers) {
|
|
5952
|
+
const labels = {
|
|
5953
|
+
presentation: "Presentation",
|
|
5954
|
+
routing: "Routing",
|
|
5955
|
+
middleware: "Middleware",
|
|
5956
|
+
services: "Services",
|
|
5957
|
+
domain: "Domain",
|
|
5958
|
+
"data-access": "Data Access",
|
|
5959
|
+
infrastructure: "Infrastructure",
|
|
5960
|
+
config: "Config",
|
|
5961
|
+
shared: "Shared",
|
|
5962
|
+
testing: "Testing"
|
|
5963
|
+
};
|
|
5964
|
+
if (layers.length === 0) {
|
|
5965
|
+
return 'flowchart TD\n APP["Project"]';
|
|
5966
|
+
}
|
|
5967
|
+
const ordered = [...layers];
|
|
5968
|
+
const lines = ["flowchart TD"];
|
|
5969
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
5970
|
+
const layer = ordered[i];
|
|
5971
|
+
lines.push(` L${i}["${labels[layer]}"]`);
|
|
5972
|
+
if (i > 0) lines.push(` L${i - 1} --> L${i}`);
|
|
5973
|
+
}
|
|
5974
|
+
return lines.join("\n");
|
|
5975
|
+
}
|
|
5976
|
+
async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
|
|
5977
|
+
const projectRoot = path17.resolve(rootDir, project.path || ".");
|
|
5978
|
+
const allFiles = await walkSourceFiles(projectRoot, cache);
|
|
5979
|
+
const layerSet = /* @__PURE__ */ new Set();
|
|
5980
|
+
for (const rel of allFiles) {
|
|
5981
|
+
const classification = classifyFile(rel, archetype);
|
|
5982
|
+
if (classification) {
|
|
5983
|
+
layerSet.add(classification.layer);
|
|
5984
|
+
}
|
|
5985
|
+
}
|
|
5986
|
+
const layerOrder = [
|
|
5987
|
+
"presentation",
|
|
5988
|
+
"routing",
|
|
5989
|
+
"middleware",
|
|
5990
|
+
"services",
|
|
5991
|
+
"domain",
|
|
5992
|
+
"data-access",
|
|
5993
|
+
"infrastructure",
|
|
5994
|
+
"config",
|
|
5995
|
+
"shared",
|
|
5996
|
+
"testing"
|
|
5997
|
+
];
|
|
5998
|
+
const orderedLayers = layerOrder.filter((l) => layerSet.has(l));
|
|
5999
|
+
return generateLayerFlowMermaid(orderedLayers);
|
|
6000
|
+
}
|
|
6025
6001
|
async function scanArchitecture(rootDir, projects, tooling, services, cache) {
|
|
6026
6002
|
const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
|
|
6027
6003
|
const sourceFiles = await walkSourceFiles(rootDir, cache);
|
|
@@ -6382,7 +6358,7 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
6382
6358
|
".env"
|
|
6383
6359
|
]);
|
|
6384
6360
|
async function runSemgrep(args, cwd, stdin) {
|
|
6385
|
-
return new Promise((
|
|
6361
|
+
return new Promise((resolve9, reject) => {
|
|
6386
6362
|
const child = spawn3("semgrep", args, {
|
|
6387
6363
|
cwd,
|
|
6388
6364
|
shell: true,
|
|
@@ -6398,7 +6374,7 @@ async function runSemgrep(args, cwd, stdin) {
|
|
|
6398
6374
|
});
|
|
6399
6375
|
child.on("error", reject);
|
|
6400
6376
|
child.on("close", (code) => {
|
|
6401
|
-
|
|
6377
|
+
resolve9({ code: code ?? 1, stdout, stderr });
|
|
6402
6378
|
});
|
|
6403
6379
|
if (stdin !== void 0) child.stdin.write(stdin);
|
|
6404
6380
|
child.stdin.end();
|
|
@@ -6528,6 +6504,221 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
|
|
|
6528
6504
|
};
|
|
6529
6505
|
}
|
|
6530
6506
|
|
|
6507
|
+
// src/scanners/ui-purpose.ts
|
|
6508
|
+
var UI_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6509
|
+
".tsx",
|
|
6510
|
+
".jsx",
|
|
6511
|
+
".ts",
|
|
6512
|
+
".js",
|
|
6513
|
+
".vue",
|
|
6514
|
+
".svelte",
|
|
6515
|
+
".html",
|
|
6516
|
+
".mdx",
|
|
6517
|
+
".json",
|
|
6518
|
+
".yml",
|
|
6519
|
+
".yaml"
|
|
6520
|
+
]);
|
|
6521
|
+
var HIGH_SIGNAL_DEPENDENCIES = /* @__PURE__ */ new Set([
|
|
6522
|
+
"stripe",
|
|
6523
|
+
"posthog-js",
|
|
6524
|
+
"posthog-node",
|
|
6525
|
+
"@sentry/node",
|
|
6526
|
+
"@sentry/browser",
|
|
6527
|
+
"@sentry/react",
|
|
6528
|
+
"next-auth",
|
|
6529
|
+
"firebase",
|
|
6530
|
+
"auth0",
|
|
6531
|
+
"@auth0/auth0-react",
|
|
6532
|
+
"@supabase/supabase-js",
|
|
6533
|
+
"supabase",
|
|
6534
|
+
"clerk",
|
|
6535
|
+
"@clerk/clerk-js"
|
|
6536
|
+
]);
|
|
6537
|
+
var GENERIC_LOW_SIGNAL = /* @__PURE__ */ new Set(["welcome", "home", "click here", "learn more", "submit", "cancel"]);
|
|
6538
|
+
async function scanUiPurpose(rootDir, fileCache, maxItems = 300) {
|
|
6539
|
+
const entries = await fileCache.walkDir(rootDir);
|
|
6540
|
+
const files = entries.filter((e) => e.isFile);
|
|
6541
|
+
const packageJsonEntry = files.find((e) => e.relPath === "package.json");
|
|
6542
|
+
let packageJson = {};
|
|
6543
|
+
if (packageJsonEntry) {
|
|
6544
|
+
try {
|
|
6545
|
+
packageJson = JSON.parse(await fileCache.readTextFile(packageJsonEntry.absPath));
|
|
6546
|
+
} catch {
|
|
6547
|
+
packageJson = {};
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
const frameworks = detectFrameworks(packageJson);
|
|
6551
|
+
const items = [];
|
|
6552
|
+
for (const entry of files) {
|
|
6553
|
+
const ext = extension(entry.relPath);
|
|
6554
|
+
if (!UI_EXTENSIONS.has(ext)) continue;
|
|
6555
|
+
if (!isLikelyUiPath(entry.relPath)) {
|
|
6556
|
+
const routeHints2 = extractRouteHints(entry.relPath, frameworks);
|
|
6557
|
+
if (routeHints2.length > 0) {
|
|
6558
|
+
items.push(...routeHints2.map((r) => ({ ...r, file: entry.relPath })));
|
|
6559
|
+
}
|
|
6560
|
+
continue;
|
|
6561
|
+
}
|
|
6562
|
+
const routeHints = extractRouteHints(entry.relPath, frameworks);
|
|
6563
|
+
if (routeHints.length > 0) {
|
|
6564
|
+
items.push(...routeHints.map((r) => ({ ...r, file: entry.relPath })));
|
|
6565
|
+
}
|
|
6566
|
+
const src = await fileCache.readTextFile(entry.absPath);
|
|
6567
|
+
if (!src || src.length > 512e3) continue;
|
|
6568
|
+
const strings = extractUiStrings(src);
|
|
6569
|
+
for (const text of strings) {
|
|
6570
|
+
const kind = classifyString(text);
|
|
6571
|
+
const weight = scoreString(kind, text);
|
|
6572
|
+
items.push({ kind, value: text, file: entry.relPath, weight });
|
|
6573
|
+
}
|
|
6574
|
+
if (/(featureFlag|FEATURE_FLAG|launchDarkly|isFeatureEnabled|flags\.)/.test(src)) {
|
|
6575
|
+
items.push({ kind: "feature_flag", value: `flags in ${entry.relPath}`, file: entry.relPath, weight: 2 });
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
const deps = getDependencies(packageJson);
|
|
6579
|
+
for (const [name, version] of deps) {
|
|
6580
|
+
if (HIGH_SIGNAL_DEPENDENCIES.has(name)) {
|
|
6581
|
+
items.push({ kind: "dependency", value: `${name}@${version}`, file: "package.json", weight: 4 });
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
const deduped = dedupeByKindValue(items).sort((a, b) => b.weight - a.weight || a.value.localeCompare(b.value));
|
|
6585
|
+
const cappedItems = deduped.slice(0, maxItems);
|
|
6586
|
+
const unknownSignals = buildUnknowns(cappedItems);
|
|
6587
|
+
return {
|
|
6588
|
+
enabled: true,
|
|
6589
|
+
detectedFrameworks: frameworks,
|
|
6590
|
+
evidenceCount: deduped.length,
|
|
6591
|
+
capped: deduped.length > cappedItems.length,
|
|
6592
|
+
topEvidence: cappedItems,
|
|
6593
|
+
unknownSignals
|
|
6594
|
+
};
|
|
6595
|
+
}
|
|
6596
|
+
function extension(relPath) {
|
|
6597
|
+
const i = relPath.lastIndexOf(".");
|
|
6598
|
+
return i === -1 ? "" : relPath.slice(i).toLowerCase();
|
|
6599
|
+
}
|
|
6600
|
+
function isLikelyUiPath(relPath) {
|
|
6601
|
+
const lower = relPath.toLowerCase();
|
|
6602
|
+
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");
|
|
6603
|
+
}
|
|
6604
|
+
function detectFrameworks(pkgJson) {
|
|
6605
|
+
const deps = {
|
|
6606
|
+
...asRecord(pkgJson.dependencies),
|
|
6607
|
+
...asRecord(pkgJson.devDependencies)
|
|
6608
|
+
};
|
|
6609
|
+
const hits = [];
|
|
6610
|
+
if (deps.next) hits.push("nextjs");
|
|
6611
|
+
if (deps.nuxt || deps.nuxt3) hits.push("nuxt");
|
|
6612
|
+
if (deps.react) hits.push("react");
|
|
6613
|
+
if (deps.vue) hits.push("vue");
|
|
6614
|
+
if (deps.svelte) hits.push("svelte");
|
|
6615
|
+
if (deps["@angular/core"]) hits.push("angular");
|
|
6616
|
+
return Array.from(new Set(hits));
|
|
6617
|
+
}
|
|
6618
|
+
function extractRouteHints(relPath, frameworks) {
|
|
6619
|
+
const items = [];
|
|
6620
|
+
if (frameworks.includes("nextjs")) {
|
|
6621
|
+
const m = relPath.match(/^(?:src\/)?(pages|app)\/(.+)\.(tsx|jsx|ts|js)$/);
|
|
6622
|
+
if (m) {
|
|
6623
|
+
let route = "/" + m[2].replace(/(^|\/)page$/i, "").replace(/(^|\/)route$/i, "").replace(/index$/i, "").replace(/\[(?:\.\.\.)?(.+?)\]/g, ":$1").replace(/\/+/g, "/");
|
|
6624
|
+
if (route !== "/" && route.endsWith("/")) route = route.slice(0, -1);
|
|
6625
|
+
items.push({ kind: "route", value: route || "/", weight: 5 });
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
if (/(^|\/)(routes?|router)\.(ts|js|json)$/.test(relPath) || relPath.includes("/router/")) {
|
|
6629
|
+
items.push({ kind: "route", value: `route config: ${relPath}`, weight: 3 });
|
|
6630
|
+
}
|
|
6631
|
+
return items;
|
|
6632
|
+
}
|
|
6633
|
+
function extractUiStrings(src) {
|
|
6634
|
+
const out = [];
|
|
6635
|
+
const textNodeRegex = />\s*([A-Za-z0-9][^<>]{2,120}?)\s*</g;
|
|
6636
|
+
for (const m of src.matchAll(textNodeRegex)) {
|
|
6637
|
+
const s = normaliseText(m[1] ?? "");
|
|
6638
|
+
if (isUsefulString(s)) out.push(s);
|
|
6639
|
+
}
|
|
6640
|
+
const titleRegex = /<title>\s*([^<]{2,120})\s*<\/title>|title\s*[:=]\s*["'`](.{2,120}?)["'`]/g;
|
|
6641
|
+
for (const m of src.matchAll(titleRegex)) {
|
|
6642
|
+
const s = normaliseText((m[1] ?? m[2] ?? "").trim());
|
|
6643
|
+
if (isUsefulString(s)) out.push(s);
|
|
6644
|
+
}
|
|
6645
|
+
const attrRegex = /(?:aria-label|label|placeholder|alt)\s*=\s*["'`](.{2,120}?)["'`]/g;
|
|
6646
|
+
for (const m of src.matchAll(attrRegex)) {
|
|
6647
|
+
const s = normaliseText(m[1] ?? "");
|
|
6648
|
+
if (isUsefulString(s)) out.push(s);
|
|
6649
|
+
}
|
|
6650
|
+
const jsonValueRegex = /:\s*["'`](.{2,140}?)["'`]\s*[,\n]/g;
|
|
6651
|
+
for (const m of src.matchAll(jsonValueRegex)) {
|
|
6652
|
+
const s = normaliseText(m[1] ?? "");
|
|
6653
|
+
if (isUsefulString(s)) out.push(s);
|
|
6654
|
+
}
|
|
6655
|
+
return out;
|
|
6656
|
+
}
|
|
6657
|
+
function classifyString(s) {
|
|
6658
|
+
const lower = s.toLowerCase();
|
|
6659
|
+
if (/(pricing|plan|billing|subscription|trial|credit)/.test(lower)) return "copy";
|
|
6660
|
+
if (/(sign in|sign up|log in|register|invite|workspace|organization|sso|oauth)/.test(lower)) return "copy";
|
|
6661
|
+
if (/(dashboard|reports|settings|integrations|users|roles|permissions)/.test(lower)) return "heading";
|
|
6662
|
+
if (/(get started|start|scan|generate|export|run|deploy|upgrade|analy[sz]e)/.test(lower)) return "cta";
|
|
6663
|
+
if (/(overview|features|about|documentation|docs)/.test(lower)) return "title";
|
|
6664
|
+
if (/(menu|navigation|sidebar|breadcrumb)/.test(lower)) return "nav";
|
|
6665
|
+
return "copy";
|
|
6666
|
+
}
|
|
6667
|
+
function scoreString(kind, value) {
|
|
6668
|
+
const lower = value.toLowerCase();
|
|
6669
|
+
if (GENERIC_LOW_SIGNAL.has(lower)) return 0;
|
|
6670
|
+
let score = 1;
|
|
6671
|
+
if (kind === "route") score += 4;
|
|
6672
|
+
if (kind === "nav") score += 3;
|
|
6673
|
+
if (kind === "title") score += 2;
|
|
6674
|
+
if (kind === "heading") score += 2;
|
|
6675
|
+
if (kind === "cta") score += 2;
|
|
6676
|
+
if (/(pricing|billing|subscription|auth|security|integration)/.test(lower)) score += 2;
|
|
6677
|
+
if (/(dashboard|report|scan|workspace|project|repository)/.test(lower)) score += 1;
|
|
6678
|
+
return score;
|
|
6679
|
+
}
|
|
6680
|
+
function dedupeByKindValue(items) {
|
|
6681
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6682
|
+
for (const item of items) {
|
|
6683
|
+
const key = `${item.kind}::${item.value.toLowerCase()}`;
|
|
6684
|
+
const prev = seen.get(key);
|
|
6685
|
+
if (!prev || item.weight > prev.weight) {
|
|
6686
|
+
seen.set(key, item);
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
return Array.from(seen.values()).filter((i) => i.weight > 0);
|
|
6690
|
+
}
|
|
6691
|
+
function buildUnknowns(items) {
|
|
6692
|
+
const unknowns = [];
|
|
6693
|
+
const hasPricing = items.some((i) => /pricing|billing|subscription|trial|credit/i.test(i.value));
|
|
6694
|
+
const hasAuth = items.some((i) => /sign in|sign up|login|auth|sso|oauth|invite/i.test(i.value));
|
|
6695
|
+
const hasIntegrations = items.some((i) => /integration|webhook|api key|connector/i.test(i.value));
|
|
6696
|
+
const hasRoutes = items.some((i) => i.kind === "route");
|
|
6697
|
+
if (!hasPricing) unknowns.push("No pricing or billing evidence found.");
|
|
6698
|
+
if (!hasAuth) unknowns.push("No authentication or user access flow evidence found.");
|
|
6699
|
+
if (!hasIntegrations) unknowns.push("No integrations/connectors evidence found.");
|
|
6700
|
+
if (!hasRoutes) unknowns.push("No route structure evidence found.");
|
|
6701
|
+
return unknowns;
|
|
6702
|
+
}
|
|
6703
|
+
function asRecord(value) {
|
|
6704
|
+
if (!value || typeof value !== "object") return {};
|
|
6705
|
+
return value;
|
|
6706
|
+
}
|
|
6707
|
+
function getDependencies(pkgJson) {
|
|
6708
|
+
return Object.entries(asRecord(pkgJson.dependencies));
|
|
6709
|
+
}
|
|
6710
|
+
function normaliseText(s) {
|
|
6711
|
+
return s.replace(/\s+/g, " ").trim();
|
|
6712
|
+
}
|
|
6713
|
+
function isUsefulString(s) {
|
|
6714
|
+
if (!s || s.length < 3 || s.length > 160) return false;
|
|
6715
|
+
if (/^[0-9_./-]+$/.test(s)) return false;
|
|
6716
|
+
if (/^(true|false|null|undefined)$/i.test(s)) return false;
|
|
6717
|
+
if (/[<>{}]/.test(s)) return false;
|
|
6718
|
+
if (/function\s*\(|=>|console\.|import\s+|export\s+/.test(s)) return false;
|
|
6719
|
+
return true;
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6531
6722
|
// src/utils/tool-installer.ts
|
|
6532
6723
|
import { spawn as spawn4 } from "child_process";
|
|
6533
6724
|
import chalk5 from "chalk";
|
|
@@ -6538,7 +6729,7 @@ var SECURITY_TOOLS = [
|
|
|
6538
6729
|
];
|
|
6539
6730
|
var IS_WIN = process.platform === "win32";
|
|
6540
6731
|
function runCommand(cmd, args) {
|
|
6541
|
-
return new Promise((
|
|
6732
|
+
return new Promise((resolve9) => {
|
|
6542
6733
|
const child = spawn4(cmd, args, {
|
|
6543
6734
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6544
6735
|
shell: IS_WIN
|
|
@@ -6552,8 +6743,8 @@ function runCommand(cmd, args) {
|
|
|
6552
6743
|
child.stderr.on("data", (d) => {
|
|
6553
6744
|
stderr += d.toString();
|
|
6554
6745
|
});
|
|
6555
|
-
child.on("error", () =>
|
|
6556
|
-
child.on("close", (code) =>
|
|
6746
|
+
child.on("error", () => resolve9({ exitCode: 127, stdout, stderr }));
|
|
6747
|
+
child.on("close", (code) => resolve9({ exitCode: code ?? 1, stdout, stderr }));
|
|
6557
6748
|
});
|
|
6558
6749
|
}
|
|
6559
6750
|
async function commandExists(command) {
|
|
@@ -6620,6 +6811,76 @@ async function installMissingTools(log = (m) => process.stderr.write(m + "\n"))
|
|
|
6620
6811
|
return result;
|
|
6621
6812
|
}
|
|
6622
6813
|
|
|
6814
|
+
// src/utils/mermaid.ts
|
|
6815
|
+
function sanitizeId(input) {
|
|
6816
|
+
return input.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
6817
|
+
}
|
|
6818
|
+
function escapeLabel(input) {
|
|
6819
|
+
return input.replace(/"/g, '\\"');
|
|
6820
|
+
}
|
|
6821
|
+
function scoreClass(score) {
|
|
6822
|
+
if (score === void 0 || Number.isNaN(score)) return "scoreUnknown";
|
|
6823
|
+
if (score >= 70) return "scoreHigh";
|
|
6824
|
+
if (score >= 40) return "scoreModerate";
|
|
6825
|
+
return "scoreLow";
|
|
6826
|
+
}
|
|
6827
|
+
function nodeLabel(project) {
|
|
6828
|
+
const score = project.drift?.score;
|
|
6829
|
+
const scoreText = typeof score === "number" ? ` (${score})` : " (n/a)";
|
|
6830
|
+
return `${project.name}${scoreText}`;
|
|
6831
|
+
}
|
|
6832
|
+
function buildDefs() {
|
|
6833
|
+
return [
|
|
6834
|
+
"classDef scoreHigh fill:#064e3b,stroke:#10b981,color:#d1fae5,stroke-width:2px",
|
|
6835
|
+
"classDef scoreModerate fill:#78350f,stroke:#f59e0b,color:#fef3c7,stroke-width:2px",
|
|
6836
|
+
"classDef scoreLow fill:#7f1d1d,stroke:#ef4444,color:#fee2e2,stroke-width:2px",
|
|
6837
|
+
"classDef scoreUnknown fill:#334155,stroke:#94a3b8,color:#e2e8f0,stroke-width:2px"
|
|
6838
|
+
];
|
|
6839
|
+
}
|
|
6840
|
+
function generateWorkspaceRelationshipMermaid(projects) {
|
|
6841
|
+
const lines = ["flowchart LR"];
|
|
6842
|
+
const byPath = new Map(projects.map((p) => [p.path, p]));
|
|
6843
|
+
for (const project of projects) {
|
|
6844
|
+
const id = sanitizeId(project.projectId || project.path || project.name);
|
|
6845
|
+
lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
|
|
6846
|
+
lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
|
|
6847
|
+
}
|
|
6848
|
+
for (const project of projects) {
|
|
6849
|
+
const fromId = sanitizeId(project.projectId || project.path || project.name);
|
|
6850
|
+
for (const ref of project.projectReferences ?? []) {
|
|
6851
|
+
const target = byPath.get(ref.path);
|
|
6852
|
+
if (!target) continue;
|
|
6853
|
+
const toId = sanitizeId(target.projectId || target.path || target.name);
|
|
6854
|
+
lines.push(`${fromId} --> ${toId}`);
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
lines.push(...buildDefs());
|
|
6858
|
+
return { mermaid: lines.join("\n") };
|
|
6859
|
+
}
|
|
6860
|
+
function generateProjectRelationshipMermaid(project, projects) {
|
|
6861
|
+
const lines = ["flowchart LR"];
|
|
6862
|
+
const byPath = new Map(projects.map((p) => [p.path, p]));
|
|
6863
|
+
const parents = projects.filter((p) => p.projectReferences?.some((r) => r.path === project.path));
|
|
6864
|
+
const children = (project.projectReferences ?? []).map((r) => byPath.get(r.path)).filter((p) => Boolean(p));
|
|
6865
|
+
const centerId = sanitizeId(project.projectId || project.path || project.name);
|
|
6866
|
+
lines.push(`${centerId}["${escapeLabel(nodeLabel(project))}"]`);
|
|
6867
|
+
lines.push(`class ${centerId} ${scoreClass(project.drift?.score)}`);
|
|
6868
|
+
for (const parent of parents) {
|
|
6869
|
+
const id = sanitizeId(parent.projectId || parent.path || parent.name);
|
|
6870
|
+
lines.push(`${id}["${escapeLabel(nodeLabel(parent))}"]`);
|
|
6871
|
+
lines.push(`class ${id} ${scoreClass(parent.drift?.score)}`);
|
|
6872
|
+
lines.push(`${id} --> ${centerId}`);
|
|
6873
|
+
}
|
|
6874
|
+
for (const child of children) {
|
|
6875
|
+
const id = sanitizeId(child.projectId || child.path || child.name);
|
|
6876
|
+
lines.push(`${id}["${escapeLabel(nodeLabel(child))}"]`);
|
|
6877
|
+
lines.push(`class ${id} ${scoreClass(child.drift?.score)}`);
|
|
6878
|
+
lines.push(`${centerId} --> ${id}`);
|
|
6879
|
+
}
|
|
6880
|
+
lines.push(...buildDefs());
|
|
6881
|
+
return { mermaid: lines.join("\n") };
|
|
6882
|
+
}
|
|
6883
|
+
|
|
6623
6884
|
// src/commands/scan.ts
|
|
6624
6885
|
async function runScan(rootDir, opts) {
|
|
6625
6886
|
const scanStart = Date.now();
|
|
@@ -6659,7 +6920,8 @@ async function runScan(rootDir, opts) {
|
|
|
6659
6920
|
...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
6660
6921
|
...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
|
|
6661
6922
|
...scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
|
|
6662
|
-
...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : []
|
|
6923
|
+
...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
|
|
6924
|
+
...opts.uiPurpose || scanners?.uiPurpose?.enabled === true ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
|
|
6663
6925
|
] : [],
|
|
6664
6926
|
{ id: "drift", label: "Computing drift score" },
|
|
6665
6927
|
{ id: "findings", label: "Generating findings" }
|
|
@@ -6748,6 +7010,10 @@ async function runScan(rootDir, opts) {
|
|
|
6748
7010
|
project.drift = computeDriftScore([project]);
|
|
6749
7011
|
project.projectId = computeProjectId(project.path, project.name, workspaceId);
|
|
6750
7012
|
}
|
|
7013
|
+
for (const project of allProjects) {
|
|
7014
|
+
project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
|
|
7015
|
+
}
|
|
7016
|
+
const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
|
|
6751
7017
|
const extended = {};
|
|
6752
7018
|
if (scanners !== false) {
|
|
6753
7019
|
const scannerTasks = [];
|
|
@@ -6923,6 +7189,13 @@ async function runScan(rootDir, opts) {
|
|
|
6923
7189
|
);
|
|
6924
7190
|
}
|
|
6925
7191
|
}
|
|
7192
|
+
if (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) {
|
|
7193
|
+
progress.startStep("uipurpose");
|
|
7194
|
+
extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
|
|
7195
|
+
const up = extended.uiPurpose;
|
|
7196
|
+
const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
|
|
7197
|
+
progress.completeStep("uipurpose", summary, up.topEvidence.length);
|
|
7198
|
+
}
|
|
6926
7199
|
if (scanners?.architecture?.enabled !== false) {
|
|
6927
7200
|
progress.startStep("architecture");
|
|
6928
7201
|
extended.architecture = await scanArchitecture(
|
|
@@ -6934,6 +7207,14 @@ async function runScan(rootDir, opts) {
|
|
|
6934
7207
|
);
|
|
6935
7208
|
const arch = extended.architecture;
|
|
6936
7209
|
const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
|
|
7210
|
+
await Promise.all(allProjects.map(async (project) => {
|
|
7211
|
+
project.architectureMermaid = await buildProjectArchitectureMermaid(
|
|
7212
|
+
rootDir,
|
|
7213
|
+
project,
|
|
7214
|
+
arch.archetype,
|
|
7215
|
+
fileCache
|
|
7216
|
+
);
|
|
7217
|
+
}));
|
|
6937
7218
|
progress.completeStep(
|
|
6938
7219
|
"architecture",
|
|
6939
7220
|
`${arch.archetype} \xB7 ${layerCount} layer${layerCount !== 1 ? "s" : ""} \xB7 ${arch.totalClassified} files`,
|
|
@@ -7001,6 +7282,7 @@ async function runScan(rootDir, opts) {
|
|
|
7001
7282
|
}
|
|
7002
7283
|
if (extended.codeQuality) filesScanned += extended.codeQuality.filesAnalyzed;
|
|
7003
7284
|
if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
|
|
7285
|
+
if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
|
|
7004
7286
|
const durationMs = Date.now() - scanStart;
|
|
7005
7287
|
const artifact = {
|
|
7006
7288
|
schemaVersion: "1.0",
|
|
@@ -7014,7 +7296,8 @@ async function runScan(rootDir, opts) {
|
|
|
7014
7296
|
...Object.keys(extended).length > 0 ? { extended } : {},
|
|
7015
7297
|
durationMs,
|
|
7016
7298
|
filesScanned,
|
|
7017
|
-
treeSummary: treeCount
|
|
7299
|
+
treeSummary: treeCount,
|
|
7300
|
+
relationshipDiagram
|
|
7018
7301
|
};
|
|
7019
7302
|
if (opts.baseline) {
|
|
7020
7303
|
const baselinePath = path20.resolve(opts.baseline);
|
|
@@ -7145,7 +7428,15 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
7145
7428
|
if (opts.strict) process.exit(1);
|
|
7146
7429
|
}
|
|
7147
7430
|
}
|
|
7148
|
-
|
|
7431
|
+
function parseNonNegativeNumber(value, label) {
|
|
7432
|
+
if (value === void 0) return void 0;
|
|
7433
|
+
const parsed = Number(value);
|
|
7434
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
7435
|
+
throw new Error(`${label} must be a non-negative number.`);
|
|
7436
|
+
}
|
|
7437
|
+
return parsed;
|
|
7438
|
+
}
|
|
7439
|
+
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
|
|
7149
7440
|
const rootDir = path20.resolve(targetPath);
|
|
7150
7441
|
if (!await pathExists(rootDir)) {
|
|
7151
7442
|
console.error(chalk6.red(`Path does not exist: ${rootDir}`));
|
|
@@ -7162,7 +7453,10 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
7162
7453
|
dsn: opts.dsn,
|
|
7163
7454
|
region: opts.region,
|
|
7164
7455
|
strict: opts.strict,
|
|
7165
|
-
installTools: opts.installTools
|
|
7456
|
+
installTools: opts.installTools,
|
|
7457
|
+
uiPurpose: opts.uiPurpose,
|
|
7458
|
+
driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
|
|
7459
|
+
driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
|
|
7166
7460
|
};
|
|
7167
7461
|
const artifact = await runScan(rootDir, scanOpts);
|
|
7168
7462
|
if (opts.failOn) {
|
|
@@ -7179,6 +7473,27 @@ Failing: findings detected at warn level or above.`));
|
|
|
7179
7473
|
process.exit(2);
|
|
7180
7474
|
}
|
|
7181
7475
|
}
|
|
7476
|
+
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
7477
|
+
console.error(chalk6.red(`
|
|
7478
|
+
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
7479
|
+
process.exit(2);
|
|
7480
|
+
}
|
|
7481
|
+
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
7482
|
+
if (artifact.delta === void 0) {
|
|
7483
|
+
console.error(chalk6.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
7484
|
+
process.exit(2);
|
|
7485
|
+
}
|
|
7486
|
+
if (artifact.delta > 0) {
|
|
7487
|
+
const baselineScore = artifact.drift.score - artifact.delta;
|
|
7488
|
+
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
7489
|
+
const worseningPercent = artifact.delta / denominator * 100;
|
|
7490
|
+
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
7491
|
+
console.error(chalk6.red(`
|
|
7492
|
+
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
7493
|
+
process.exit(2);
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7182
7497
|
const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
|
|
7183
7498
|
if (opts.push || hasDsn) {
|
|
7184
7499
|
await autoPush(artifact, rootDir, scanOpts);
|