@vibgrate/cli 1.0.26 → 1.0.28
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 +16 -1
- package/README.md +6 -2
- package/dist/{baseline-KXUPTMQ2.js → baseline-AA2FVYAX.js} +2 -2
- package/dist/{chunk-7EEUYKZI.js → chunk-T4GNX4OC.js} +1 -1
- package/dist/{chunk-27LB7QTA.js → chunk-XLRCQ476.js} +675 -34
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +71 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/DOCS.md
CHANGED
|
@@ -40,6 +40,7 @@ For a quick overview, see the [README](./README.md). This document covers everyt
|
|
|
40
40
|
- [Breaking Change Exposure](#breaking-change-exposure)
|
|
41
41
|
- [File Hotspots](#file-hotspots)
|
|
42
42
|
- [Security Posture](#security-posture)
|
|
43
|
+
- [Security Scanners](#security-scanners)
|
|
43
44
|
- [Service Dependencies](#service-dependencies)
|
|
44
45
|
- [CI Integration](#ci-integration)
|
|
45
46
|
- [GitHub Actions](#github-actions)
|
|
@@ -65,7 +66,7 @@ Vibgrate recursively scans your repository for `package.json` (Node/TypeScript)
|
|
|
65
66
|
4. **Generates** a deterministic Upgrade Drift Score (0–100)
|
|
66
67
|
5. **Produces** findings, a full JSON artifact, and optional SARIF output
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
Core drift analysis does not execute source code. Optional security scanners can run lightweight secret heuristics and local toolchain checks. Dashboard upload remains optional.
|
|
69
70
|
|
|
70
71
|
---
|
|
71
72
|
|
|
@@ -274,6 +275,7 @@ const config: VibgrateConfig = {
|
|
|
274
275
|
breakingChangeExposure: { enabled: true },
|
|
275
276
|
fileHotspots: { enabled: true },
|
|
276
277
|
securityPosture: { enabled: true },
|
|
278
|
+
securityScanners: { enabled: true },
|
|
277
279
|
serviceDependencies: { enabled: true },
|
|
278
280
|
},
|
|
279
281
|
};
|
|
@@ -398,6 +400,19 @@ Structural security hygiene indicators (not a secret scanner):
|
|
|
398
400
|
- `.env` files tracked outside `.gitignore`
|
|
399
401
|
- Audit severity counts (via `npm audit --json`)
|
|
400
402
|
|
|
403
|
+
|
|
404
|
+
### Security Scanners
|
|
405
|
+
|
|
406
|
+
Security scanner orchestration and readiness analysis focused on modern SAST and secrets tooling:
|
|
407
|
+
|
|
408
|
+
- Semgrep support for SAST (version detection + freshness checks)
|
|
409
|
+
- Gitleaks and TruffleHog support for secret scanning readiness
|
|
410
|
+
- Recommended minimum version checks to highlight stale engines/signatures
|
|
411
|
+
- Config discovery (`.semgrep.yml`, `.gitleaks.toml`, `.trufflehog.yml`)
|
|
412
|
+
- Cache-backed heuristic secret signals to add value even when binaries are unavailable
|
|
413
|
+
|
|
414
|
+
> This scanner does not guarantee full secret detection or rule coverage by itself; it reports toolchain status and lightweight in-repo indicators so teams can decide how to harden CI enforcement.
|
|
415
|
+
|
|
401
416
|
### Service Dependencies
|
|
402
417
|
|
|
403
418
|
Maps external service and platform dependencies by detecting SDK packages:
|
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ Works across **Node.js/TypeScript** and **.NET** projects in the same scan. Dete
|
|
|
162
162
|
|
|
163
163
|
Designed to live in your build pipeline. Returns meaningful exit codes, produces SARIF output for GitHub Code Scanning and Azure DevOps, and requires zero configuration to get started.
|
|
164
164
|
|
|
165
|
-
###
|
|
165
|
+
### 13 Extended Scanners
|
|
166
166
|
|
|
167
167
|
Beyond the core drift score, Vibgrate runs a suite of extended scanners — all optional, all privacy-safe:
|
|
168
168
|
|
|
@@ -177,7 +177,10 @@ Beyond the core drift score, Vibgrate runs a suite of extended scanners — all
|
|
|
177
177
|
| **Breaking Change Exposure** | Packages known to cause upgrade pain, legacy polyfills |
|
|
178
178
|
| **File Hotspots** | Codebase shape — file counts, sizes, depth, shared packages |
|
|
179
179
|
| **Security Posture** | Lockfile hygiene, `.gitignore` coverage, audit severity counts |
|
|
180
|
+
| **Security Scanners** | Semgrep (SAST) + Gitleaks/TruffleHog readiness, version risk checks, heuristic secret signals |
|
|
180
181
|
| **Service Dependencies** | External SDK detection — payment, auth, cloud, databases, messaging |
|
|
182
|
+
| **Code Quality** | Cyclomatic complexity, function length, nesting depth, god files, dead-code estimate, circular imports |
|
|
183
|
+
| **OWASP Category Mapping** | Semgrep OSS findings mapped to OWASP Top 10 categories (fast or cache-input mode) |
|
|
181
184
|
|
|
182
185
|
### Baseline & Delta Tracking
|
|
183
186
|
|
|
@@ -284,7 +287,8 @@ export default config;
|
|
|
284
287
|
|
|
285
288
|
Vibgrate is designed to be safe to run on any codebase:
|
|
286
289
|
|
|
287
|
-
- **No source code is
|
|
290
|
+
- **No source code content is exfiltrated** — code-quality metrics are computed locally and only aggregated numbers are emitted
|
|
291
|
+
- **Source code is only read when explicitly needed** — core drift scanners use manifests/configs; OWASP mapping can inspect source files via Semgrep
|
|
288
292
|
- **No secrets are scanned** — ever
|
|
289
293
|
- **No git history, authors, or commit messages** — only HEAD SHA and branch name for traceability
|
|
290
294
|
- **No data leaves your machine** unless you explicitly run `vibgrate push` or `vibgrate scan --push`
|
|
@@ -23,7 +23,7 @@ var Semaphore = class {
|
|
|
23
23
|
this.available--;
|
|
24
24
|
return Promise.resolve();
|
|
25
25
|
}
|
|
26
|
-
return new Promise((
|
|
26
|
+
return new Promise((resolve9) => this.queue.push(resolve9));
|
|
27
27
|
}
|
|
28
28
|
release() {
|
|
29
29
|
const next = this.queue.shift();
|
|
@@ -291,7 +291,7 @@ var FileCache = class _FileCache {
|
|
|
291
291
|
const result = await Promise.race([
|
|
292
292
|
readPromise.then((e) => ({ ok: true, entries: e })),
|
|
293
293
|
new Promise(
|
|
294
|
-
(
|
|
294
|
+
(resolve9) => setTimeout(() => resolve9({ ok: false }), STUCK_TIMEOUT_MS)
|
|
295
295
|
)
|
|
296
296
|
]);
|
|
297
297
|
if (!result.ok) {
|
|
@@ -940,14 +940,14 @@ function formatExtended(ext) {
|
|
|
940
940
|
}
|
|
941
941
|
}
|
|
942
942
|
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
943
|
-
const
|
|
943
|
+
const ts2 = ext.tsModernity;
|
|
944
944
|
lines.push(chalk.bold.underline(" TypeScript"));
|
|
945
945
|
const parts = [];
|
|
946
|
-
parts.push(`v${
|
|
947
|
-
if (
|
|
948
|
-
else if (
|
|
949
|
-
if (
|
|
950
|
-
if (
|
|
946
|
+
parts.push(`v${ts2.typescriptVersion}`);
|
|
947
|
+
if (ts2.strict === true) parts.push(chalk.green("strict \u2714"));
|
|
948
|
+
else if (ts2.strict === false) parts.push(chalk.yellow("strict \u2716"));
|
|
949
|
+
if (ts2.moduleType) parts.push(ts2.moduleType.toUpperCase());
|
|
950
|
+
if (ts2.target) parts.push(`target: ${ts2.target}`);
|
|
951
951
|
lines.push(` ${parts.join(chalk.dim(" \xB7 "))}`);
|
|
952
952
|
lines.push("");
|
|
953
953
|
}
|
|
@@ -966,6 +966,24 @@ function formatExtended(ext) {
|
|
|
966
966
|
lines.push("");
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
|
+
if (ext.owaspCategoryMapping) {
|
|
970
|
+
const ow = ext.owaspCategoryMapping;
|
|
971
|
+
lines.push(chalk.bold.underline(" OWASP Category Mapping"));
|
|
972
|
+
if (!ow.available) {
|
|
973
|
+
lines.push(` ${chalk.yellow("Scanner unavailable")}: ${ow.errors[0] ?? "semgrep not found"}`);
|
|
974
|
+
} else {
|
|
975
|
+
lines.push(` Scanner: Semgrep OSS (${ow.mode})`);
|
|
976
|
+
lines.push(` Findings: ${ow.findings.length} across ${Object.keys(ow.categoryCounts).length} categories`);
|
|
977
|
+
const top = Object.entries(ow.categoryCounts).slice(0, 6);
|
|
978
|
+
if (top.length > 0) {
|
|
979
|
+
lines.push(` Top Categories: ${top.map(([cat, count]) => `${cat} (${count})`).join(", ")}`);
|
|
980
|
+
}
|
|
981
|
+
if (ow.errors.length > 0) {
|
|
982
|
+
lines.push(` ${chalk.yellow("Partial errors")}: ${ow.errors.length}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
lines.push("");
|
|
986
|
+
}
|
|
969
987
|
if (ext.securityPosture) {
|
|
970
988
|
const sec = ext.securityPosture;
|
|
971
989
|
lines.push(chalk.bold.underline(" Security Posture"));
|
|
@@ -978,6 +996,27 @@ function formatExtended(ext) {
|
|
|
978
996
|
lines.push(` ${checks.join(chalk.dim(" \xB7 "))}`);
|
|
979
997
|
lines.push("");
|
|
980
998
|
}
|
|
999
|
+
if (ext.securityScanners) {
|
|
1000
|
+
const ss = ext.securityScanners;
|
|
1001
|
+
lines.push(chalk.bold.underline(" Security Scanners"));
|
|
1002
|
+
const tools = [ss.semgrep, ...ss.secretScanners];
|
|
1003
|
+
for (const tool of tools) {
|
|
1004
|
+
const status = tool.status === "up-to-date" ? chalk.green("up-to-date") : tool.status === "review-needed" ? chalk.yellow("review-needed") : tool.status === "unavailable" ? chalk.red("unavailable") : chalk.yellow("unknown");
|
|
1005
|
+
lines.push(` ${tool.name}: ${status}${tool.version ? chalk.dim(` v${tool.version}`) : ""}`);
|
|
1006
|
+
if (tool.risks.length > 0) {
|
|
1007
|
+
lines.push(` ${chalk.dim(tool.risks[0])}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const configs = [];
|
|
1011
|
+
if (ss.configFiles.semgrep) configs.push(".semgrep.yml");
|
|
1012
|
+
if (ss.configFiles.gitleaks) configs.push(".gitleaks.toml");
|
|
1013
|
+
if (ss.configFiles.trufflehog) configs.push(".trufflehog.yml");
|
|
1014
|
+
lines.push(` Configs: ${configs.length > 0 ? configs.join(", ") : chalk.dim("none detected")}`);
|
|
1015
|
+
if (ss.heuristicFindings.length > 0) {
|
|
1016
|
+
lines.push(` ${chalk.red("Potential secret signals")}: ${ss.heuristicFindings.length}`);
|
|
1017
|
+
}
|
|
1018
|
+
lines.push("");
|
|
1019
|
+
}
|
|
981
1020
|
if (ext.platformMatrix) {
|
|
982
1021
|
const pm = ext.platformMatrix;
|
|
983
1022
|
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
@@ -991,6 +1030,17 @@ function formatExtended(ext) {
|
|
|
991
1030
|
lines.push("");
|
|
992
1031
|
}
|
|
993
1032
|
}
|
|
1033
|
+
if (ext.codeQuality) {
|
|
1034
|
+
const cq = ext.codeQuality;
|
|
1035
|
+
lines.push(chalk.bold.underline(" Code Quality"));
|
|
1036
|
+
lines.push(` Files: ${chalk.white(`${cq.filesAnalyzed}`)} \xB7 Functions: ${chalk.white(`${cq.functionsAnalyzed}`)} \xB7 Avg complexity: ${chalk.white(`${cq.avgCyclomaticComplexity}`)} \xB7 Avg length: ${chalk.white(`${cq.avgFunctionLength}`)} lines`);
|
|
1037
|
+
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
1038
|
+
if (cq.godFiles.length > 0) {
|
|
1039
|
+
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
1040
|
+
lines.push(` ${chalk.yellow("God files")}: ${preview}`);
|
|
1041
|
+
}
|
|
1042
|
+
lines.push("");
|
|
1043
|
+
}
|
|
994
1044
|
if (ext.dependencyGraph) {
|
|
995
1045
|
const dg = ext.dependencyGraph;
|
|
996
1046
|
if (dg.lockfileType) {
|
|
@@ -1591,7 +1641,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1591
1641
|
});
|
|
1592
1642
|
|
|
1593
1643
|
// src/commands/scan.ts
|
|
1594
|
-
import * as
|
|
1644
|
+
import * as path20 from "path";
|
|
1595
1645
|
import { Command as Command3 } from "commander";
|
|
1596
1646
|
import chalk5 from "chalk";
|
|
1597
1647
|
|
|
@@ -1602,8 +1652,8 @@ import * as semver2 from "semver";
|
|
|
1602
1652
|
// src/utils/timeout.ts
|
|
1603
1653
|
async function withTimeout(promise, ms) {
|
|
1604
1654
|
let timer;
|
|
1605
|
-
const timeout = new Promise((
|
|
1606
|
-
timer = setTimeout(() =>
|
|
1655
|
+
const timeout = new Promise((resolve9) => {
|
|
1656
|
+
timer = setTimeout(() => resolve9({ ok: false }), ms);
|
|
1607
1657
|
});
|
|
1608
1658
|
try {
|
|
1609
1659
|
const result = await Promise.race([
|
|
@@ -1628,7 +1678,7 @@ function maxStable(versions) {
|
|
|
1628
1678
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
1629
1679
|
}
|
|
1630
1680
|
async function npmViewJson(args, cwd) {
|
|
1631
|
-
return new Promise((
|
|
1681
|
+
return new Promise((resolve9, reject) => {
|
|
1632
1682
|
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
1633
1683
|
cwd,
|
|
1634
1684
|
shell: true,
|
|
@@ -1646,13 +1696,13 @@ async function npmViewJson(args, cwd) {
|
|
|
1646
1696
|
}
|
|
1647
1697
|
const trimmed = out.trim();
|
|
1648
1698
|
if (!trimmed) {
|
|
1649
|
-
|
|
1699
|
+
resolve9(null);
|
|
1650
1700
|
return;
|
|
1651
1701
|
}
|
|
1652
1702
|
try {
|
|
1653
|
-
|
|
1703
|
+
resolve9(JSON.parse(trimmed));
|
|
1654
1704
|
} catch {
|
|
1655
|
-
|
|
1705
|
+
resolve9(trimmed.replace(/^"|"$/g, ""));
|
|
1656
1706
|
}
|
|
1657
1707
|
});
|
|
1658
1708
|
});
|
|
@@ -4422,6 +4472,138 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4422
4472
|
return result;
|
|
4423
4473
|
}
|
|
4424
4474
|
|
|
4475
|
+
// src/scanners/security-scanners.ts
|
|
4476
|
+
import { spawn as spawn2 } from "child_process";
|
|
4477
|
+
import * as path16 from "path";
|
|
4478
|
+
var TOOL_MATRIX = [
|
|
4479
|
+
{ key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
|
|
4480
|
+
{ key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
|
|
4481
|
+
{ key: "trufflehog", category: "secrets", command: "trufflehog", versionArgs: ["--version"], minRecommendedVersion: "3.80.0" }
|
|
4482
|
+
];
|
|
4483
|
+
var SEMVER_RE = /(\d+\.\d+\.\d+)/;
|
|
4484
|
+
var SECRET_HEURISTICS = [
|
|
4485
|
+
{ detector: "aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
4486
|
+
{ detector: "github-token", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
4487
|
+
{ detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
4488
|
+
{ detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
|
|
4489
|
+
];
|
|
4490
|
+
var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
|
|
4491
|
+
const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4492
|
+
let stdout = "";
|
|
4493
|
+
let stderr = "";
|
|
4494
|
+
child.stdout.on("data", (d) => {
|
|
4495
|
+
stdout += d.toString();
|
|
4496
|
+
});
|
|
4497
|
+
child.stderr.on("data", (d) => {
|
|
4498
|
+
stderr += d.toString();
|
|
4499
|
+
});
|
|
4500
|
+
child.on("error", reject);
|
|
4501
|
+
child.on("close", (code) => {
|
|
4502
|
+
resolve9({ stdout, stderr, exitCode: code ?? 1 });
|
|
4503
|
+
});
|
|
4504
|
+
});
|
|
4505
|
+
function compareSemver(a, b) {
|
|
4506
|
+
const av = a.split(".").map((v) => Number(v));
|
|
4507
|
+
const bv = b.split(".").map((v) => Number(v));
|
|
4508
|
+
for (let i = 0; i < 3; i++) {
|
|
4509
|
+
const ai = av[i] ?? 0;
|
|
4510
|
+
const bi = bv[i] ?? 0;
|
|
4511
|
+
if (ai > bi) return 1;
|
|
4512
|
+
if (ai < bi) return -1;
|
|
4513
|
+
}
|
|
4514
|
+
return 0;
|
|
4515
|
+
}
|
|
4516
|
+
function parseVersion(input) {
|
|
4517
|
+
const match = input.match(SEMVER_RE);
|
|
4518
|
+
return match ? match[1] : null;
|
|
4519
|
+
}
|
|
4520
|
+
async function assessTool(tool, runner) {
|
|
4521
|
+
try {
|
|
4522
|
+
const out = await runner(tool.command, tool.versionArgs);
|
|
4523
|
+
const combined = `${out.stdout}
|
|
4524
|
+
${out.stderr}`.trim();
|
|
4525
|
+
const version = parseVersion(combined);
|
|
4526
|
+
const risks = [];
|
|
4527
|
+
if (out.exitCode !== 0 || !version) {
|
|
4528
|
+
risks.push("Installed but version could not be verified; output format may have changed.");
|
|
4529
|
+
return {
|
|
4530
|
+
name: tool.key,
|
|
4531
|
+
category: tool.category,
|
|
4532
|
+
command: tool.command,
|
|
4533
|
+
available: true,
|
|
4534
|
+
version,
|
|
4535
|
+
minRecommendedVersion: tool.minRecommendedVersion,
|
|
4536
|
+
status: "unknown",
|
|
4537
|
+
risks
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
const status = compareSemver(version, tool.minRecommendedVersion) >= 0 ? "up-to-date" : "review-needed";
|
|
4541
|
+
if (status === "review-needed") {
|
|
4542
|
+
risks.push(`Detected version ${version} is older than recommended ${tool.minRecommendedVersion}; newer signatures/rules may be missing.`);
|
|
4543
|
+
}
|
|
4544
|
+
return {
|
|
4545
|
+
name: tool.key,
|
|
4546
|
+
category: tool.category,
|
|
4547
|
+
command: tool.command,
|
|
4548
|
+
available: true,
|
|
4549
|
+
version,
|
|
4550
|
+
minRecommendedVersion: tool.minRecommendedVersion,
|
|
4551
|
+
status,
|
|
4552
|
+
risks
|
|
4553
|
+
};
|
|
4554
|
+
} catch {
|
|
4555
|
+
return {
|
|
4556
|
+
name: tool.key,
|
|
4557
|
+
category: tool.category,
|
|
4558
|
+
command: tool.command,
|
|
4559
|
+
available: false,
|
|
4560
|
+
version: null,
|
|
4561
|
+
minRecommendedVersion: tool.minRecommendedVersion,
|
|
4562
|
+
status: "unavailable",
|
|
4563
|
+
risks: ["Tool is not installed or not on PATH; Vibgrate cannot execute this scanner directly."]
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
async function detectSecretHeuristics(rootDir, cache) {
|
|
4568
|
+
const entries = await cache.walkDir(rootDir);
|
|
4569
|
+
const findings = [];
|
|
4570
|
+
for (const entry of entries) {
|
|
4571
|
+
if (!entry.isFile) continue;
|
|
4572
|
+
const ext = path16.extname(entry.name).toLowerCase();
|
|
4573
|
+
if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
|
|
4574
|
+
const content = await cache.readTextFile(entry.absPath);
|
|
4575
|
+
if (!content || content.length > 3e5) continue;
|
|
4576
|
+
for (const detector of SECRET_HEURISTICS) {
|
|
4577
|
+
const match = detector.pattern.exec(content);
|
|
4578
|
+
detector.pattern.lastIndex = 0;
|
|
4579
|
+
if (match) {
|
|
4580
|
+
findings.push({
|
|
4581
|
+
file: entry.relPath,
|
|
4582
|
+
detector: detector.detector,
|
|
4583
|
+
sample: match[0].slice(0, 16)
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
if (findings.length >= 25) return findings;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
return findings;
|
|
4590
|
+
}
|
|
4591
|
+
async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
|
|
4592
|
+
const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
|
|
4593
|
+
const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
|
|
4594
|
+
const configFiles = {
|
|
4595
|
+
semgrep: await cache.pathExists(path16.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path16.join(rootDir, ".semgrep.yaml")),
|
|
4596
|
+
gitleaks: await cache.pathExists(path16.join(rootDir, ".gitleaks.toml")),
|
|
4597
|
+
trufflehog: await cache.pathExists(path16.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path16.join(rootDir, ".trufflehog.yaml"))
|
|
4598
|
+
};
|
|
4599
|
+
return {
|
|
4600
|
+
semgrep,
|
|
4601
|
+
secretScanners: [gitleaks, trufflehog],
|
|
4602
|
+
configFiles,
|
|
4603
|
+
heuristicFindings
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4425
4607
|
// src/scanners/service-dependencies.ts
|
|
4426
4608
|
var SERVICE_CATEGORIES = {
|
|
4427
4609
|
payment: {
|
|
@@ -4837,7 +5019,7 @@ function scanServiceDependencies(projects) {
|
|
|
4837
5019
|
}
|
|
4838
5020
|
|
|
4839
5021
|
// src/scanners/architecture.ts
|
|
4840
|
-
import * as
|
|
5022
|
+
import * as path17 from "path";
|
|
4841
5023
|
import * as fs6 from "fs/promises";
|
|
4842
5024
|
var ARCHETYPE_SIGNALS = [
|
|
4843
5025
|
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
@@ -5136,9 +5318,9 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5136
5318
|
const entries = await cache.walkDir(rootDir);
|
|
5137
5319
|
return entries.filter((e) => {
|
|
5138
5320
|
if (!e.isFile) return false;
|
|
5139
|
-
const name =
|
|
5321
|
+
const name = path17.basename(e.absPath);
|
|
5140
5322
|
if (name.startsWith(".") && name !== ".") return false;
|
|
5141
|
-
const ext =
|
|
5323
|
+
const ext = path17.extname(name);
|
|
5142
5324
|
return SOURCE_EXTENSIONS.has(ext);
|
|
5143
5325
|
}).map((e) => e.relPath);
|
|
5144
5326
|
}
|
|
@@ -5152,15 +5334,15 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5152
5334
|
}
|
|
5153
5335
|
for (const entry of entries) {
|
|
5154
5336
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
5155
|
-
const fullPath =
|
|
5337
|
+
const fullPath = path17.join(dir, entry.name);
|
|
5156
5338
|
if (entry.isDirectory()) {
|
|
5157
5339
|
if (!IGNORE_DIRS.has(entry.name)) {
|
|
5158
5340
|
await walk(fullPath);
|
|
5159
5341
|
}
|
|
5160
5342
|
} else if (entry.isFile()) {
|
|
5161
|
-
const ext =
|
|
5343
|
+
const ext = path17.extname(entry.name);
|
|
5162
5344
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
5163
|
-
files.push(
|
|
5345
|
+
files.push(path17.relative(rootDir, fullPath));
|
|
5164
5346
|
}
|
|
5165
5347
|
}
|
|
5166
5348
|
}
|
|
@@ -5184,7 +5366,7 @@ function classifyFile(filePath, archetype) {
|
|
|
5184
5366
|
}
|
|
5185
5367
|
}
|
|
5186
5368
|
if (!bestMatch || bestMatch.confidence < 0.7) {
|
|
5187
|
-
const baseName =
|
|
5369
|
+
const baseName = path17.basename(filePath, path17.extname(filePath));
|
|
5188
5370
|
const cleanBase = baseName.replace(/\.(test|spec)$/, "");
|
|
5189
5371
|
for (const rule of SUFFIX_RULES) {
|
|
5190
5372
|
if (cleanBase.endsWith(rule.suffix)) {
|
|
@@ -5368,6 +5550,411 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
|
|
|
5368
5550
|
};
|
|
5369
5551
|
}
|
|
5370
5552
|
|
|
5553
|
+
// src/scanners/code-quality.ts
|
|
5554
|
+
import * as path18 from "path";
|
|
5555
|
+
import * as ts from "typescript";
|
|
5556
|
+
var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
5557
|
+
var DEFAULT_RESULT = {
|
|
5558
|
+
filesAnalyzed: 0,
|
|
5559
|
+
functionsAnalyzed: 0,
|
|
5560
|
+
avgCyclomaticComplexity: 0,
|
|
5561
|
+
avgFunctionLength: 0,
|
|
5562
|
+
maxNestingDepth: 0,
|
|
5563
|
+
godFiles: [],
|
|
5564
|
+
circularDependencies: 0,
|
|
5565
|
+
deadCodePercent: 0
|
|
5566
|
+
};
|
|
5567
|
+
async function scanCodeQuality(rootDir, cache) {
|
|
5568
|
+
const filePaths = await findSourceFiles(rootDir, cache);
|
|
5569
|
+
if (filePaths.length === 0) return { ...DEFAULT_RESULT };
|
|
5570
|
+
let totalFunctions = 0;
|
|
5571
|
+
let totalComplexity = 0;
|
|
5572
|
+
let totalFunctionLength = 0;
|
|
5573
|
+
let maxNestingDepth = 0;
|
|
5574
|
+
let deadFunctions = 0;
|
|
5575
|
+
const godFiles = [];
|
|
5576
|
+
const depGraph = /* @__PURE__ */ new Map();
|
|
5577
|
+
for (const filePath of filePaths) {
|
|
5578
|
+
let raw = "";
|
|
5579
|
+
try {
|
|
5580
|
+
raw = cache ? await cache.readTextFile(filePath) : await readTextFile(filePath);
|
|
5581
|
+
} catch {
|
|
5582
|
+
continue;
|
|
5583
|
+
}
|
|
5584
|
+
if (!raw.trim()) continue;
|
|
5585
|
+
const rel = normalizeModuleId(path18.relative(rootDir, filePath));
|
|
5586
|
+
const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
|
|
5587
|
+
const imports = collectLocalImports(source, path18.dirname(filePath), rootDir);
|
|
5588
|
+
depGraph.set(rel, imports);
|
|
5589
|
+
const fileMetrics = computeFileMetrics(source, raw);
|
|
5590
|
+
totalFunctions += fileMetrics.functionsAnalyzed;
|
|
5591
|
+
totalComplexity += fileMetrics.totalComplexity;
|
|
5592
|
+
totalFunctionLength += fileMetrics.totalFunctionLength;
|
|
5593
|
+
maxNestingDepth = Math.max(maxNestingDepth, fileMetrics.maxNestingDepth);
|
|
5594
|
+
deadFunctions += fileMetrics.deadFunctionCount;
|
|
5595
|
+
const fileAvgComplexity = fileMetrics.functionsAnalyzed > 0 ? fileMetrics.totalComplexity / fileMetrics.functionsAnalyzed : 0;
|
|
5596
|
+
if (fileMetrics.lines >= 450 || fileMetrics.functionsAnalyzed >= 25 || fileMetrics.functionsAnalyzed >= 10 && fileAvgComplexity >= 8) {
|
|
5597
|
+
godFiles.push({
|
|
5598
|
+
path: rel,
|
|
5599
|
+
lines: fileMetrics.lines,
|
|
5600
|
+
functionCount: fileMetrics.functionsAnalyzed,
|
|
5601
|
+
averageComplexity: round2(fileAvgComplexity)
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
const circularDependencies = countCircularDependencyChains(depGraph);
|
|
5606
|
+
const deadCodePercent = totalFunctions > 0 ? deadFunctions / totalFunctions * 100 : 0;
|
|
5607
|
+
return {
|
|
5608
|
+
filesAnalyzed: depGraph.size,
|
|
5609
|
+
functionsAnalyzed: totalFunctions,
|
|
5610
|
+
avgCyclomaticComplexity: totalFunctions > 0 ? round2(totalComplexity / totalFunctions) : 0,
|
|
5611
|
+
avgFunctionLength: totalFunctions > 0 ? round2(totalFunctionLength / totalFunctions) : 0,
|
|
5612
|
+
maxNestingDepth,
|
|
5613
|
+
godFiles: godFiles.sort((a, b) => b.lines - a.lines || b.functionCount - a.functionCount).slice(0, 10),
|
|
5614
|
+
circularDependencies,
|
|
5615
|
+
deadCodePercent: round2(deadCodePercent)
|
|
5616
|
+
};
|
|
5617
|
+
}
|
|
5618
|
+
async function findSourceFiles(rootDir, cache) {
|
|
5619
|
+
if (cache) {
|
|
5620
|
+
const entries = await cache.walkDir(rootDir);
|
|
5621
|
+
return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path18.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
|
|
5622
|
+
}
|
|
5623
|
+
const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path18.extname(name).toLowerCase()));
|
|
5624
|
+
return files;
|
|
5625
|
+
}
|
|
5626
|
+
function collectLocalImports(source, fileDir, rootDir) {
|
|
5627
|
+
const deps = /* @__PURE__ */ new Set();
|
|
5628
|
+
const visit = (node) => {
|
|
5629
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
5630
|
+
const target = resolveLocalImport(node.moduleSpecifier.text, fileDir, rootDir);
|
|
5631
|
+
if (target) deps.add(target);
|
|
5632
|
+
}
|
|
5633
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments[0] && ts.isStringLiteral(node.arguments[0])) {
|
|
5634
|
+
const target = resolveLocalImport(node.arguments[0].text, fileDir, rootDir);
|
|
5635
|
+
if (target) deps.add(target);
|
|
5636
|
+
}
|
|
5637
|
+
visitEach(node, visit);
|
|
5638
|
+
};
|
|
5639
|
+
visit(source);
|
|
5640
|
+
return [...deps];
|
|
5641
|
+
}
|
|
5642
|
+
function resolveLocalImport(specifier, fileDir, rootDir) {
|
|
5643
|
+
if (!specifier.startsWith(".")) return null;
|
|
5644
|
+
const rawTarget = path18.resolve(fileDir, specifier);
|
|
5645
|
+
const normalized = path18.relative(rootDir, rawTarget).replace(/\\/g, "/");
|
|
5646
|
+
if (!normalized || normalized.startsWith("..")) return null;
|
|
5647
|
+
return normalizeModuleId(normalized);
|
|
5648
|
+
}
|
|
5649
|
+
function normalizeModuleId(relPath) {
|
|
5650
|
+
return relPath.replace(/\\/g, "/").replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "").replace(/\/index$/, "");
|
|
5651
|
+
}
|
|
5652
|
+
function computeFileMetrics(source, raw) {
|
|
5653
|
+
let functionsAnalyzed = 0;
|
|
5654
|
+
let totalComplexity = 0;
|
|
5655
|
+
let totalFunctionLength = 0;
|
|
5656
|
+
let maxNestingDepth = 0;
|
|
5657
|
+
let deadFunctionCount = 0;
|
|
5658
|
+
const functionDecls = [];
|
|
5659
|
+
const visit = (node) => {
|
|
5660
|
+
if (isFunctionLike(node)) {
|
|
5661
|
+
const complexity = computeCyclomatic(node);
|
|
5662
|
+
const lineLength = computeNodeLineLength(source, node);
|
|
5663
|
+
const nestingDepth = computeMaxNestingDepth(node);
|
|
5664
|
+
functionsAnalyzed++;
|
|
5665
|
+
totalComplexity += complexity;
|
|
5666
|
+
totalFunctionLength += lineLength;
|
|
5667
|
+
maxNestingDepth = Math.max(maxNestingDepth, nestingDepth);
|
|
5668
|
+
}
|
|
5669
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
5670
|
+
functionDecls.push(node);
|
|
5671
|
+
}
|
|
5672
|
+
visitEach(node, visit);
|
|
5673
|
+
};
|
|
5674
|
+
visit(source);
|
|
5675
|
+
const functionBodies = raw;
|
|
5676
|
+
for (const fn of functionDecls) {
|
|
5677
|
+
if (isExported(fn)) continue;
|
|
5678
|
+
const name = fn.name?.text;
|
|
5679
|
+
if (!name) continue;
|
|
5680
|
+
const refs = countWholeWord(functionBodies, name);
|
|
5681
|
+
if (refs <= 1) deadFunctionCount++;
|
|
5682
|
+
}
|
|
5683
|
+
return {
|
|
5684
|
+
lines: raw.split(/\r?\n/).length,
|
|
5685
|
+
functionsAnalyzed,
|
|
5686
|
+
totalComplexity,
|
|
5687
|
+
totalFunctionLength,
|
|
5688
|
+
maxNestingDepth,
|
|
5689
|
+
deadFunctionCount
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
function computeCyclomatic(fn) {
|
|
5693
|
+
let complexity = 1;
|
|
5694
|
+
const visit = (node) => {
|
|
5695
|
+
switch (node.kind) {
|
|
5696
|
+
case ts.SyntaxKind.IfStatement:
|
|
5697
|
+
case ts.SyntaxKind.ForStatement:
|
|
5698
|
+
case ts.SyntaxKind.ForOfStatement:
|
|
5699
|
+
case ts.SyntaxKind.ForInStatement:
|
|
5700
|
+
case ts.SyntaxKind.WhileStatement:
|
|
5701
|
+
case ts.SyntaxKind.DoStatement:
|
|
5702
|
+
case ts.SyntaxKind.CaseClause:
|
|
5703
|
+
case ts.SyntaxKind.CatchClause:
|
|
5704
|
+
case ts.SyntaxKind.ConditionalExpression:
|
|
5705
|
+
complexity++;
|
|
5706
|
+
break;
|
|
5707
|
+
case ts.SyntaxKind.BinaryExpression: {
|
|
5708
|
+
const be = node;
|
|
5709
|
+
if (be.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || be.operatorToken.kind === ts.SyntaxKind.BarBarToken || be.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) {
|
|
5710
|
+
complexity++;
|
|
5711
|
+
}
|
|
5712
|
+
break;
|
|
5713
|
+
}
|
|
5714
|
+
default:
|
|
5715
|
+
break;
|
|
5716
|
+
}
|
|
5717
|
+
visitEach(node, visit);
|
|
5718
|
+
};
|
|
5719
|
+
visit(fn);
|
|
5720
|
+
return complexity;
|
|
5721
|
+
}
|
|
5722
|
+
function computeMaxNestingDepth(node) {
|
|
5723
|
+
let maxDepth = 0;
|
|
5724
|
+
const walk = (current, depth) => {
|
|
5725
|
+
const nextDepth = isNestingNode(current) ? depth + 1 : depth;
|
|
5726
|
+
maxDepth = Math.max(maxDepth, nextDepth);
|
|
5727
|
+
visitEach(current, (child) => walk(child, nextDepth));
|
|
5728
|
+
};
|
|
5729
|
+
walk(node, 0);
|
|
5730
|
+
return Math.max(0, maxDepth - 1);
|
|
5731
|
+
}
|
|
5732
|
+
function countCircularDependencyChains(graph) {
|
|
5733
|
+
let cycles = 0;
|
|
5734
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5735
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
5736
|
+
const dfs = (node) => {
|
|
5737
|
+
visited.add(node);
|
|
5738
|
+
inStack.add(node);
|
|
5739
|
+
const deps = graph.get(node) ?? [];
|
|
5740
|
+
for (const dep of deps) {
|
|
5741
|
+
if (!graph.has(dep)) continue;
|
|
5742
|
+
if (!visited.has(dep)) {
|
|
5743
|
+
dfs(dep);
|
|
5744
|
+
} else if (inStack.has(dep)) {
|
|
5745
|
+
cycles++;
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
inStack.delete(node);
|
|
5749
|
+
};
|
|
5750
|
+
for (const node of graph.keys()) {
|
|
5751
|
+
if (!visited.has(node)) dfs(node);
|
|
5752
|
+
}
|
|
5753
|
+
return cycles;
|
|
5754
|
+
}
|
|
5755
|
+
function computeNodeLineLength(source, node) {
|
|
5756
|
+
const start = source.getLineAndCharacterOfPosition(node.getStart(source)).line;
|
|
5757
|
+
const end = source.getLineAndCharacterOfPosition(node.getEnd()).line;
|
|
5758
|
+
return end - start + 1;
|
|
5759
|
+
}
|
|
5760
|
+
function isFunctionLike(node) {
|
|
5761
|
+
return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isConstructorDeclaration(node);
|
|
5762
|
+
}
|
|
5763
|
+
function isNestingNode(node) {
|
|
5764
|
+
return node.kind === ts.SyntaxKind.IfStatement || node.kind === ts.SyntaxKind.ForStatement || node.kind === ts.SyntaxKind.ForOfStatement || node.kind === ts.SyntaxKind.ForInStatement || node.kind === ts.SyntaxKind.WhileStatement || node.kind === ts.SyntaxKind.DoStatement || node.kind === ts.SyntaxKind.SwitchStatement || node.kind === ts.SyntaxKind.TryStatement || node.kind === ts.SyntaxKind.CatchClause;
|
|
5765
|
+
}
|
|
5766
|
+
function isExported(node) {
|
|
5767
|
+
if (!ts.canHaveModifiers(node)) return false;
|
|
5768
|
+
const modifiers = ts.getModifiers(node);
|
|
5769
|
+
return !!modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
5770
|
+
}
|
|
5771
|
+
function countWholeWord(input, word) {
|
|
5772
|
+
const re = new RegExp(`\\b${escapeRegExp(word)}\\b`, "g");
|
|
5773
|
+
return input.match(re)?.length ?? 0;
|
|
5774
|
+
}
|
|
5775
|
+
function escapeRegExp(input) {
|
|
5776
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5777
|
+
}
|
|
5778
|
+
function round2(n) {
|
|
5779
|
+
return Math.round(n * 100) / 100;
|
|
5780
|
+
}
|
|
5781
|
+
function visitEach(node, cb) {
|
|
5782
|
+
node.forEachChild(cb);
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
// src/scanners/owasp-category-mapping.ts
|
|
5786
|
+
import { spawn as spawn3 } from "child_process";
|
|
5787
|
+
import * as path19 from "path";
|
|
5788
|
+
var OWASP_CONFIG = "p/owasp-top-ten";
|
|
5789
|
+
var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5790
|
+
".js",
|
|
5791
|
+
".jsx",
|
|
5792
|
+
".ts",
|
|
5793
|
+
".tsx",
|
|
5794
|
+
".mjs",
|
|
5795
|
+
".cjs",
|
|
5796
|
+
".py",
|
|
5797
|
+
".rb",
|
|
5798
|
+
".java",
|
|
5799
|
+
".cs",
|
|
5800
|
+
".go",
|
|
5801
|
+
".php",
|
|
5802
|
+
".scala",
|
|
5803
|
+
".kt",
|
|
5804
|
+
".yaml",
|
|
5805
|
+
".yml",
|
|
5806
|
+
".json",
|
|
5807
|
+
".toml",
|
|
5808
|
+
".xml",
|
|
5809
|
+
".env"
|
|
5810
|
+
]);
|
|
5811
|
+
async function runSemgrep(args, cwd, stdin) {
|
|
5812
|
+
return new Promise((resolve9, reject) => {
|
|
5813
|
+
const child = spawn3("semgrep", args, {
|
|
5814
|
+
cwd,
|
|
5815
|
+
shell: true,
|
|
5816
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5817
|
+
});
|
|
5818
|
+
let stdout = "";
|
|
5819
|
+
let stderr = "";
|
|
5820
|
+
child.stdout.on("data", (d) => {
|
|
5821
|
+
stdout += String(d);
|
|
5822
|
+
});
|
|
5823
|
+
child.stderr.on("data", (d) => {
|
|
5824
|
+
stderr += String(d);
|
|
5825
|
+
});
|
|
5826
|
+
child.on("error", reject);
|
|
5827
|
+
child.on("close", (code) => {
|
|
5828
|
+
resolve9({ code: code ?? 1, stdout, stderr });
|
|
5829
|
+
});
|
|
5830
|
+
if (stdin !== void 0) child.stdin.write(stdin);
|
|
5831
|
+
child.stdin.end();
|
|
5832
|
+
});
|
|
5833
|
+
}
|
|
5834
|
+
function parseSemgrepJson(raw) {
|
|
5835
|
+
try {
|
|
5836
|
+
return JSON.parse(raw);
|
|
5837
|
+
} catch {
|
|
5838
|
+
return { results: [] };
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
function normalizeCategory(item) {
|
|
5842
|
+
if (typeof item !== "string") return null;
|
|
5843
|
+
const val = item.trim();
|
|
5844
|
+
return val.length > 0 ? val : null;
|
|
5845
|
+
}
|
|
5846
|
+
function categoriesFromMetadata(metadata) {
|
|
5847
|
+
if (!metadata) return [];
|
|
5848
|
+
const raw = metadata.owasp;
|
|
5849
|
+
if (Array.isArray(raw)) {
|
|
5850
|
+
return raw.map(normalizeCategory).filter((v) => v !== null);
|
|
5851
|
+
}
|
|
5852
|
+
const single = normalizeCategory(raw);
|
|
5853
|
+
return single ? [single] : [];
|
|
5854
|
+
}
|
|
5855
|
+
function severityLabel(severity) {
|
|
5856
|
+
const s = (severity ?? "").toUpperCase();
|
|
5857
|
+
if (s === "ERROR") return "high";
|
|
5858
|
+
if (s === "WARNING") return "medium";
|
|
5859
|
+
return "low";
|
|
5860
|
+
}
|
|
5861
|
+
function parseFindings(results, rootDir) {
|
|
5862
|
+
return results.map((r) => {
|
|
5863
|
+
const metadata = r.extra?.metadata;
|
|
5864
|
+
return {
|
|
5865
|
+
ruleId: r.check_id ?? "unknown",
|
|
5866
|
+
path: r.path ? path19.relative(rootDir, path19.resolve(rootDir, r.path)) : "",
|
|
5867
|
+
line: r.start?.line ?? 1,
|
|
5868
|
+
endLine: r.end?.line,
|
|
5869
|
+
message: r.extra?.message ?? "Potential security issue",
|
|
5870
|
+
severity: severityLabel(r.extra?.severity),
|
|
5871
|
+
categories: categoriesFromMetadata(metadata),
|
|
5872
|
+
cwe: typeof metadata?.cwe === "string" ? metadata.cwe : null
|
|
5873
|
+
};
|
|
5874
|
+
});
|
|
5875
|
+
}
|
|
5876
|
+
function summarizeCategories(findings) {
|
|
5877
|
+
const counts = {};
|
|
5878
|
+
for (const finding of findings) {
|
|
5879
|
+
for (const cat of finding.categories) {
|
|
5880
|
+
counts[cat] = (counts[cat] ?? 0) + 1;
|
|
5881
|
+
}
|
|
5882
|
+
}
|
|
5883
|
+
return Object.fromEntries(Object.entries(counts).sort((a, b) => b[1] - a[1]));
|
|
5884
|
+
}
|
|
5885
|
+
async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = runSemgrep) {
|
|
5886
|
+
const mode = options.mode ?? "fast";
|
|
5887
|
+
if (mode === "fast") {
|
|
5888
|
+
try {
|
|
5889
|
+
const args = ["scan", "--config", OWASP_CONFIG, "--json", "--quiet", rootDir];
|
|
5890
|
+
const result = await runner(args, rootDir);
|
|
5891
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
5892
|
+
return {
|
|
5893
|
+
scanner: "semgrep",
|
|
5894
|
+
available: false,
|
|
5895
|
+
mode,
|
|
5896
|
+
scannedFiles: 0,
|
|
5897
|
+
findings: [],
|
|
5898
|
+
categoryCounts: {},
|
|
5899
|
+
errors: [result.stderr.trim() || `semgrep exited with code ${result.code}`]
|
|
5900
|
+
};
|
|
5901
|
+
}
|
|
5902
|
+
const parsed = parseSemgrepJson(result.stdout);
|
|
5903
|
+
const findings2 = parseFindings(parsed.results ?? [], rootDir);
|
|
5904
|
+
return {
|
|
5905
|
+
scanner: "semgrep",
|
|
5906
|
+
available: true,
|
|
5907
|
+
mode,
|
|
5908
|
+
scannedFiles: 0,
|
|
5909
|
+
findings: findings2,
|
|
5910
|
+
categoryCounts: summarizeCategories(findings2),
|
|
5911
|
+
errors: []
|
|
5912
|
+
};
|
|
5913
|
+
} catch (err) {
|
|
5914
|
+
return {
|
|
5915
|
+
scanner: "semgrep",
|
|
5916
|
+
available: false,
|
|
5917
|
+
mode,
|
|
5918
|
+
scannedFiles: 0,
|
|
5919
|
+
findings: [],
|
|
5920
|
+
categoryCounts: {},
|
|
5921
|
+
errors: [err instanceof Error ? err.message : "semgrep unavailable"]
|
|
5922
|
+
};
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
const entries = cache ? await cache.walkDir(rootDir) : [];
|
|
5926
|
+
const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path19.extname(e.name).toLowerCase()));
|
|
5927
|
+
const findings = [];
|
|
5928
|
+
const errors = [];
|
|
5929
|
+
let scannedFiles = 0;
|
|
5930
|
+
for (const file of files) {
|
|
5931
|
+
try {
|
|
5932
|
+
const content = cache ? await cache.readTextFile(file.absPath) : "";
|
|
5933
|
+
if (!content) continue;
|
|
5934
|
+
const args = ["scan", "--config", OWASP_CONFIG, "--json", "--quiet", "--stdin", "--stdin-filename", file.relPath];
|
|
5935
|
+
const result = await runner(args, rootDir, content);
|
|
5936
|
+
if (result.code !== 0 && !result.stdout.trim()) {
|
|
5937
|
+
errors.push(result.stderr.trim() || `semgrep failed for ${file.relPath}`);
|
|
5938
|
+
continue;
|
|
5939
|
+
}
|
|
5940
|
+
const parsed = parseSemgrepJson(result.stdout);
|
|
5941
|
+
findings.push(...parseFindings(parsed.results ?? [], rootDir));
|
|
5942
|
+
scannedFiles += 1;
|
|
5943
|
+
} catch (err) {
|
|
5944
|
+
errors.push(err instanceof Error ? err.message : `semgrep failed for ${file.relPath}`);
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
return {
|
|
5948
|
+
scanner: "semgrep",
|
|
5949
|
+
available: errors.length === 0 || findings.length > 0,
|
|
5950
|
+
mode,
|
|
5951
|
+
scannedFiles,
|
|
5952
|
+
findings,
|
|
5953
|
+
categoryCounts: summarizeCategories(findings),
|
|
5954
|
+
errors
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5371
5958
|
// src/commands/scan.ts
|
|
5372
5959
|
async function runScan(rootDir, opts) {
|
|
5373
5960
|
const scanStart = Date.now();
|
|
@@ -5394,12 +5981,15 @@ async function runScan(rootDir, opts) {
|
|
|
5394
5981
|
...scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
|
|
5395
5982
|
...scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
|
|
5396
5983
|
...scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
|
|
5984
|
+
...scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
|
|
5397
5985
|
...scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
|
|
5398
5986
|
...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
5399
5987
|
...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
5400
5988
|
...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
5401
5989
|
...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
|
|
5402
|
-
...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : []
|
|
5990
|
+
...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
|
|
5991
|
+
...scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
|
|
5992
|
+
...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : []
|
|
5403
5993
|
] : [],
|
|
5404
5994
|
{ id: "drift", label: "Computing drift score" },
|
|
5405
5995
|
{ id: "findings", label: "Generating findings" }
|
|
@@ -5532,6 +6122,22 @@ async function runScan(rootDir, opts) {
|
|
|
5532
6122
|
})
|
|
5533
6123
|
);
|
|
5534
6124
|
}
|
|
6125
|
+
if (scanners?.securityScanners?.enabled !== false) {
|
|
6126
|
+
progress.startStep("secscan");
|
|
6127
|
+
scannerTasks.push(
|
|
6128
|
+
scanSecurityScanners(rootDir, fileCache).then((result) => {
|
|
6129
|
+
extended.securityScanners = result;
|
|
6130
|
+
const unavailable = [result.semgrep, ...result.secretScanners].filter((t) => !t.available).length;
|
|
6131
|
+
const stale = [result.semgrep, ...result.secretScanners].filter((t) => t.status === "review-needed").length;
|
|
6132
|
+
const findings2 = result.heuristicFindings.length;
|
|
6133
|
+
const parts = [];
|
|
6134
|
+
if (unavailable > 0) parts.push(`${unavailable} missing`);
|
|
6135
|
+
if (stale > 0) parts.push(`${stale} outdated`);
|
|
6136
|
+
if (findings2 > 0) parts.push(`${findings2} secret signal${findings2 !== 1 ? "s" : ""}`);
|
|
6137
|
+
progress.completeStep("secscan", parts.join(" \xB7 ") || "ready");
|
|
6138
|
+
})
|
|
6139
|
+
);
|
|
6140
|
+
}
|
|
5535
6141
|
if (scanners?.buildDeploy?.enabled !== false) {
|
|
5536
6142
|
progress.startStep("build");
|
|
5537
6143
|
scannerTasks.push(
|
|
@@ -5576,6 +6182,19 @@ async function runScan(rootDir, opts) {
|
|
|
5576
6182
|
})
|
|
5577
6183
|
);
|
|
5578
6184
|
}
|
|
6185
|
+
if (scanners?.codeQuality?.enabled !== false) {
|
|
6186
|
+
progress.startStep("codequality");
|
|
6187
|
+
scannerTasks.push(
|
|
6188
|
+
scanCodeQuality(rootDir, fileCache).then((result) => {
|
|
6189
|
+
extended.codeQuality = result;
|
|
6190
|
+
const cqParts = [];
|
|
6191
|
+
cqParts.push(`${result.filesAnalyzed} files`);
|
|
6192
|
+
cqParts.push(`${result.functionsAnalyzed} functions`);
|
|
6193
|
+
if (result.circularDependencies > 0) cqParts.push(`${result.circularDependencies} cycles`);
|
|
6194
|
+
progress.completeStep("codequality", cqParts.join(" \xB7 "), result.functionsAnalyzed);
|
|
6195
|
+
})
|
|
6196
|
+
);
|
|
6197
|
+
}
|
|
5579
6198
|
if (scanners?.dependencyRisk?.enabled !== false) {
|
|
5580
6199
|
progress.startStep("deprisk");
|
|
5581
6200
|
scannerTasks.push(
|
|
@@ -5590,6 +6209,25 @@ async function runScan(rootDir, opts) {
|
|
|
5590
6209
|
);
|
|
5591
6210
|
}
|
|
5592
6211
|
await Promise.all(scannerTasks);
|
|
6212
|
+
if (scanners?.owaspCategoryMapping?.enabled !== false) {
|
|
6213
|
+
progress.startStep("owasp");
|
|
6214
|
+
extended.owaspCategoryMapping = await scanOwaspCategoryMapping(
|
|
6215
|
+
rootDir,
|
|
6216
|
+
fileCache,
|
|
6217
|
+
{ mode: scanners?.owaspCategoryMapping?.mode ?? "fast" }
|
|
6218
|
+
);
|
|
6219
|
+
const owasp = extended.owaspCategoryMapping;
|
|
6220
|
+
if (!owasp.available) {
|
|
6221
|
+
progress.completeStep("owasp", "scanner unavailable");
|
|
6222
|
+
} else {
|
|
6223
|
+
const catCount = Object.keys(owasp.categoryCounts).length;
|
|
6224
|
+
progress.completeStep(
|
|
6225
|
+
"owasp",
|
|
6226
|
+
`${owasp.findings.length} finding${owasp.findings.length !== 1 ? "s" : ""} \xB7 ${catCount} categor${catCount === 1 ? "y" : "ies"}`,
|
|
6227
|
+
owasp.findings.length
|
|
6228
|
+
);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
5593
6231
|
if (scanners?.architecture?.enabled !== false) {
|
|
5594
6232
|
progress.startStep("architecture");
|
|
5595
6233
|
extended.architecture = await scanArchitecture(
|
|
@@ -5659,18 +6297,21 @@ async function runScan(rootDir, opts) {
|
|
|
5659
6297
|
}
|
|
5660
6298
|
if (extended.fileHotspots) filesScanned += extended.fileHotspots.totalFiles;
|
|
5661
6299
|
if (extended.securityPosture) filesScanned += 1;
|
|
6300
|
+
if (extended.securityScanners) filesScanned += extended.securityScanners.heuristicFindings.length;
|
|
5662
6301
|
if (extended.tsModernity?.typescriptVersion) filesScanned += 1;
|
|
5663
6302
|
if (extended.dependencyGraph?.lockfileType) filesScanned += 1;
|
|
5664
6303
|
if (extended.buildDeploy) {
|
|
5665
6304
|
filesScanned += extended.buildDeploy.docker.dockerfileCount;
|
|
5666
6305
|
filesScanned += extended.buildDeploy.ci.length;
|
|
5667
6306
|
}
|
|
6307
|
+
if (extended.codeQuality) filesScanned += extended.codeQuality.filesAnalyzed;
|
|
6308
|
+
if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
|
|
5668
6309
|
const durationMs = Date.now() - scanStart;
|
|
5669
6310
|
const artifact = {
|
|
5670
6311
|
schemaVersion: "1.0",
|
|
5671
6312
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5672
6313
|
vibgrateVersion: VERSION,
|
|
5673
|
-
rootPath:
|
|
6314
|
+
rootPath: path20.basename(rootDir),
|
|
5674
6315
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
5675
6316
|
projects: allProjects,
|
|
5676
6317
|
drift,
|
|
@@ -5681,7 +6322,7 @@ async function runScan(rootDir, opts) {
|
|
|
5681
6322
|
treeSummary: treeCount
|
|
5682
6323
|
};
|
|
5683
6324
|
if (opts.baseline) {
|
|
5684
|
-
const baselinePath =
|
|
6325
|
+
const baselinePath = path20.resolve(opts.baseline);
|
|
5685
6326
|
if (await pathExists(baselinePath)) {
|
|
5686
6327
|
try {
|
|
5687
6328
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -5692,9 +6333,9 @@ async function runScan(rootDir, opts) {
|
|
|
5692
6333
|
}
|
|
5693
6334
|
}
|
|
5694
6335
|
}
|
|
5695
|
-
const vibgrateDir =
|
|
6336
|
+
const vibgrateDir = path20.join(rootDir, ".vibgrate");
|
|
5696
6337
|
await ensureDir(vibgrateDir);
|
|
5697
|
-
await writeJsonFile(
|
|
6338
|
+
await writeJsonFile(path20.join(vibgrateDir, "scan_result.json"), artifact);
|
|
5698
6339
|
await saveScanHistory(rootDir, {
|
|
5699
6340
|
timestamp: artifact.timestamp,
|
|
5700
6341
|
totalDurationMs: durationMs,
|
|
@@ -5704,10 +6345,10 @@ async function runScan(rootDir, opts) {
|
|
|
5704
6345
|
});
|
|
5705
6346
|
for (const project of allProjects) {
|
|
5706
6347
|
if (project.drift && project.path) {
|
|
5707
|
-
const projectDir =
|
|
5708
|
-
const projectVibgrateDir =
|
|
6348
|
+
const projectDir = path20.resolve(rootDir, project.path);
|
|
6349
|
+
const projectVibgrateDir = path20.join(projectDir, ".vibgrate");
|
|
5709
6350
|
await ensureDir(projectVibgrateDir);
|
|
5710
|
-
await writeJsonFile(
|
|
6351
|
+
await writeJsonFile(path20.join(projectVibgrateDir, "project_score.json"), {
|
|
5711
6352
|
projectId: project.projectId,
|
|
5712
6353
|
name: project.name,
|
|
5713
6354
|
type: project.type,
|
|
@@ -5724,7 +6365,7 @@ async function runScan(rootDir, opts) {
|
|
|
5724
6365
|
if (opts.format === "json") {
|
|
5725
6366
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
5726
6367
|
if (opts.out) {
|
|
5727
|
-
await writeTextFile(
|
|
6368
|
+
await writeTextFile(path20.resolve(opts.out), jsonStr);
|
|
5728
6369
|
console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
5729
6370
|
} else {
|
|
5730
6371
|
console.log(jsonStr);
|
|
@@ -5733,7 +6374,7 @@ async function runScan(rootDir, opts) {
|
|
|
5733
6374
|
const sarif = formatSarif(artifact);
|
|
5734
6375
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
5735
6376
|
if (opts.out) {
|
|
5736
|
-
await writeTextFile(
|
|
6377
|
+
await writeTextFile(path20.resolve(opts.out), sarifStr);
|
|
5737
6378
|
console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
5738
6379
|
} else {
|
|
5739
6380
|
console.log(sarifStr);
|
|
@@ -5742,7 +6383,7 @@ async function runScan(rootDir, opts) {
|
|
|
5742
6383
|
const text = formatText(artifact);
|
|
5743
6384
|
console.log(text);
|
|
5744
6385
|
if (opts.out) {
|
|
5745
|
-
await writeTextFile(
|
|
6386
|
+
await writeTextFile(path20.resolve(opts.out), text);
|
|
5746
6387
|
}
|
|
5747
6388
|
}
|
|
5748
6389
|
return artifact;
|
|
@@ -5801,7 +6442,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
5801
6442
|
}
|
|
5802
6443
|
}
|
|
5803
6444
|
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").action(async (targetPath, opts) => {
|
|
5804
|
-
const rootDir =
|
|
6445
|
+
const rootDir = path20.resolve(targetPath);
|
|
5805
6446
|
if (!await pathExists(rootDir)) {
|
|
5806
6447
|
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
5807
6448
|
process.exit(1);
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-GN3IWKSY.js";
|
|
5
5
|
import {
|
|
6
6
|
baselineCommand
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-T4GNX4OC.js";
|
|
8
8
|
import {
|
|
9
9
|
VERSION,
|
|
10
10
|
dsnCommand,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
readJsonFile,
|
|
16
16
|
scanCommand,
|
|
17
17
|
writeDefaultConfig
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-XLRCQ476.js";
|
|
19
19
|
|
|
20
20
|
// src/cli.ts
|
|
21
21
|
import { Command as Command4 } from "commander";
|
|
@@ -38,7 +38,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
38
38
|
console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
|
|
39
39
|
}
|
|
40
40
|
if (opts.baseline) {
|
|
41
|
-
const { runBaseline } = await import("./baseline-
|
|
41
|
+
const { runBaseline } = await import("./baseline-AA2FVYAX.js");
|
|
42
42
|
await runBaseline(rootDir);
|
|
43
43
|
}
|
|
44
44
|
console.log("");
|
package/dist/index.d.ts
CHANGED
|
@@ -107,6 +107,10 @@ interface ScanOptions {
|
|
|
107
107
|
interface ScannerToggle {
|
|
108
108
|
enabled: boolean;
|
|
109
109
|
}
|
|
110
|
+
type OwaspScannerMode = 'fast' | 'cache-input';
|
|
111
|
+
interface OwaspScannerConfig extends ScannerToggle {
|
|
112
|
+
mode?: OwaspScannerMode;
|
|
113
|
+
}
|
|
110
114
|
interface ScannersConfig {
|
|
111
115
|
platformMatrix?: ScannerToggle;
|
|
112
116
|
dependencyRisk?: ScannerToggle;
|
|
@@ -117,8 +121,11 @@ interface ScannersConfig {
|
|
|
117
121
|
breakingChangeExposure?: ScannerToggle;
|
|
118
122
|
fileHotspots?: ScannerToggle;
|
|
119
123
|
securityPosture?: ScannerToggle;
|
|
124
|
+
securityScanners?: ScannerToggle;
|
|
120
125
|
serviceDependencies?: ScannerToggle;
|
|
121
126
|
architecture?: ScannerToggle;
|
|
127
|
+
codeQuality?: ScannerToggle;
|
|
128
|
+
owaspCategoryMapping?: OwaspScannerConfig;
|
|
122
129
|
}
|
|
123
130
|
interface VibgrateConfig {
|
|
124
131
|
include?: string[];
|
|
@@ -258,6 +265,32 @@ interface ServiceDependenciesResult {
|
|
|
258
265
|
storage: ServiceDependencyItem[];
|
|
259
266
|
search: ServiceDependencyItem[];
|
|
260
267
|
}
|
|
268
|
+
type SecurityScannerStatus = 'up-to-date' | 'review-needed' | 'unknown' | 'unavailable';
|
|
269
|
+
interface SecurityToolAssessment {
|
|
270
|
+
name: 'semgrep' | 'gitleaks' | 'trufflehog';
|
|
271
|
+
category: 'sast' | 'secrets';
|
|
272
|
+
command: string;
|
|
273
|
+
available: boolean;
|
|
274
|
+
version: string | null;
|
|
275
|
+
minRecommendedVersion: string;
|
|
276
|
+
status: SecurityScannerStatus;
|
|
277
|
+
risks: string[];
|
|
278
|
+
}
|
|
279
|
+
interface SecretHeuristicFinding {
|
|
280
|
+
file: string;
|
|
281
|
+
detector: string;
|
|
282
|
+
sample: string;
|
|
283
|
+
}
|
|
284
|
+
interface SecurityScannersResult {
|
|
285
|
+
semgrep: SecurityToolAssessment;
|
|
286
|
+
secretScanners: SecurityToolAssessment[];
|
|
287
|
+
configFiles: {
|
|
288
|
+
semgrep: boolean;
|
|
289
|
+
gitleaks: boolean;
|
|
290
|
+
trufflehog: boolean;
|
|
291
|
+
};
|
|
292
|
+
heuristicFindings: SecretHeuristicFinding[];
|
|
293
|
+
}
|
|
261
294
|
/** Detected project archetype (fingerprint) */
|
|
262
295
|
type ProjectArchetype = 'nextjs' | 'remix' | 'sveltekit' | 'nuxt' | 'nestjs' | 'express' | 'fastify' | 'hono' | 'koa' | 'serverless' | 'library' | 'cli' | 'monorepo' | 'unknown';
|
|
263
296
|
/** Architectural layer classification */
|
|
@@ -300,6 +333,22 @@ interface ArchitectureResult {
|
|
|
300
333
|
/** Files that could not be classified */
|
|
301
334
|
unclassified: number;
|
|
302
335
|
}
|
|
336
|
+
interface GodFile {
|
|
337
|
+
path: string;
|
|
338
|
+
lines: number;
|
|
339
|
+
functionCount: number;
|
|
340
|
+
averageComplexity: number;
|
|
341
|
+
}
|
|
342
|
+
interface CodeQualityResult {
|
|
343
|
+
filesAnalyzed: number;
|
|
344
|
+
functionsAnalyzed: number;
|
|
345
|
+
avgCyclomaticComplexity: number;
|
|
346
|
+
avgFunctionLength: number;
|
|
347
|
+
maxNestingDepth: number;
|
|
348
|
+
godFiles: GodFile[];
|
|
349
|
+
circularDependencies: number;
|
|
350
|
+
deadCodePercent: number;
|
|
351
|
+
}
|
|
303
352
|
interface ExtendedScanResults {
|
|
304
353
|
platformMatrix?: PlatformMatrixResult;
|
|
305
354
|
dependencyRisk?: DependencyRiskResult;
|
|
@@ -310,8 +359,30 @@ interface ExtendedScanResults {
|
|
|
310
359
|
breakingChangeExposure?: BreakingChangeExposureResult;
|
|
311
360
|
fileHotspots?: FileHotspotsResult;
|
|
312
361
|
securityPosture?: SecurityPostureResult;
|
|
362
|
+
securityScanners?: SecurityScannersResult;
|
|
313
363
|
serviceDependencies?: ServiceDependenciesResult;
|
|
314
364
|
architecture?: ArchitectureResult;
|
|
365
|
+
codeQuality?: CodeQualityResult;
|
|
366
|
+
owaspCategoryMapping?: OwaspCategoryMappingResult;
|
|
367
|
+
}
|
|
368
|
+
interface OwaspFinding {
|
|
369
|
+
ruleId: string;
|
|
370
|
+
path: string;
|
|
371
|
+
line: number;
|
|
372
|
+
endLine?: number;
|
|
373
|
+
message: string;
|
|
374
|
+
severity: 'low' | 'medium' | 'high';
|
|
375
|
+
categories: string[];
|
|
376
|
+
cwe: string | null;
|
|
377
|
+
}
|
|
378
|
+
interface OwaspCategoryMappingResult {
|
|
379
|
+
scanner: 'semgrep';
|
|
380
|
+
available: boolean;
|
|
381
|
+
mode: OwaspScannerMode;
|
|
382
|
+
scannedFiles: number;
|
|
383
|
+
findings: OwaspFinding[];
|
|
384
|
+
categoryCounts: Record<string, number>;
|
|
385
|
+
errors: string[];
|
|
315
386
|
}
|
|
316
387
|
|
|
317
388
|
declare function runScan(rootDir: string, opts: ScanOptions): Promise<ScanArtifact>;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibgrate/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "CLI for measuring upgrade drift across Node & .NET projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"eslint": "^9.0.0",
|
|
44
44
|
"tsup": "^8.0.0",
|
|
45
45
|
"tsx": "^4.0.0",
|
|
46
|
-
"typescript": "^5.4.0",
|
|
47
46
|
"vitest": "^2.0.0"
|
|
48
47
|
},
|
|
49
48
|
"dependencies": {
|
|
@@ -51,6 +50,7 @@
|
|
|
51
50
|
"commander": "^12.0.0",
|
|
52
51
|
"fast-xml-parser": "^4.3.0",
|
|
53
52
|
"semver": "^7.6.0",
|
|
53
|
+
"typescript": "^5.4.0",
|
|
54
54
|
"zod": "^3.23.0"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|