@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 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
- No source code is read. No secrets are scanned. The CLI works entirely offline dashboard upload is optional.
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
- ### Ten Extended Scanners
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 read** — only `package.json`, `tsconfig.json`, lockfiles, and project manifests
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`
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  baselineCommand,
3
3
  runBaseline
4
- } from "./chunk-7EEUYKZI.js";
5
- import "./chunk-27LB7QTA.js";
4
+ } from "./chunk-T4GNX4OC.js";
5
+ import "./chunk-XLRCQ476.js";
6
6
  export {
7
7
  baselineCommand,
8
8
  runBaseline
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runScan,
3
3
  writeJsonFile
4
- } from "./chunk-27LB7QTA.js";
4
+ } from "./chunk-XLRCQ476.js";
5
5
 
6
6
  // src/commands/baseline.ts
7
7
  import * as path from "path";
@@ -23,7 +23,7 @@ var Semaphore = class {
23
23
  this.available--;
24
24
  return Promise.resolve();
25
25
  }
26
- return new Promise((resolve7) => this.queue.push(resolve7));
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
- (resolve7) => setTimeout(() => resolve7({ ok: false }), STUCK_TIMEOUT_MS)
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 ts = ext.tsModernity;
943
+ const ts2 = ext.tsModernity;
944
944
  lines.push(chalk.bold.underline(" TypeScript"));
945
945
  const parts = [];
946
- parts.push(`v${ts.typescriptVersion}`);
947
- if (ts.strict === true) parts.push(chalk.green("strict \u2714"));
948
- else if (ts.strict === false) parts.push(chalk.yellow("strict \u2716"));
949
- if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
950
- if (ts.target) parts.push(`target: ${ts.target}`);
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 path17 from "path";
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((resolve7) => {
1606
- timer = setTimeout(() => resolve7({ ok: false }), ms);
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((resolve7, reject) => {
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
- resolve7(null);
1699
+ resolve9(null);
1650
1700
  return;
1651
1701
  }
1652
1702
  try {
1653
- resolve7(JSON.parse(trimmed));
1703
+ resolve9(JSON.parse(trimmed));
1654
1704
  } catch {
1655
- resolve7(trimmed.replace(/^"|"$/g, ""));
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 path16 from "path";
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 = path16.basename(e.absPath);
5321
+ const name = path17.basename(e.absPath);
5140
5322
  if (name.startsWith(".") && name !== ".") return false;
5141
- const ext = path16.extname(name);
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 = path16.join(dir, entry.name);
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 = path16.extname(entry.name);
5343
+ const ext = path17.extname(entry.name);
5162
5344
  if (SOURCE_EXTENSIONS.has(ext)) {
5163
- files.push(path16.relative(rootDir, fullPath));
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 = path16.basename(filePath, path16.extname(filePath));
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: path17.basename(rootDir),
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 = path17.resolve(opts.baseline);
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 = path17.join(rootDir, ".vibgrate");
6336
+ const vibgrateDir = path20.join(rootDir, ".vibgrate");
5696
6337
  await ensureDir(vibgrateDir);
5697
- await writeJsonFile(path17.join(vibgrateDir, "scan_result.json"), artifact);
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 = path17.resolve(rootDir, project.path);
5708
- const projectVibgrateDir = path17.join(projectDir, ".vibgrate");
6348
+ const projectDir = path20.resolve(rootDir, project.path);
6349
+ const projectVibgrateDir = path20.join(projectDir, ".vibgrate");
5709
6350
  await ensureDir(projectVibgrateDir);
5710
- await writeJsonFile(path17.join(projectVibgrateDir, "project_score.json"), {
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(path17.resolve(opts.out), jsonStr);
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(path17.resolve(opts.out), sarifStr);
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(path17.resolve(opts.out), text);
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 = path17.resolve(targetPath);
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-7EEUYKZI.js";
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-27LB7QTA.js";
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-KXUPTMQ2.js");
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
@@ -7,7 +7,7 @@ import {
7
7
  formatText,
8
8
  generateFindings,
9
9
  runScan
10
- } from "./chunk-27LB7QTA.js";
10
+ } from "./chunk-XLRCQ476.js";
11
11
  export {
12
12
  computeDriftScore,
13
13
  formatMarkdown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibgrate/cli",
3
- "version": "1.0.26",
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": {