@runsec/mcp 1.0.70 → 1.0.71

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/dist/index.js CHANGED
@@ -401,16 +401,38 @@ async function ignoreFinding(args) {
401
401
 
402
402
  // src/engine/ruleEngine.ts
403
403
  var import_node_fs8 = require("fs");
404
- var import_node_path13 = __toESM(require("path"));
404
+ var import_node_path14 = __toESM(require("path"));
405
405
  var import_ignore = __toESM(require("ignore"));
406
406
 
407
407
  // src/engine/unifiedScanPipeline.ts
408
- var import_node_path12 = __toESM(require("path"));
408
+ var import_node_path13 = __toESM(require("path"));
409
409
 
410
410
  // src/engine/cognitiveEngine.ts
411
411
  var import_node_crypto2 = require("crypto");
412
412
  var import_node_fs3 = __toESM(require("fs"));
413
413
  var import_node_path3 = __toESM(require("path"));
414
+
415
+ // src/engine/secretHeuristics.ts
416
+ var ENV_INTERP_RE = /\$\{[A-Z0-9_]+\}|\$[A-Z][A-Z0-9_]{2,}|%\([A-Za-z0-9_.]+\)s|process\.env\.|os\.getenv\(|getenv\(|environ\[/i;
417
+ var LOCKFILE_BASENAMES = /^(?:poetry\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|cargo\.lock|composer\.lock|gemfile\.lock)$/i;
418
+ function hasEnvironmentInterpolation(text) {
419
+ return ENV_INTERP_RE.test(text);
420
+ }
421
+ function isLockfileOrModulesPath(relPath) {
422
+ const normalized = relPath.replace(/\\/g, "/").toLowerCase();
423
+ if (!normalized || normalized === ".") return false;
424
+ if (normalized.includes("/node_modules/") || normalized.startsWith("node_modules/")) return true;
425
+ const base = normalized.split("/").pop() ?? normalized;
426
+ if (LOCKFILE_BASENAMES.test(base)) return true;
427
+ if (base.endsWith(".lock")) return true;
428
+ if (/-lock\.json$/i.test(base)) return true;
429
+ return false;
430
+ }
431
+ function isTrufflehogVerified(verifiedFlag, description) {
432
+ return verifiedFlag || /\(verified\)/i.test(description);
433
+ }
434
+
435
+ // src/engine/cognitiveEngine.ts
414
436
  var PRIMARY_LOG_THRESHOLD = 0.8;
415
437
  var CONFIDENCE_THRESHOLD = PRIMARY_LOG_THRESHOLD;
416
438
  var ELITE_LOW_CONFIDENCE_CAP = 0.28;
@@ -479,11 +501,20 @@ function looksLikeStaticLiteralMatch(finding) {
479
501
  if (snippet.length < 3) return false;
480
502
  return /^["'][^"']{0,200}["']\s*$/u.test(snippet);
481
503
  }
504
+ function isUnverifiedTrufflehogSecret(finding) {
505
+ return finding.category === "secrets" && !/\(verified\)/i.test(finding.description);
506
+ }
482
507
  function envOrConfigOnly(title, finding) {
483
508
  const t = title.toLowerCase();
484
509
  if (/\b(env var|environment|os\.getenv|process\.env|feature flag)\b/i.test(t)) return true;
485
- const blob = `${finding.description} ${finding.match_text}`.toLowerCase();
486
- return blob.includes("getenv") || blob.includes("process.env");
510
+ const blob = `${finding.description} ${finding.match_text} ${finding.snippet ?? ""}`;
511
+ if (hasEnvironmentInterpolation(blob)) return true;
512
+ const lowered = blob.toLowerCase();
513
+ return lowered.includes("getenv") || lowered.includes("process.env");
514
+ }
515
+ function precedentEnvironmentInterpolation(title, msg, snippet, description) {
516
+ if (/\(verified\)/i.test(description)) return false;
517
+ return hasEnvironmentInterpolation(`${title} ${msg} ${snippet}`);
487
518
  }
488
519
  function isSqlInjectionFinding(finding) {
489
520
  const blob = combinedTitleAndMessage(finding).toLowerCase();
@@ -728,6 +759,14 @@ function elitePrecedents(score, finding, relPath) {
728
759
  s = Math.min(s, 0.12);
729
760
  reasons.push("precedent:shell_no_untrusted_input_attack_path");
730
761
  }
762
+ if (precedentEnvironmentInterpolation(title, msg, snippet, finding.description)) {
763
+ s = Math.min(s, 0.15);
764
+ reasons.push("precedent:environment_interpolation");
765
+ }
766
+ if (finding.category === "secrets" && isUnverifiedTrufflehogSecret(finding) && isLockfileOrModulesPath(relPath)) {
767
+ s = Math.min(s, 0.1);
768
+ reasons.push("precedent:lockfile_or_modules_path");
769
+ }
731
770
  return [Math.max(0.05, s), reasons];
732
771
  }
733
772
  function comparativeAnalysisBoost(repoRoot, relPath, apply) {
@@ -795,6 +834,22 @@ function baseConfidenceForFinding(finding, phase1, relPath, category, repoRoot)
795
834
  } else if (category === "secrets") {
796
835
  if (sev === "CRITICAL") score = 0.95;
797
836
  else if (finding.match_text.includes("(verified)")) score = 0.93;
837
+ if (/pii\s+email/i.test(title) || finding.rule_id.includes("pii-email")) {
838
+ const piiVerified = /\(verified\)/i.test(finding.description);
839
+ score = Math.min(score, piiVerified ? 0.45 : 0.32);
840
+ reasons.push("pii_email_deprioritized");
841
+ }
842
+ if (isUnverifiedTrufflehogSecret(finding)) {
843
+ const secretBlob = `${finding.match_text} ${finding.snippet ?? ""}`;
844
+ if (hasEnvironmentInterpolation(secretBlob)) {
845
+ score = Math.min(score, 0.15);
846
+ reasons.push("environment_interpolation_placeholder");
847
+ }
848
+ if (isLockfileOrModulesPath(relPath)) {
849
+ score = Math.min(score, 0.1);
850
+ reasons.push("lockfile_or_modules_path");
851
+ }
852
+ }
798
853
  }
799
854
  const libs = phase1.protection_libs_detected ?? [];
800
855
  if (libs.length && protectionMatchesMetric(title, libs)) {
@@ -1618,6 +1673,9 @@ function trufflehogFileAndLine(raw) {
1618
1673
  }
1619
1674
  function severityForSecret(detectorName, verified) {
1620
1675
  const name = detectorName.toLowerCase();
1676
+ if (name.includes("pii email")) {
1677
+ return verified ? "LOW" : "INFO";
1678
+ }
1621
1679
  if (CRITICAL_SECRET_DETECTORS.test(name) || verified === true) {
1622
1680
  return "CRITICAL";
1623
1681
  }
@@ -1634,6 +1692,12 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
1634
1692
  const redacted = String(raw.Redacted ?? "").trim();
1635
1693
  const rawSecret = String(raw.Raw ?? "").trim();
1636
1694
  const display = redacted || rawSecret || "[secret redacted]";
1695
+ const description = `TruffleHog: exposed ${detector}${verified ? " (verified)" : ""}`;
1696
+ if (!isTrufflehogVerified(verified, description)) {
1697
+ if (isLockfileOrModulesPath(rel)) continue;
1698
+ const blob = `${display} ${rawSecret}`;
1699
+ if (hasEnvironmentInterpolation(blob)) continue;
1700
+ }
1637
1701
  const severity = severityForSecret(detector, verified);
1638
1702
  findings.push({
1639
1703
  category: "secrets",
@@ -1643,7 +1707,7 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
1643
1707
  asvsLevel: "L1",
1644
1708
  asvsTrace: "V6.4.1",
1645
1709
  severity,
1646
- description: `TruffleHog: exposed ${detector}${verified ? " (verified)" : ""}`,
1710
+ description,
1647
1711
  file_path: rel,
1648
1712
  line,
1649
1713
  match_text: display.slice(0, 200),
@@ -2321,7 +2385,38 @@ async function runSemgrepScan(opts) {
2321
2385
  // src/engine/supplyChainRunner.ts
2322
2386
  var import_node_child_process2 = require("child_process");
2323
2387
  var import_node_util2 = require("util");
2388
+ var import_node_path12 = __toESM(require("path"));
2389
+
2390
+ // src/engine/trufflehogExclude.ts
2391
+ var import_promises = __toESM(require("fs/promises"));
2392
+ var import_node_os = __toESM(require("os"));
2324
2393
  var import_node_path11 = __toESM(require("path"));
2394
+ var TRUFFLEHOG_EXCLUDE_PATTERNS = [
2395
+ "**/*.lock",
2396
+ "**/package-lock.json",
2397
+ "**/pnpm-lock.yaml",
2398
+ "**/yarn.lock",
2399
+ "**/poetry.lock",
2400
+ "**/Cargo.lock",
2401
+ "**/composer.lock",
2402
+ "**/Gemfile.lock",
2403
+ "**/*-lock.json",
2404
+ "**/node_modules/**"
2405
+ ];
2406
+ async function createTrufflehogExcludeFile() {
2407
+ const tmpDir = await import_promises.default.mkdtemp(import_node_path11.default.join(import_node_os.default.tmpdir(), "runsec-th-exclude-"));
2408
+ const excludeFilePath = import_node_path11.default.join(tmpDir, "exclude-paths.txt");
2409
+ await import_promises.default.writeFile(excludeFilePath, `${TRUFFLEHOG_EXCLUDE_PATTERNS.join("\n")}
2410
+ `, "utf-8");
2411
+ return {
2412
+ excludeFilePath,
2413
+ cleanup: async () => {
2414
+ await import_promises.default.rm(tmpDir, { recursive: true, force: true }).catch(() => void 0);
2415
+ }
2416
+ };
2417
+ }
2418
+
2419
+ // src/engine/supplyChainRunner.ts
2325
2420
  var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
2326
2421
  var MAX_BUFFER_BYTES2 = 64 * 1024 * 1024;
2327
2422
  var TRUFFLEHOG_DOCKER = "trufflesecurity/trufflehog:latest";
@@ -2416,12 +2511,12 @@ async function runExec(bin, argv, cwd, engineLabel) {
2416
2511
  }
2417
2512
  }
2418
2513
  function getTrufflehogConfigPath() {
2419
- return import_node_path11.default.join(getDataDirectory(), "trufflehog-config.yaml");
2514
+ return import_node_path12.default.join(getDataDirectory(), "trufflehog-config.yaml");
2420
2515
  }
2421
2516
  function resolveScanTarget(workspaceRoot, scanTargets) {
2422
- const root = import_node_path11.default.resolve(workspaceRoot);
2423
- const abs = scanTargets.length === 1 ? import_node_path11.default.resolve(scanTargets[0]) : root;
2424
- const relRaw = import_node_path11.default.relative(root, abs);
2517
+ const root = import_node_path12.default.resolve(workspaceRoot);
2518
+ const abs = scanTargets.length === 1 ? import_node_path12.default.resolve(scanTargets[0]) : root;
2519
+ const relRaw = import_node_path12.default.relative(root, abs);
2425
2520
  const rel = relRaw && !relRaw.startsWith("..") ? relRaw.replace(/\\/g, "/") : ".";
2426
2521
  return { abs, rel };
2427
2522
  }
@@ -2437,25 +2532,98 @@ function parseTrufflehogStdout(stdout) {
2437
2532
  }
2438
2533
  return out;
2439
2534
  }
2535
+ function trufflehogArgvWithExcludes(targetArg, configPath, excludeFilePath, dockerConfigPath) {
2536
+ const config = dockerConfigPath ?? configPath;
2537
+ return [
2538
+ "filesystem",
2539
+ targetArg,
2540
+ "--json",
2541
+ "--config",
2542
+ config,
2543
+ "--exclude-paths",
2544
+ excludeFilePath
2545
+ ];
2546
+ }
2440
2547
  async function runTrufflehogScan(opts) {
2441
- const root = import_node_path11.default.resolve(opts.workspaceRoot);
2548
+ const root = import_node_path12.default.resolve(opts.workspaceRoot);
2442
2549
  const { rel } = resolveScanTarget(root, opts.scanTargets);
2443
2550
  const configPath = getTrufflehogConfigPath();
2444
2551
  const targetArg = rel === "." ? "." : rel;
2445
- const localArgv = ["filesystem", targetArg, "--json", "--config", configPath];
2446
- const trufflehogBin = resolveEngineCommand("trufflehog");
2552
+ const excludeArtifacts = await createTrufflehogExcludeFile();
2447
2553
  try {
2448
- const { stdout, stderr, exitCode } = await runExec(trufflehogBin, localArgv, root, "trufflehog");
2449
- const findings = parseTrufflehogStdout(stdout);
2450
- if (exitCode !== 0 && findings.length === 0) {
2451
- const error = resolveSupplyChainErrorMessage("trufflehog", { stderr, exitCode });
2452
- logEngineExecFailure("trufflehog", trufflehogBin, exitCode, stderr, error);
2453
- logSupplyChainErrorStatus("trufflehog", { stderr, error, exitCode });
2454
- return { engine: "trufflehog", status: "error", findings: [], stderr: stderr.trim() || error, error };
2455
- }
2456
- return { engine: "trufflehog", status: "ok", findings, stderr };
2457
- } catch (error) {
2458
- if (!isENOENTError(error)) {
2554
+ const localArgv = trufflehogArgvWithExcludes(targetArg, configPath, excludeArtifacts.excludeFilePath);
2555
+ const trufflehogBin = resolveEngineCommand("trufflehog");
2556
+ try {
2557
+ const { stdout, stderr, exitCode } = await runExec(trufflehogBin, localArgv, root, "trufflehog");
2558
+ const findings = parseTrufflehogStdout(stdout);
2559
+ if (exitCode !== 0 && findings.length === 0) {
2560
+ const error = resolveSupplyChainErrorMessage("trufflehog", { stderr, exitCode });
2561
+ logEngineExecFailure("trufflehog", trufflehogBin, exitCode, stderr, error);
2562
+ logSupplyChainErrorStatus("trufflehog", { stderr, error, exitCode });
2563
+ return { engine: "trufflehog", status: "error", findings: [], stderr: stderr.trim() || error, error };
2564
+ }
2565
+ return { engine: "trufflehog", status: "ok", findings, stderr };
2566
+ } catch (error) {
2567
+ if (!isENOENTError(error)) {
2568
+ logScanCatchFailure("trufflehog", error);
2569
+ const message = error instanceof Error ? error.message : String(error);
2570
+ const captured = readExecError2(error);
2571
+ const resolved = resolveSupplyChainErrorMessage("trufflehog", {
2572
+ stderr: captured?.stderr,
2573
+ error: message,
2574
+ exitCode: captured?.exitCode
2575
+ });
2576
+ logSupplyChainErrorStatus("trufflehog", {
2577
+ stderr: captured?.stderr,
2578
+ error: resolved,
2579
+ exitCode: captured?.exitCode
2580
+ });
2581
+ return {
2582
+ engine: "trufflehog",
2583
+ status: "error",
2584
+ findings: [],
2585
+ stderr: captured?.stderr?.trim() || resolved,
2586
+ error: resolved
2587
+ };
2588
+ }
2589
+ }
2590
+ const dockerRel = rel === "." ? "/src" : `/src/${rel}`;
2591
+ const dataDir = getDataDirectory();
2592
+ const dockerExcludePath = "/runsec-exclude/exclude-paths.txt";
2593
+ const dockerArgv = [
2594
+ "run",
2595
+ "--rm",
2596
+ "-v",
2597
+ `${root}:/src`,
2598
+ "-v",
2599
+ `${dataDir}:/runsec-data:ro`,
2600
+ "-v",
2601
+ `${import_node_path12.default.dirname(excludeArtifacts.excludeFilePath)}:/runsec-exclude:ro`,
2602
+ TRUFFLEHOG_DOCKER,
2603
+ ...trufflehogArgvWithExcludes(
2604
+ dockerRel,
2605
+ configPath,
2606
+ dockerExcludePath,
2607
+ "/runsec-data/trufflehog-config.yaml"
2608
+ )
2609
+ ];
2610
+ try {
2611
+ const { stdout, stderr, exitCode } = await runExec("docker", dockerArgv, void 0, "docker-trufflehog");
2612
+ const findings = parseTrufflehogStdout(stdout);
2613
+ if (exitCode !== 0 && findings.length === 0) {
2614
+ const error = resolveSupplyChainErrorMessage("trufflehog", { stderr, error: stderr.trim(), exitCode });
2615
+ logEngineExecFailure("docker-trufflehog", "docker", exitCode, stderr, error);
2616
+ logSupplyChainErrorStatus("docker-trufflehog", { stderr, error, exitCode });
2617
+ return {
2618
+ engine: "docker-trufflehog",
2619
+ status: "error",
2620
+ findings: [],
2621
+ stderr: stderr.trim() || error,
2622
+ error
2623
+ };
2624
+ }
2625
+ return { engine: "docker-trufflehog", status: "ok", findings, stderr };
2626
+ } catch (error) {
2459
2627
  logScanCatchFailure("trufflehog", error);
2460
2628
  const message = error instanceof Error ? error.message : String(error);
2461
2629
  const captured = readExecError2(error);
@@ -2464,70 +2632,18 @@ async function runTrufflehogScan(opts) {
2464
2632
  error: message,
2465
2633
  exitCode: captured?.exitCode
2466
2634
  });
2467
- logSupplyChainErrorStatus("trufflehog", {
2468
- stderr: captured?.stderr,
2469
- error: resolved,
2470
- exitCode: captured?.exitCode
2471
- });
2635
+ const fullError = isENOENTError(error) ? `${resolved} ${engineSetupHint("trufflehog")}` : `trufflehog not available: ${resolved}. ${engineSetupHint("trufflehog")}`;
2636
+ logSupplyChainErrorStatus("trufflehog", { stderr: captured?.stderr, error: fullError, exitCode: captured?.exitCode });
2472
2637
  return {
2473
2638
  engine: "trufflehog",
2474
2639
  status: "error",
2475
2640
  findings: [],
2476
- stderr: captured?.stderr?.trim() || resolved,
2477
- error: resolved
2641
+ stderr: captured?.stderr?.trim() || fullError,
2642
+ error: fullError
2478
2643
  };
2479
2644
  }
2480
- }
2481
- const dockerRel = rel === "." ? "/src" : `/src/${rel}`;
2482
- const dataDir = getDataDirectory();
2483
- const dockerArgv = [
2484
- "run",
2485
- "--rm",
2486
- "-v",
2487
- `${root}:/src`,
2488
- "-v",
2489
- `${dataDir}:/runsec-data:ro`,
2490
- TRUFFLEHOG_DOCKER,
2491
- "filesystem",
2492
- dockerRel,
2493
- "--json",
2494
- "--config",
2495
- "/runsec-data/trufflehog-config.yaml"
2496
- ];
2497
- try {
2498
- const { stdout, stderr, exitCode } = await runExec("docker", dockerArgv, void 0, "docker-trufflehog");
2499
- const findings = parseTrufflehogStdout(stdout);
2500
- if (exitCode !== 0 && findings.length === 0) {
2501
- const error = resolveSupplyChainErrorMessage("trufflehog", { stderr, error: stderr.trim(), exitCode });
2502
- logEngineExecFailure("docker-trufflehog", "docker", exitCode, stderr, error);
2503
- logSupplyChainErrorStatus("docker-trufflehog", { stderr, error, exitCode });
2504
- return {
2505
- engine: "docker-trufflehog",
2506
- status: "error",
2507
- findings: [],
2508
- stderr: stderr.trim() || error,
2509
- error
2510
- };
2511
- }
2512
- return { engine: "docker-trufflehog", status: "ok", findings, stderr };
2513
- } catch (error) {
2514
- logScanCatchFailure("trufflehog", error);
2515
- const message = error instanceof Error ? error.message : String(error);
2516
- const captured = readExecError2(error);
2517
- const resolved = resolveSupplyChainErrorMessage("trufflehog", {
2518
- stderr: captured?.stderr,
2519
- error: message,
2520
- exitCode: captured?.exitCode
2521
- });
2522
- const fullError = isENOENTError(error) ? `${resolved} ${engineSetupHint("trufflehog")}` : `trufflehog not available: ${resolved}. ${engineSetupHint("trufflehog")}`;
2523
- logSupplyChainErrorStatus("trufflehog", { stderr: captured?.stderr, error: fullError, exitCode: captured?.exitCode });
2524
- return {
2525
- engine: "trufflehog",
2526
- status: "error",
2527
- findings: [],
2528
- stderr: captured?.stderr?.trim() || fullError,
2529
- error: fullError
2530
- };
2645
+ } finally {
2646
+ await excludeArtifacts.cleanup();
2531
2647
  }
2532
2648
  }
2533
2649
  function parseSyftStdout(stdout) {
@@ -2540,7 +2656,7 @@ function parseSyftStdout(stdout) {
2540
2656
  }
2541
2657
  }
2542
2658
  async function runSyftScan(opts) {
2543
- const root = import_node_path11.default.resolve(opts.workspaceRoot);
2659
+ const root = import_node_path12.default.resolve(opts.workspaceRoot);
2544
2660
  const { abs } = resolveScanTarget(root, opts.scanTargets);
2545
2661
  const scanPath = abs;
2546
2662
  const syftBin = resolveEngineCommand("syft");
@@ -2596,7 +2712,7 @@ async function runSyftScan(opts) {
2596
2712
  };
2597
2713
  }
2598
2714
  }
2599
- const rel = import_node_path11.default.relative(root, scanPath).replace(/\\/g, "/") || ".";
2715
+ const rel = import_node_path12.default.relative(root, scanPath).replace(/\\/g, "/") || ".";
2600
2716
  const dockerTarget = rel === "." ? "/src" : `/src/${rel}`;
2601
2717
  const dockerArgv = ["run", "--rm", "-v", `${root}:/src`, SYFT_DOCKER, dockerTarget, "-o", "json"];
2602
2718
  try {
@@ -2776,7 +2892,7 @@ function applyCognitiveFilter(workspaceRoot, findings) {
2776
2892
  async function runUnifiedScanPipeline(opts) {
2777
2893
  const startedAt = Date.now();
2778
2894
  const { standard, workspace_path, scan_targets, skipped_by_ignore } = opts;
2779
- const workspaceRoot = import_node_path12.default.resolve(workspace_path);
2895
+ const workspaceRoot = import_node_path13.default.resolve(workspace_path);
2780
2896
  const rules = getRulesRegistry()[standard];
2781
2897
  const fpIgnoreEntries = await loadIgnoreEntries(workspaceRoot);
2782
2898
  const { semgrepOutcome, trufflehogOutcome, syftOutcome, concurrent_duration_ms } = await runScanEnginesConcurrent(
@@ -2940,7 +3056,7 @@ async function buildIgnoreMatcher(workspaceRoot) {
2940
3056
  "**/coverage/**",
2941
3057
  "**/vendor/**"
2942
3058
  ]);
2943
- const runsecIgnorePath = import_node_path13.default.join(workspaceRoot, RUNSEC_IGNORE_FILE);
3059
+ const runsecIgnorePath = import_node_path14.default.join(workspaceRoot, RUNSEC_IGNORE_FILE);
2944
3060
  try {
2945
3061
  const content = await import_node_fs8.promises.readFile(runsecIgnorePath, "utf-8");
2946
3062
  matcher.add(content);
@@ -2949,7 +3065,7 @@ async function buildIgnoreMatcher(workspaceRoot) {
2949
3065
  return matcher;
2950
3066
  }
2951
3067
  async function collectFilesWithStats(workspacePath, targetFiles) {
2952
- const root = import_node_path13.default.resolve(workspacePath);
3068
+ const root = import_node_path14.default.resolve(workspacePath);
2953
3069
  const ignoreMatcher = await buildIgnoreMatcher(root);
2954
3070
  let skippedByIgnore = 0;
2955
3071
  let stat;
@@ -2964,8 +3080,8 @@ async function collectFilesWithStats(workspacePath, targetFiles) {
2964
3080
  if (targetFiles?.length) {
2965
3081
  const out = [];
2966
3082
  for (const f of targetFiles) {
2967
- const candidate = import_node_path13.default.resolve(root, f);
2968
- const relativeCandidate = normalizeRelativePath3(import_node_path13.default.relative(root, candidate));
3083
+ const candidate = import_node_path14.default.resolve(root, f);
3084
+ const relativeCandidate = normalizeRelativePath3(import_node_path14.default.relative(root, candidate));
2969
3085
  if (!relativeCandidate || ignoreMatcher.ignores(relativeCandidate) || shouldIgnoreByDefault(relativeCandidate)) {
2970
3086
  skippedByIgnore += 1;
2971
3087
  continue;
@@ -3005,7 +3121,7 @@ function buildAuditReportMetrics(result) {
3005
3121
  async function executeAudit(toolName, args) {
3006
3122
  const standard = STANDARD_TOOL_MAP[toolName];
3007
3123
  if (!standard) throw new Error(`Unknown audit tool: ${toolName}`);
3008
- const workspaceRoot = import_node_path13.default.resolve(args.workspace_path);
3124
+ const workspaceRoot = import_node_path14.default.resolve(args.workspace_path);
3009
3125
  const { files: scanTargets, skipped_by_ignore } = await collectFilesWithStats(workspaceRoot, args.target_files);
3010
3126
  const result = await runUnifiedScanPipeline({
3011
3127
  standard,
@@ -3019,7 +3135,7 @@ async function executeAudit(toolName, args) {
3019
3135
 
3020
3136
  // src/engine/reportFormatter.ts
3021
3137
  var import_node_fs9 = __toESM(require("fs"));
3022
- var import_node_path14 = __toESM(require("path"));
3138
+ var import_node_path15 = __toESM(require("path"));
3023
3139
  var REPORT_SECTION_TITLES = {
3024
3140
  code: "Code Vulnerabilities",
3025
3141
  secrets: "Exposed Secrets",
@@ -3112,7 +3228,7 @@ function shortRuleLabel(ruleId) {
3112
3228
  return parts[parts.length - 1] || ruleId;
3113
3229
  }
3114
3230
  function snippetLanguage(filePath) {
3115
- const ext = import_node_path14.default.extname(filePath).replace(/^\./, "").toLowerCase();
3231
+ const ext = import_node_path15.default.extname(filePath).replace(/^\./, "").toLowerCase();
3116
3232
  const map = {
3117
3233
  yml: "yaml",
3118
3234
  yaml: "yaml",
@@ -3214,9 +3330,9 @@ var TECH_STACK_BY_EXT = {
3214
3330
  };
3215
3331
  function techStackFromFilePath(filePath) {
3216
3332
  const normalized = String(filePath ?? "").replace(/\\/g, "/");
3217
- const base = import_node_path14.default.basename(normalized).toLowerCase();
3333
+ const base = import_node_path15.default.basename(normalized).toLowerCase();
3218
3334
  if (base === "dockerfile" || base.startsWith("dockerfile.")) return "Docker";
3219
- const ext = import_node_path14.default.extname(normalized).toLowerCase();
3335
+ const ext = import_node_path15.default.extname(normalized).toLowerCase();
3220
3336
  return TECH_STACK_BY_EXT[ext] ?? "Other";
3221
3337
  }
3222
3338
  function countFindingsByTechStack(findings) {
@@ -3478,8 +3594,8 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
3478
3594
  return finalizeReportMarkdown(out.join("\n"));
3479
3595
  }
3480
3596
  function resolveReportPath(workspacePath) {
3481
- const base = workspacePath?.trim() ? import_node_path14.default.resolve(workspacePath) : process.cwd();
3482
- return import_node_path14.default.join(base, "runsec-report.md");
3597
+ const base = workspacePath?.trim() ? import_node_path15.default.resolve(workspacePath) : process.cwd();
3598
+ return import_node_path15.default.join(base, "runsec-report.md");
3483
3599
  }
3484
3600
  function generateMarkdownReport(standard, findings, metrics, workspacePath) {
3485
3601
  const m = metrics || {};
@@ -3502,16 +3618,16 @@ Simply confirm that the scan is complete and the report is saved at the path abo
3502
3618
 
3503
3619
  // src/engine/reviewFormatter.ts
3504
3620
  var import_node_fs14 = __toESM(require("fs"));
3505
- var import_node_path20 = __toESM(require("path"));
3621
+ var import_node_path21 = __toESM(require("path"));
3506
3622
 
3507
3623
  // src/engine/threatModelEngine.ts
3508
3624
  var import_node_crypto4 = require("crypto");
3509
3625
  var import_node_fs13 = __toESM(require("fs"));
3510
- var import_node_path19 = __toESM(require("path"));
3626
+ var import_node_path20 = __toESM(require("path"));
3511
3627
 
3512
3628
  // src/skills/skillsApi.ts
3513
3629
  var import_node_fs12 = __toESM(require("fs"));
3514
- var import_node_path18 = __toESM(require("path"));
3630
+ var import_node_path19 = __toESM(require("path"));
3515
3631
 
3516
3632
  // src/skills/patternParser.ts
3517
3633
  var METRIC_ID_RE = /^[A-Z0-9]{2,4}-[0-9A-Za-z.\-]+$/;
@@ -3584,25 +3700,25 @@ function parsePatternRows(patternsText) {
3584
3700
  }
3585
3701
 
3586
3702
  // src/skills/paths.ts
3587
- var import_node_path15 = __toESM(require("path"));
3703
+ var import_node_path16 = __toESM(require("path"));
3588
3704
  var RUNSEC_RELEASE_VERSION = "v1.0";
3589
3705
  var RAG_CACHE_SCHEMA_VERSION = 3;
3590
3706
  var ANTI_HALLUCINATION_PROMPT = "You MUST re-run a RunSec audit tool (runsec_audit_general or a scoped runsec_audit_*) after every remediation. Security claims without a fresh scanner PASS are invalid.";
3591
3707
  function getSkillsDirectory() {
3592
- return import_node_path15.default.join(getDataDirectory(), "skills");
3708
+ return import_node_path16.default.join(getDataDirectory(), "skills");
3593
3709
  }
3594
3710
  function getRagCachePath() {
3595
- return import_node_path15.default.join(getDataDirectory(), ".rag-cache.json");
3711
+ return import_node_path16.default.join(getDataDirectory(), ".rag-cache.json");
3596
3712
  }
3597
3713
 
3598
3714
  // src/skills/ragIndex.ts
3599
3715
  var import_node_crypto3 = require("crypto");
3600
3716
  var import_node_fs11 = __toESM(require("fs"));
3601
- var import_node_path17 = __toESM(require("path"));
3717
+ var import_node_path18 = __toESM(require("path"));
3602
3718
 
3603
3719
  // src/skills/skillLoader.ts
3604
3720
  var import_node_fs10 = __toESM(require("fs"));
3605
- var import_node_path16 = __toESM(require("path"));
3721
+ var import_node_path17 = __toESM(require("path"));
3606
3722
  function loadSkillManifests() {
3607
3723
  const skillsDir = getSkillsDirectory();
3608
3724
  const manifests = {};
@@ -3611,7 +3727,7 @@ function loadSkillManifests() {
3611
3727
  }
3612
3728
  for (const entry of import_node_fs10.default.readdirSync(skillsDir, { withFileTypes: true })) {
3613
3729
  if (!entry.isDirectory()) continue;
3614
- const skillJson = import_node_path16.default.join(skillsDir, entry.name, "skill.json");
3730
+ const skillJson = import_node_path17.default.join(skillsDir, entry.name, "skill.json");
3615
3731
  if (!import_node_fs10.default.existsSync(skillJson)) continue;
3616
3732
  const data = JSON.parse(import_node_fs10.default.readFileSync(skillJson, "utf-8"));
3617
3733
  const sid = String(data.skill_id ?? entry.name);
@@ -3620,7 +3736,7 @@ function loadSkillManifests() {
3620
3736
  return manifests;
3621
3737
  }
3622
3738
  function skillDirectory(manifest) {
3623
- return import_node_path16.default.join(getSkillsDirectory(), manifest.__dir_name);
3739
+ return import_node_path17.default.join(getSkillsDirectory(), manifest.__dir_name);
3624
3740
  }
3625
3741
 
3626
3742
  // src/skills/ragIndex.ts
@@ -3674,7 +3790,7 @@ function listSkillSourceFiles() {
3674
3790
  const files = [];
3675
3791
  const walk = (dir) => {
3676
3792
  for (const entry of import_node_fs11.default.readdirSync(dir, { withFileTypes: true })) {
3677
- const full = import_node_path17.default.join(dir, entry.name);
3793
+ const full = import_node_path18.default.join(dir, entry.name);
3678
3794
  if (entry.isDirectory()) walk(full);
3679
3795
  else if (entry.isFile()) files.push(full);
3680
3796
  }
@@ -3687,7 +3803,7 @@ function computeFilesChecksum() {
3687
3803
  const entries = [];
3688
3804
  for (const full of files) {
3689
3805
  const st = import_node_fs11.default.statSync(full);
3690
- const rel = import_node_path17.default.relative(getSkillsDirectory(), full).replace(/\\/g, "/");
3806
+ const rel = import_node_path18.default.relative(getSkillsDirectory(), full).replace(/\\/g, "/");
3691
3807
  const mtimeNs = typeof st.mtimeNs === "bigint" ? Number(st.mtimeNs) : Math.round(st.mtimeMs * 1e6);
3692
3808
  entries.push({
3693
3809
  path: rel,
@@ -3718,8 +3834,8 @@ function buildRagIndex() {
3718
3834
  const manifests = loadSkillManifests();
3719
3835
  for (const [skill_id, manifest] of Object.entries(manifests)) {
3720
3836
  const dir = skillDirectory(manifest);
3721
- const indexPath = import_node_path17.default.join(dir, "index.md");
3722
- const patternsPath = import_node_path17.default.join(dir, "patterns.md");
3837
+ const indexPath = import_node_path18.default.join(dir, "index.md");
3838
+ const patternsPath = import_node_path18.default.join(dir, "patterns.md");
3723
3839
  if (!import_node_fs11.default.existsSync(indexPath) || !import_node_fs11.default.existsSync(patternsPath)) continue;
3724
3840
  const indexText = import_node_fs11.default.readFileSync(indexPath, "utf-8");
3725
3841
  const patternsText = import_node_fs11.default.readFileSync(patternsPath, "utf-8");
@@ -3944,7 +4060,7 @@ function selectSkillsForContext(opts) {
3944
4060
  const query = [opts.question ?? "", opts.file_path ?? "", opts.file_content ?? ""].filter(Boolean).join("\n");
3945
4061
  const semScores = query ? semanticSkillScores(query) : {};
3946
4062
  const keywordBoosts = query ? skillBoosts(query) : {};
3947
- const suffix = import_node_path17.default.extname(opts.file_path ?? "").toLowerCase();
4063
+ const suffix = import_node_path18.default.extname(opts.file_path ?? "").toLowerCase();
3948
4064
  const hay = query.toLowerCase();
3949
4065
  const ranked = [];
3950
4066
  for (const [sid, manifest] of Object.entries(manifests)) {
@@ -3981,7 +4097,7 @@ function findPatternChunkByMetric(metricId) {
3981
4097
 
3982
4098
  // src/skills/skillsApi.ts
3983
4099
  function loadComplianceSnapshot() {
3984
- const p = import_node_path18.default.join(getDataDirectory(), "rule-compliance-map.json");
4100
+ const p = import_node_path19.default.join(getDataDirectory(), "rule-compliance-map.json");
3985
4101
  if (!import_node_fs12.default.existsSync(p)) return {};
3986
4102
  try {
3987
4103
  return JSON.parse(import_node_fs12.default.readFileSync(p, "utf-8"));
@@ -3991,8 +4107,8 @@ function loadComplianceSnapshot() {
3991
4107
  }
3992
4108
  function extractTestbedExample(metricId, examplePath, skillsRoot) {
3993
4109
  if (!examplePath) return "";
3994
- const p = import_node_path18.default.isAbsolute(examplePath) ? examplePath : import_node_path18.default.join(import_node_path18.default.dirname(skillsRoot), "..", "..", examplePath);
3995
- const resolved = import_node_fs12.default.existsSync(p) ? p : import_node_path18.default.join(getDataDirectory(), "..", "..", examplePath);
4110
+ const p = import_node_path19.default.isAbsolute(examplePath) ? examplePath : import_node_path19.default.join(import_node_path19.default.dirname(skillsRoot), "..", "..", examplePath);
4111
+ const resolved = import_node_fs12.default.existsSync(p) ? p : import_node_path19.default.join(getDataDirectory(), "..", "..", examplePath);
3996
4112
  if (!import_node_fs12.default.existsSync(resolved)) return "";
3997
4113
  const lines = import_node_fs12.default.readFileSync(resolved, "utf-8").split(/\r?\n/);
3998
4114
  const needle = `Vulnerable: ${metricId}`;
@@ -4046,8 +4162,8 @@ function getSkillContext(args) {
4046
4162
  }
4047
4163
  const manifest = manifests[skill_id];
4048
4164
  const dir = skillDirectory(manifest);
4049
- const indexPath = import_node_path18.default.join(dir, "index.md");
4050
- const patternsPath = import_node_path18.default.join(dir, "patterns.md");
4165
+ const indexPath = import_node_path19.default.join(dir, "index.md");
4166
+ const patternsPath = import_node_path19.default.join(dir, "patterns.md");
4051
4167
  if (!import_node_fs12.default.existsSync(indexPath) || !import_node_fs12.default.existsSync(patternsPath)) {
4052
4168
  return {
4053
4169
  error: `incomplete skill data for ${skill_id}`,
@@ -4068,11 +4184,11 @@ function getSkillContext(args) {
4068
4184
  source: row.source
4069
4185
  });
4070
4186
  }
4071
- const skillsRoot = import_node_path18.default.dirname(dir);
4187
+ const skillsRoot = import_node_path19.default.dirname(dir);
4072
4188
  const response = {
4073
4189
  skill_id,
4074
- index_path: import_node_path18.default.relative(getDataDirectory(), indexPath).replace(/\\/g, "/"),
4075
- patterns_path: import_node_path18.default.relative(getDataDirectory(), patternsPath).replace(/\\/g, "/"),
4190
+ index_path: import_node_path19.default.relative(getDataDirectory(), indexPath).replace(/\\/g, "/"),
4191
+ patterns_path: import_node_path19.default.relative(getDataDirectory(), patternsPath).replace(/\\/g, "/"),
4076
4192
  index_md: import_node_fs12.default.readFileSync(indexPath, "utf-8"),
4077
4193
  patterns_md: import_node_fs12.default.readFileSync(patternsPath, "utf-8"),
4078
4194
  patterns_by_stack: Object.fromEntries(Object.keys(grouped).sort().map((k) => [k, grouped[k]])),
@@ -4114,7 +4230,7 @@ function askGuidance(question) {
4114
4230
  if (!q) return { error: "question is required" };
4115
4231
  const best = semanticSearch(q, 50, "pattern", true);
4116
4232
  const manifests = loadSkillManifests();
4117
- const skillsRoot = import_node_path18.default.join(getDataDirectory(), "skills");
4233
+ const skillsRoot = import_node_path19.default.join(getDataDirectory(), "skills");
4118
4234
  const required = requiredMetricIds(q);
4119
4235
  const out = [];
4120
4236
  const seen = /* @__PURE__ */ new Set();
@@ -4220,10 +4336,10 @@ function walkFiles2(root, opts) {
4220
4336
  const entries = import_node_fs13.default.readdirSync(dir, { withFileTypes: true });
4221
4337
  for (const ent of entries) {
4222
4338
  if (out.length >= max) break;
4223
- const full = import_node_path19.default.join(dir, ent.name);
4339
+ const full = import_node_path20.default.join(dir, ent.name);
4224
4340
  if (ent.isDirectory()) {
4225
4341
  if (!skip.has(ent.name)) stack.push(full);
4226
- } else if (ent.isFile() && exts.has(import_node_path19.default.extname(ent.name).toLowerCase())) {
4342
+ } else if (ent.isFile() && exts.has(import_node_path20.default.extname(ent.name).toLowerCase())) {
4227
4343
  out.push(full);
4228
4344
  }
4229
4345
  }
@@ -4264,7 +4380,7 @@ function syftPackages(payload) {
4264
4380
  return out.sort((a, b) => a.name.localeCompare(b.name));
4265
4381
  }
4266
4382
  function collectRepoThreatSignals(scanRoot, syftPayload) {
4267
- const root = import_node_path19.default.resolve(scanRoot);
4383
+ const root = import_node_path20.default.resolve(scanRoot);
4268
4384
  const scanRel = ".";
4269
4385
  const signals = {
4270
4386
  scan_root: scanRel,
@@ -4310,11 +4426,11 @@ function collectRepoThreatSignals(scanRoot, syftPayload) {
4310
4426
  continue;
4311
4427
  }
4312
4428
  for (const ent of entries) {
4313
- const full = import_node_path19.default.join(dir, ent.name);
4429
+ const full = import_node_path20.default.join(dir, ent.name);
4314
4430
  if (ent.isDirectory()) {
4315
4431
  if (!skip.has(ent.name) && !ent.name.startsWith(".")) stack.push(full);
4316
4432
  } else if (ent.isFile() && patternNames.includes(ent.name)) {
4317
- const rel = import_node_path19.default.relative(root, full).replace(/\\/g, "/");
4433
+ const rel = import_node_path20.default.relative(root, full).replace(/\\/g, "/");
4318
4434
  if (!rel.startsWith("..")) handler(full, rel);
4319
4435
  }
4320
4436
  }
@@ -4326,15 +4442,15 @@ function collectRepoThreatSignals(scanRoot, syftPayload) {
4326
4442
  });
4327
4443
  globWalk(["requirements.txt", "pyproject.toml", "go.mod"], (full, rel) => {
4328
4444
  addKey(rel);
4329
- if (import_node_path19.default.basename(full) === "requirements.txt") {
4445
+ if (import_node_path20.default.basename(full) === "requirements.txt") {
4330
4446
  for (const d of parseRequirementsDeps(full)) deps.add(d);
4331
- } else if (import_node_path19.default.basename(full) === "pyproject.toml") {
4447
+ } else if (import_node_path20.default.basename(full) === "pyproject.toml") {
4332
4448
  const txt = readTextSafe(full, 8e4).toLowerCase();
4333
4449
  for (const m of txt.matchAll(/['"]([a-zA-Z0-9_.\-]+)['"]/g)) deps.add(m[1].toLowerCase());
4334
4450
  }
4335
4451
  });
4336
4452
  for (const name of ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"]) {
4337
- const fp = import_node_path19.default.join(root, name);
4453
+ const fp = import_node_path20.default.join(root, name);
4338
4454
  if (import_node_fs13.default.existsSync(fp)) {
4339
4455
  addKey(name);
4340
4456
  if (name === "Dockerfile") signals.flags.has_dockerfile = true;
@@ -4342,14 +4458,14 @@ function collectRepoThreatSignals(scanRoot, syftPayload) {
4342
4458
  }
4343
4459
  }
4344
4460
  for (const full of walkFiles2(root, { maxFiles: 80, extensions: /* @__PURE__ */ new Set([".yaml", ".yml"]) })) {
4345
- const base = import_node_path19.default.basename(full).toLowerCase();
4461
+ const base = import_node_path20.default.basename(full).toLowerCase();
4346
4462
  if (base.includes("deploy") || base.includes("helm") || base.includes("k8s") || base.includes("values")) {
4347
4463
  signals.flags.has_k8s_yaml = true;
4348
- addKey(import_node_path19.default.relative(root, full).replace(/\\/g, "/"));
4464
+ addKey(import_node_path20.default.relative(root, full).replace(/\\/g, "/"));
4349
4465
  break;
4350
4466
  }
4351
4467
  }
4352
- if (import_node_fs13.default.existsSync(import_node_path19.default.join(root, ".github", "workflows"))) {
4468
+ if (import_node_fs13.default.existsSync(import_node_path20.default.join(root, ".github", "workflows"))) {
4353
4469
  signals.flags.has_github_workflows = true;
4354
4470
  }
4355
4471
  for (const pkg of signals.syft_packages) {
@@ -4381,7 +4497,7 @@ ${context}
4381
4497
  ${repoFp}`).digest("hex");
4382
4498
  }
4383
4499
  function loadThreatModelCache(workspaceRoot) {
4384
- const p = import_node_path19.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4500
+ const p = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4385
4501
  if (!import_node_fs13.default.existsSync(p)) {
4386
4502
  return { schema_version: THREAT_MODEL_CACHE_SCHEMA_VERSION, entries: {} };
4387
4503
  }
@@ -4397,7 +4513,7 @@ function loadThreatModelCache(workspaceRoot) {
4397
4513
  }
4398
4514
  }
4399
4515
  function saveThreatModelCache(workspaceRoot, cacheKey, markdown, baseline) {
4400
- const p = import_node_path19.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4516
+ const p = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4401
4517
  const payload = loadThreatModelCache(workspaceRoot);
4402
4518
  payload.entries[cacheKey] = {
4403
4519
  markdown,
@@ -4610,7 +4726,7 @@ function loadRepoCrosscheckHaystack(scanRoot, maxChars = 35e4) {
4610
4726
  let total = 0;
4611
4727
  const files = walkFiles2(scanRoot, { maxFiles: 220 });
4612
4728
  for (const full of files) {
4613
- const rel = import_node_path19.default.relative(scanRoot, full).replace(/\\/g, "/");
4729
+ const rel = import_node_path20.default.relative(scanRoot, full).replace(/\\/g, "/");
4614
4730
  const snippet = readTextSafe(full, 14e3);
4615
4731
  const block = `
4616
4732
  --- ${rel} ---
@@ -4820,8 +4936,8 @@ function generateThreatModelMarkdown(profile, baseline, signals, ragKeywords, ca
4820
4936
  `;
4821
4937
  }
4822
4938
  async function runThreatModelEngine(opts) {
4823
- const workspaceRoot = import_node_path19.default.resolve(opts.workspace_path);
4824
- const projectName = (opts.project_name ?? import_node_path19.default.basename(workspaceRoot)).trim() || "project";
4939
+ const workspaceRoot = import_node_path20.default.resolve(opts.workspace_path);
4940
+ const projectName = (opts.project_name ?? import_node_path20.default.basename(workspaceRoot)).trim() || "project";
4825
4941
  const userContext = (opts.context ?? "").trim();
4826
4942
  const profile = detectSecurityProfile(projectName, userContext);
4827
4943
  const baseline = ARCHITECTURE_BASELINES[profile];
@@ -4836,13 +4952,13 @@ Project: ${projectName}`;
4836
4952
  const cache = loadThreatModelCache(workspaceRoot);
4837
4953
  const cached = cache.entries[cacheKey];
4838
4954
  if (cached?.markdown) {
4839
- const reportPath2 = import_node_path19.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
4955
+ const reportPath2 = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
4840
4956
  import_node_fs13.default.writeFileSync(reportPath2, cached.markdown, "utf-8");
4841
4957
  return {
4842
4958
  profile,
4843
4959
  markdown: cached.markdown,
4844
4960
  report_path: reportPath2,
4845
- cache_path: import_node_path19.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE),
4961
+ cache_path: import_node_path20.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE),
4846
4962
  cache_hit: true,
4847
4963
  cache_key: cacheKey,
4848
4964
  repo_fingerprint: repoFp,
@@ -4874,7 +4990,7 @@ Project: ${projectName}`;
4874
4990
  rag_keywords: [...ragKeywords].sort().slice(0, 40)
4875
4991
  };
4876
4992
  const cachePath = saveThreatModelCache(workspaceRoot, cacheKey, markdown, baselinePayload);
4877
- const reportPath = import_node_path19.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
4993
+ const reportPath = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
4878
4994
  import_node_fs13.default.writeFileSync(reportPath, markdown, "utf-8");
4879
4995
  return {
4880
4996
  profile,
@@ -5090,8 +5206,8 @@ function buildSecurityReviewMarkdown(opts) {
5090
5206
  return out.join("\n");
5091
5207
  }
5092
5208
  function resolveSecurityReviewPath(workspacePath) {
5093
- const base = workspacePath?.trim() ? import_node_path20.default.resolve(workspacePath) : process.cwd();
5094
- return import_node_path20.default.join(base, SECURITY_REVIEW_REPORT_FILE);
5209
+ const base = workspacePath?.trim() ? import_node_path21.default.resolve(workspacePath) : process.cwd();
5210
+ return import_node_path21.default.join(base, SECURITY_REVIEW_REPORT_FILE);
5095
5211
  }
5096
5212
  function writeSecurityReviewReport(markdown, workspacePath) {
5097
5213
  const reportPath = resolveSecurityReviewPath(workspacePath);
@@ -5100,8 +5216,8 @@ function writeSecurityReviewReport(markdown, workspacePath) {
5100
5216
  return reportPath;
5101
5217
  }
5102
5218
  async function executeSecurityReview(opts) {
5103
- const workspaceRoot = import_node_path20.default.resolve(opts.workspace_path);
5104
- const projectName = (opts.project_name ?? import_node_path20.default.basename(workspaceRoot)).trim() || import_node_path20.default.basename(workspaceRoot);
5219
+ const workspaceRoot = import_node_path21.default.resolve(opts.workspace_path);
5220
+ const projectName = (opts.project_name ?? import_node_path21.default.basename(workspaceRoot)).trim() || import_node_path21.default.basename(workspaceRoot);
5105
5221
  console.error("[runsec] security review: starting unified scan + STRIDE threat model");
5106
5222
  const [audit, threatModel] = await Promise.all([
5107
5223
  executeAudit("runsec_audit_general", {
@@ -5135,17 +5251,17 @@ async function executeSecurityReview(opts) {
5135
5251
 
5136
5252
  // src/engine/remediation.ts
5137
5253
  var import_node_fs16 = __toESM(require("fs"));
5138
- var import_node_path22 = __toESM(require("path"));
5254
+ var import_node_path23 = __toESM(require("path"));
5139
5255
 
5140
5256
  // src/skills/metricLookup.ts
5141
5257
  var import_node_fs15 = __toESM(require("fs"));
5142
- var import_node_path21 = __toESM(require("path"));
5258
+ var import_node_path22 = __toESM(require("path"));
5143
5259
  function findMetricRow(metricId) {
5144
5260
  const mid = metricId.trim().toUpperCase();
5145
5261
  if (!mid) return null;
5146
5262
  const manifests = loadSkillManifests();
5147
5263
  for (const [, manifest] of Object.entries(manifests)) {
5148
- const patternsPath = import_node_path21.default.join(skillDirectory(manifest), "patterns.md");
5264
+ const patternsPath = import_node_path22.default.join(skillDirectory(manifest), "patterns.md");
5149
5265
  if (!import_node_fs15.default.existsSync(patternsPath)) continue;
5150
5266
  const rows = parsePatternRows(import_node_fs15.default.readFileSync(patternsPath, "utf-8"));
5151
5267
  const row = rows.find((r) => r.metric_id.toUpperCase() === mid);
@@ -5160,12 +5276,12 @@ function normalizeRelPath2(p) {
5160
5276
  return p.replace(/\\/g, "/").replace(/^\.\/+/, "");
5161
5277
  }
5162
5278
  function resolveTargetFile(workspaceRoot, filePath) {
5163
- const root = import_node_path22.default.resolve(workspaceRoot);
5279
+ const root = import_node_path23.default.resolve(workspaceRoot);
5164
5280
  const rel = normalizeRelPath2(filePath.trim());
5165
5281
  if (!rel) return { error: "file_path is required" };
5166
- const abs = import_node_path22.default.resolve(root, rel);
5167
- const relFromRoot = import_node_path22.default.relative(root, abs).replace(/\\/g, "/");
5168
- if (relFromRoot.startsWith("..") || import_node_path22.default.isAbsolute(relFromRoot)) {
5282
+ const abs = import_node_path23.default.resolve(root, rel);
5283
+ const relFromRoot = import_node_path23.default.relative(root, abs).replace(/\\/g, "/");
5284
+ if (relFromRoot.startsWith("..") || import_node_path23.default.isAbsolute(relFromRoot)) {
5169
5285
  return { error: `file_path must be inside workspace: ${root}` };
5170
5286
  }
5171
5287
  for (const seg of BLOCKED_PATH_SEGMENTS) {
@@ -5245,7 +5361,7 @@ function writeBackup(absPath) {
5245
5361
  return backupPath;
5246
5362
  }
5247
5363
  function applyDvs001Fix(absPath) {
5248
- const rel = import_node_path22.default.basename(absPath);
5364
+ const rel = import_node_path23.default.basename(absPath);
5249
5365
  const lines = import_node_fs16.default.readFileSync(absPath, "utf-8").split(/\r?\n/);
5250
5366
  const newLines = lines.filter((ln) => !/^\s*user\s+root\s*$/i.test(ln.trim()));
5251
5367
  const hasNonRootUser = newLines.some(
@@ -5577,6 +5693,47 @@ function isRemediationTool(name) {
5577
5693
  return REMEDIATION_TOOL_NAMES.includes(name);
5578
5694
  }
5579
5695
 
5696
+ // src/complianceScores.ts
5697
+ var SEVERITY_PENALTY = {
5698
+ CRITICAL: 15,
5699
+ HIGH: 10,
5700
+ MEDIUM: 5,
5701
+ LOW: 2,
5702
+ INFO: 1
5703
+ };
5704
+ var STANDARD_FRAMEWORK_KEY = {
5705
+ "PCI-DSS": "pciDss",
5706
+ SOC2: "soc2",
5707
+ HIPAA: "hipaa"
5708
+ };
5709
+ function compliancePctFromFindings(findings, scannedFiles) {
5710
+ if (findings.length === 0) return 100;
5711
+ let penalty = 0;
5712
+ for (const f of findings) {
5713
+ const sev = String(f.severity ?? "MEDIUM").toUpperCase();
5714
+ penalty += SEVERITY_PENALTY[sev] ?? 5;
5715
+ }
5716
+ const files = Math.max(1, scannedFiles);
5717
+ const densityPenalty = findings.length / files * 8;
5718
+ const raw = 100 - penalty - densityPenalty;
5719
+ return Math.max(0, Math.min(100, Math.round(raw * 10) / 10));
5720
+ }
5721
+ function buildHubComplianceBlock(result) {
5722
+ const scanned = result.scanned_files_count > 0 ? result.scanned_files_count : result.files_scanned;
5723
+ const asvs = compliancePctFromFindings(result.findings, scanned);
5724
+ const compliance = {
5725
+ asvs,
5726
+ pciDss: null,
5727
+ soc2: null,
5728
+ hipaa: null
5729
+ };
5730
+ const frameworkKey = STANDARD_FRAMEWORK_KEY[result.standard];
5731
+ if (frameworkKey) {
5732
+ compliance[frameworkKey] = asvs;
5733
+ }
5734
+ return { compliance, complianceASVS: asvs };
5735
+ }
5736
+
5580
5737
  // src/telemetryClient.ts
5581
5738
  function countSeverityMetrics(findings) {
5582
5739
  const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
@@ -5600,12 +5757,15 @@ function resolveScanVerdict(result) {
5600
5757
  function buildHubUploadPayload(result, reportMetrics, workspacePath, projectId) {
5601
5758
  const metrics = countSeverityMetrics(result.findings);
5602
5759
  const verdict = resolveScanVerdict(result);
5760
+ const { compliance, complianceASVS } = buildHubComplianceBlock(result);
5603
5761
  const runsecJson = {
5604
5762
  source: "runsec_mcp",
5605
5763
  standard: result.standard,
5606
5764
  workspace_path: workspacePath,
5607
5765
  verdict,
5608
5766
  metrics,
5767
+ compliance,
5768
+ complianceASVS,
5609
5769
  audit: result,
5610
5770
  report_metrics: reportMetrics,
5611
5771
  findings: result.findings,