@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/data/trufflehog-config.yaml +275 -7
- package/dist/index.js +310 -150
- package/package.json +1 -1
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
|
|
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
|
|
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}
|
|
486
|
-
|
|
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
|
|
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
|
|
2514
|
+
return import_node_path12.default.join(getDataDirectory(), "trufflehog-config.yaml");
|
|
2420
2515
|
}
|
|
2421
2516
|
function resolveScanTarget(workspaceRoot, scanTargets) {
|
|
2422
|
-
const root =
|
|
2423
|
-
const abs = scanTargets.length === 1 ?
|
|
2424
|
-
const relRaw =
|
|
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 =
|
|
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
|
|
2446
|
-
const trufflehogBin = resolveEngineCommand("trufflehog");
|
|
2552
|
+
const excludeArtifacts = await createTrufflehogExcludeFile();
|
|
2447
2553
|
try {
|
|
2448
|
-
const
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
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
|
-
|
|
2468
|
-
|
|
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() ||
|
|
2477
|
-
error:
|
|
2641
|
+
stderr: captured?.stderr?.trim() || fullError,
|
|
2642
|
+
error: fullError
|
|
2478
2643
|
};
|
|
2479
2644
|
}
|
|
2480
|
-
}
|
|
2481
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2968
|
-
const relativeCandidate = normalizeRelativePath3(
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
3333
|
+
const base = import_node_path15.default.basename(normalized).toLowerCase();
|
|
3218
3334
|
if (base === "dockerfile" || base.startsWith("dockerfile.")) return "Docker";
|
|
3219
|
-
const ext =
|
|
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() ?
|
|
3482
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3708
|
+
return import_node_path16.default.join(getDataDirectory(), "skills");
|
|
3593
3709
|
}
|
|
3594
3710
|
function getRagCachePath() {
|
|
3595
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3722
|
-
const patternsPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3995
|
-
const resolved = import_node_fs12.default.existsSync(p) ? p :
|
|
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 =
|
|
4050
|
-
const patternsPath =
|
|
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 =
|
|
4187
|
+
const skillsRoot = import_node_path19.default.dirname(dir);
|
|
4072
4188
|
const response = {
|
|
4073
4189
|
skill_id,
|
|
4074
|
-
index_path:
|
|
4075
|
-
patterns_path:
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
4445
|
+
if (import_node_path20.default.basename(full) === "requirements.txt") {
|
|
4330
4446
|
for (const d of parseRequirementsDeps(full)) deps.add(d);
|
|
4331
|
-
} else if (
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4824
|
-
const projectName = (opts.project_name ??
|
|
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 =
|
|
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:
|
|
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 =
|
|
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() ?
|
|
5094
|
-
return
|
|
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 =
|
|
5104
|
-
const projectName = (opts.project_name ??
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5167
|
-
const relFromRoot =
|
|
5168
|
-
if (relFromRoot.startsWith("..") ||
|
|
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 =
|
|
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,
|