@runsec/mcp 1.0.87 → 1.0.89
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 +501 -221
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -623,6 +623,49 @@ function isTestOnlyPath(relPath) {
|
|
|
623
623
|
if (base.includes("conftest.py") || base === "pytest.ini" || base.includes("jest.config")) return true;
|
|
624
624
|
return false;
|
|
625
625
|
}
|
|
626
|
+
function isDevOrLocalConfigPath(relPath) {
|
|
627
|
+
const r = relPath.toLowerCase().replace(/\\/g, "/");
|
|
628
|
+
const base = import_node_path3.default.posix.basename(r);
|
|
629
|
+
if (isTestOnlyPath(relPath)) return true;
|
|
630
|
+
if (/-dev\b|\.dev\.|\/dev\/|docker-compose-dev|compose\.dev|local\.|\.local\.|\.env\.example|\.env\.sample|\.env\.template/.test(r)) {
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
if (base.startsWith("test_") || base.includes(".test.") || base.includes(".spec.")) return true;
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
function isProductionCoreConfigPath(relPath) {
|
|
637
|
+
const r = relPath.toLowerCase().replace(/\\/g, "/");
|
|
638
|
+
const base = import_node_path3.default.posix.basename(r);
|
|
639
|
+
if (isDevOrLocalConfigPath(relPath)) return false;
|
|
640
|
+
if (base === "settings.py" || base === "production.py" || base === "prod.py") return true;
|
|
641
|
+
if (/\/settings\/production\.py$/.test(r) || /\/config\/production\./.test(r)) return true;
|
|
642
|
+
if (base.includes("production") && !base.includes("non-production")) return true;
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
var ALLOWED_HOSTS_WILDCARD_RE = /allowed_hosts\s*=\s*\[\s*['"]\*['"]\s*\]|allowed_hosts\s*=\s*\{\s*['"]\*['"]\s*\}/i;
|
|
646
|
+
function isHighImpactProductionMisconfig(finding, relPath) {
|
|
647
|
+
if (finding.category !== "code") return false;
|
|
648
|
+
if (!isProductionCoreConfigPath(relPath)) return false;
|
|
649
|
+
const blob = `${finding.snippet ?? ""} ${finding.match_text ?? ""} ${finding.description ?? ""}`;
|
|
650
|
+
return ALLOWED_HOSTS_WILDCARD_RE.test(blob);
|
|
651
|
+
}
|
|
652
|
+
function computeDynamicCodeConfidence(finding, relPath, sev) {
|
|
653
|
+
const reasons = [];
|
|
654
|
+
let score;
|
|
655
|
+
if (sev === "CRITICAL" || sev === "ERROR") score = 0.9;
|
|
656
|
+
else if (sev === "WARNING" || sev === "HIGH") score = 0.78;
|
|
657
|
+
else if (sev === "MEDIUM") score = 0.62;
|
|
658
|
+
else score = 0.48;
|
|
659
|
+
if (isHighImpactProductionMisconfig(finding, relPath)) {
|
|
660
|
+
score = Math.max(score, 0.94);
|
|
661
|
+
reasons.push("production_core_misconfig_elevated");
|
|
662
|
+
}
|
|
663
|
+
if (isDevOrLocalConfigPath(relPath)) {
|
|
664
|
+
score = Math.min(score, 0.1);
|
|
665
|
+
reasons.push("dev_or_local_config_clamped");
|
|
666
|
+
}
|
|
667
|
+
return { score, reasons };
|
|
668
|
+
}
|
|
626
669
|
function combinedTitleAndMessage(finding) {
|
|
627
670
|
return `${finding.description} ${finding.match_text}`.trim();
|
|
628
671
|
}
|
|
@@ -965,9 +1008,9 @@ function baseConfidenceForFinding(finding, phase1, relPath, category, repoRoot)
|
|
|
965
1008
|
const title = finding.title ?? finding.description;
|
|
966
1009
|
const sev = (finding.severity || "").toUpperCase();
|
|
967
1010
|
if (category === "code") {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1011
|
+
const dynamic = computeDynamicCodeConfidence(finding, relPath, sev);
|
|
1012
|
+
score = dynamic.score;
|
|
1013
|
+
reasons.push(...dynamic.reasons);
|
|
971
1014
|
} else if (category === "secrets") {
|
|
972
1015
|
if (sev === "CRITICAL") score = 0.95;
|
|
973
1016
|
else if (findingIsVerified(finding) || finding.match_text.includes("(verified)")) score = 0.93;
|
|
@@ -1287,9 +1330,13 @@ var CWE_NAMES = {
|
|
|
1287
1330
|
"CWE-639": "Authorization Bypass Through User-Controlled Key",
|
|
1288
1331
|
"CWE-770": "Allocation of Resources Without Limits or Throttling",
|
|
1289
1332
|
"CWE-798": "Use of Hard-coded Credentials",
|
|
1333
|
+
"CWE-312": "Cleartext Storage of Sensitive Information",
|
|
1334
|
+
"CWE-494": "Download of Code Without Integrity Check",
|
|
1335
|
+
"CWE-1188": "Insecure Default Initialization of Resource",
|
|
1290
1336
|
"CWE-915": "Improperly Controlled Modification of Dynamically-Determined Object Attributes",
|
|
1291
1337
|
"CWE-918": "Server-Side Request Forgery",
|
|
1292
|
-
"CWE-1336": "Improper Neutralization of Special Elements in Template Engine"
|
|
1338
|
+
"CWE-1336": "Improper Neutralization of Special Elements in Template Engine",
|
|
1339
|
+
"CWE-1104": "Use of Unmaintained Third Party Components"
|
|
1293
1340
|
};
|
|
1294
1341
|
var CVSS_MAP = {
|
|
1295
1342
|
"CWE-89": [9.8, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"],
|
|
@@ -1324,12 +1371,13 @@ function extractCweDeepFallback(result) {
|
|
|
1324
1371
|
const match = blob.match(CWE_RE);
|
|
1325
1372
|
return match ? match[0].toUpperCase() : null;
|
|
1326
1373
|
}
|
|
1327
|
-
var UNKNOWN_CWE_TYPE_LABEL = "Unknown Vulnerability Type";
|
|
1328
1374
|
function formatCweDisplay(cweId) {
|
|
1329
1375
|
const normalized = cweId.match(CWE_RE)?.[0]?.toUpperCase();
|
|
1330
|
-
if (!normalized || normalized === "UNKNOWN")
|
|
1376
|
+
if (!normalized || normalized === "UNKNOWN") {
|
|
1377
|
+
return "CWE-494: Download of Code Without Integrity Check";
|
|
1378
|
+
}
|
|
1331
1379
|
const name = CWE_NAMES[normalized];
|
|
1332
|
-
return name ? `${normalized}: ${name}` : `${normalized}:
|
|
1380
|
+
return name ? `${normalized}: ${name}` : `${normalized}: Security Weakness (Unmapped CWE)`;
|
|
1333
1381
|
}
|
|
1334
1382
|
function calculateCvss(cweId, semgrepSeverity = "WARNING") {
|
|
1335
1383
|
const normalized = cweId.match(CWE_RE)?.[0]?.toUpperCase();
|
|
@@ -1662,6 +1710,59 @@ function formatAsvsLevelDisplay(level) {
|
|
|
1662
1710
|
return `Level ${normalized.replace(/^L/i, "")}`;
|
|
1663
1711
|
}
|
|
1664
1712
|
|
|
1713
|
+
// src/rules/cweMapper.ts
|
|
1714
|
+
var CWE_RE2 = /CWE-\d+/i;
|
|
1715
|
+
var SECRET_RULE_RE = /trufflehog|(?:^|\.)secrets\.|cloud-secrets|sec-\d{3}|hard[-_]?coded|credential|api[-_]?key|token|password|vault|customregex/i;
|
|
1716
|
+
var INPUT_RULE_RE = /domain[-_]?input|validation|allowed_hosts|host[-_]?header|ssrf|xss|sqli|sql[-_]?injection|csrf|injection|sanitize|origin|dja-|div-|fas-016|nst-014|ruby-017|aac-001/i;
|
|
1717
|
+
var INFRA_RULE_RE = /infra|k8s|helm|docker|compose|terraform|tf-|deployment|hardening|cis-|kube|container|image:|pinned|sbom|syft/i;
|
|
1718
|
+
function normalizeCweToken(value) {
|
|
1719
|
+
if (typeof value !== "string") return null;
|
|
1720
|
+
const match = value.trim().match(CWE_RE2);
|
|
1721
|
+
return match ? match[0].toUpperCase() : null;
|
|
1722
|
+
}
|
|
1723
|
+
function classifyRuleForCweFallback(ruleId, category, description) {
|
|
1724
|
+
if (category === "secrets") return "secret";
|
|
1725
|
+
if (category === "dependencies") return "infra";
|
|
1726
|
+
const blob = `${ruleId} ${description ?? ""}`.toLowerCase();
|
|
1727
|
+
if (SECRET_RULE_RE.test(blob)) return "secret";
|
|
1728
|
+
if (INPUT_RULE_RE.test(blob)) return "input";
|
|
1729
|
+
if (INFRA_RULE_RE.test(blob)) return "infra";
|
|
1730
|
+
return "generic";
|
|
1731
|
+
}
|
|
1732
|
+
function fallbackCweForKind(kind) {
|
|
1733
|
+
switch (kind) {
|
|
1734
|
+
case "secret":
|
|
1735
|
+
return "CWE-798";
|
|
1736
|
+
case "input":
|
|
1737
|
+
return "CWE-20";
|
|
1738
|
+
case "infra":
|
|
1739
|
+
return "CWE-1188";
|
|
1740
|
+
default:
|
|
1741
|
+
return "CWE-494";
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
function resolveCweId(input) {
|
|
1745
|
+
const fromField = normalizeCweToken(input.cwe);
|
|
1746
|
+
if (fromField) return fromField;
|
|
1747
|
+
const fromName = normalizeCweToken(input.cwe_name);
|
|
1748
|
+
if (fromName) return fromName;
|
|
1749
|
+
const kind = classifyRuleForCweFallback(
|
|
1750
|
+
String(input.rule_id ?? ""),
|
|
1751
|
+
input.category,
|
|
1752
|
+
input.description
|
|
1753
|
+
);
|
|
1754
|
+
return fallbackCweForKind(kind);
|
|
1755
|
+
}
|
|
1756
|
+
function resolveCweDisplay(input) {
|
|
1757
|
+
const existing = String(input.cwe_name ?? "").trim();
|
|
1758
|
+
const existingId = normalizeCweToken(existing);
|
|
1759
|
+
if (existingId && !/unknown/i.test(existing)) {
|
|
1760
|
+
return formatCweDisplay(existingId);
|
|
1761
|
+
}
|
|
1762
|
+
const cweId = resolveCweId(input);
|
|
1763
|
+
return formatCweDisplay(cweId);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1665
1766
|
// src/engine/semgrepMapping.ts
|
|
1666
1767
|
function normalizeRelativePath(value) {
|
|
1667
1768
|
return value.replace(/\\/g, "/");
|
|
@@ -1756,7 +1857,13 @@ function mapSemgrepResultToFinding(result, workspaceRoot) {
|
|
|
1756
1857
|
metadata: result.extra?.metadata,
|
|
1757
1858
|
message
|
|
1758
1859
|
});
|
|
1759
|
-
const
|
|
1860
|
+
const rawCwe = semgrepCwe !== "UNKNOWN" ? semgrepCwe : registryRule?.cwe && registryRule.cwe !== "UNKNOWN" ? registryRule.cwe : fallbackMeta.cwe;
|
|
1861
|
+
const cwe = resolveCweId({
|
|
1862
|
+
cwe: rawCwe,
|
|
1863
|
+
rule_id: registryRule?.id ?? checkId,
|
|
1864
|
+
category: "code",
|
|
1865
|
+
description: message
|
|
1866
|
+
});
|
|
1760
1867
|
const metadata = result.extra?.metadata;
|
|
1761
1868
|
const remediation = extractRemediationFromMetadata(metadata);
|
|
1762
1869
|
const remediation_lang = extractRemediationLangFromMetadata(metadata);
|
|
@@ -1775,7 +1882,7 @@ function mapSemgrepResultToFinding(result, workspaceRoot) {
|
|
|
1775
1882
|
category: "code",
|
|
1776
1883
|
rule_id: registryRule?.id ?? checkId,
|
|
1777
1884
|
cwe,
|
|
1778
|
-
cwe_name:
|
|
1885
|
+
cwe_name: resolveCweDisplay({ cwe, rule_id: registryRule?.id ?? checkId, category: "code", description: message }),
|
|
1779
1886
|
severity: contextSeverity.severity,
|
|
1780
1887
|
original_severity,
|
|
1781
1888
|
description: message || registryRule?.description || checkId,
|
|
@@ -1871,10 +1978,13 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
|
|
|
1871
1978
|
const display = redacted || rawSecret || "[secret redacted]";
|
|
1872
1979
|
const description = `TruffleHog: exposed ${detector}${verified ? " (verified)" : ""}`;
|
|
1873
1980
|
const severity = severityForSecret(detector, verified);
|
|
1981
|
+
const rule_id = `runsec.secrets.trufflehog.${detector.toLowerCase().replace(/\s+/g, "-")}`;
|
|
1982
|
+
const cwe = resolveCweId({ cwe: "CWE-798", rule_id, category: "secrets", description, detector_name: detector });
|
|
1874
1983
|
findings.push({
|
|
1875
1984
|
category: "secrets",
|
|
1876
|
-
rule_id
|
|
1877
|
-
cwe
|
|
1985
|
+
rule_id,
|
|
1986
|
+
cwe,
|
|
1987
|
+
cwe_name: resolveCweDisplay({ cwe, rule_id, category: "secrets", description, detector_name: detector }),
|
|
1878
1988
|
stackTags: ["Secrets"],
|
|
1879
1989
|
asvsLevel: "L1",
|
|
1880
1990
|
asvsTrace: "V6.4.1",
|
|
@@ -3328,6 +3438,7 @@ function loadVersion() {
|
|
|
3328
3438
|
var RUNSEC_MCP_VERSION = loadVersion();
|
|
3329
3439
|
|
|
3330
3440
|
// src/engine/reportFormatter.ts
|
|
3441
|
+
var ALLOWED_HOSTS_WILDCARD_RE2 = /allowed_hosts\s*=\s*\[\s*['"]\*['"]\s*\]|allowed_hosts\s*=\s*\{\s*['"]\*['"]\s*\}/i;
|
|
3331
3442
|
var REPORT_SECTION_TITLES = {
|
|
3332
3443
|
code: "Code Vulnerabilities",
|
|
3333
3444
|
secrets: "Exposed Secrets",
|
|
@@ -3386,10 +3497,103 @@ function normalizeCategory(row) {
|
|
|
3386
3497
|
const c = row.category;
|
|
3387
3498
|
if (c === "secrets" || c === "dependencies" || c === "code") return c;
|
|
3388
3499
|
const rule = String(row.rule_id ?? "").toLowerCase();
|
|
3389
|
-
if (rule.includes("trufflehog") ||
|
|
3390
|
-
if (rule.includes("syft") || rule.includes("
|
|
3500
|
+
if (rule.includes("trufflehog") || row.detector_name) return "secrets";
|
|
3501
|
+
if (rule.includes("syft") || rule.includes("sbom")) return "dependencies";
|
|
3391
3502
|
return "code";
|
|
3392
3503
|
}
|
|
3504
|
+
function serviceComponentFromPath(filePath) {
|
|
3505
|
+
const normalized = String(filePath ?? "").replace(/\\/g, "/");
|
|
3506
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
3507
|
+
if (parts.length === 0) return "workspace";
|
|
3508
|
+
if (parts[0] === "src" && parts[1]) return parts[1];
|
|
3509
|
+
if (["app", "apps", "services", "packages", "deploy", "infra", "config"].includes(parts[0]) && parts[1]) {
|
|
3510
|
+
return `${parts[0]}/${parts[1]}`;
|
|
3511
|
+
}
|
|
3512
|
+
return parts[0];
|
|
3513
|
+
}
|
|
3514
|
+
function buildComponentMatrix(primary, suppressed) {
|
|
3515
|
+
const map = /* @__PURE__ */ new Map();
|
|
3516
|
+
const touch = (filePath) => {
|
|
3517
|
+
const key = serviceComponentFromPath(filePath);
|
|
3518
|
+
let row = map.get(key);
|
|
3519
|
+
if (!row) {
|
|
3520
|
+
row = { component: key, critical: 0, high: 0, medium: 0, suppressed: 0 };
|
|
3521
|
+
map.set(key, row);
|
|
3522
|
+
}
|
|
3523
|
+
return row;
|
|
3524
|
+
};
|
|
3525
|
+
for (const f of primary) {
|
|
3526
|
+
const row = touch(String(f.file_path ?? "workspace"));
|
|
3527
|
+
const sev = normalizeSeverity(f.severity);
|
|
3528
|
+
if (sev === "CRITICAL") row.critical += 1;
|
|
3529
|
+
else if (sev === "HIGH") row.high += 1;
|
|
3530
|
+
else if (sev === "MEDIUM") row.medium += 1;
|
|
3531
|
+
}
|
|
3532
|
+
for (const f of suppressed) {
|
|
3533
|
+
touch(String(f.file_path ?? "workspace")).suppressed += 1;
|
|
3534
|
+
}
|
|
3535
|
+
return Array.from(map.values()).sort(
|
|
3536
|
+
(a, b) => b.critical + b.high + b.medium + b.suppressed - (a.critical + a.high + a.medium + a.suppressed) || a.component.localeCompare(b.component)
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
function buildDeveloperTopActions(findings, limit = 3) {
|
|
3540
|
+
const ranked = [...findings].sort((a, b) => {
|
|
3541
|
+
const sev = severityRank(a.severity) - severityRank(b.severity);
|
|
3542
|
+
if (sev !== 0) return sev;
|
|
3543
|
+
const confA = typeof a.confidence_score === "number" ? a.confidence_score : 0;
|
|
3544
|
+
const confB = typeof b.confidence_score === "number" ? b.confidence_score : 0;
|
|
3545
|
+
return confB - confA;
|
|
3546
|
+
});
|
|
3547
|
+
const out = [];
|
|
3548
|
+
for (const f of ranked) {
|
|
3549
|
+
if (out.length >= limit) break;
|
|
3550
|
+
out.push({
|
|
3551
|
+
file_path: String(f.file_path ?? "unknown"),
|
|
3552
|
+
line: Number(f.line ?? 0),
|
|
3553
|
+
severity: normalizeSeverity(f.severity),
|
|
3554
|
+
rule_id: String(f.rule_id ?? "unknown_rule"),
|
|
3555
|
+
description: String(f.description ?? f.rule_id ?? "Security finding")
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
return out;
|
|
3559
|
+
}
|
|
3560
|
+
function buildProofOfConcept(group, example) {
|
|
3561
|
+
const blob = `${group.description} ${example?.snippet ?? ""} ${group.rule_id}`;
|
|
3562
|
+
if (ALLOWED_HOSTS_WILDCARD_RE2.test(blob) || /dja-005|insecure allowed_hosts/i.test(blob)) {
|
|
3563
|
+
return {
|
|
3564
|
+
bash: [
|
|
3565
|
+
"# Host header poisoning \u2014 run only against systems you are authorized to test",
|
|
3566
|
+
'curl -i -H "Host: evil-attacker.com" "https://<your-production-domain>/"'
|
|
3567
|
+
].join("\n")
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
if (/\bssrf\b|cwe-918|aac-001|nst-014|ruby-017|fas-016/i.test(blob)) {
|
|
3571
|
+
return {
|
|
3572
|
+
bash: [
|
|
3573
|
+
"# SSRF/metadata probe \u2014 should be blocked by egress controls",
|
|
3574
|
+
'curl -i --max-time 3 "http://169.254.169.254/latest/meta-data/"'
|
|
3575
|
+
].join("\n")
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
if (/csrf|cwe-352/i.test(blob)) {
|
|
3579
|
+
return {
|
|
3580
|
+
bash: [
|
|
3581
|
+
"# CSRF check \u2014 confirm state-changing routes require anti-CSRF token",
|
|
3582
|
+
'curl -i -X POST "https://<your-production-domain>/api/state-changing" -H "Cookie: session=<value>"'
|
|
3583
|
+
].join("\n")
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
return null;
|
|
3587
|
+
}
|
|
3588
|
+
function formatRemediationAsDiff(remediation, example, description) {
|
|
3589
|
+
const fix = remediation.trim();
|
|
3590
|
+
if (!fix || !example?.snippet?.trim() || example.snippet === NO_CODE_SNIPPET) return fix;
|
|
3591
|
+
if (description && fix === description.trim()) return fix;
|
|
3592
|
+
const bad = example.snippet.trim();
|
|
3593
|
+
if (bad.includes("\n") || fix.includes("\n")) return fix;
|
|
3594
|
+
if (bad === fix) return fix;
|
|
3595
|
+
return ["--- vulnerable", "+++ recommended", `-${bad}`, `+${fix}`].join("\n");
|
|
3596
|
+
}
|
|
3393
3597
|
var SEVERITY_RANK = {
|
|
3394
3598
|
CRITICAL: 0,
|
|
3395
3599
|
ERROR: 0,
|
|
@@ -3478,63 +3682,6 @@ function formatStackTagsInline(tags) {
|
|
|
3478
3682
|
if (!tags.length) return "";
|
|
3479
3683
|
return tags.map((t) => `**[${safeText(t)}]**`).join(" ");
|
|
3480
3684
|
}
|
|
3481
|
-
var TECH_STACK_BY_EXT = {
|
|
3482
|
-
".py": "Python",
|
|
3483
|
-
".js": "JavaScript/TypeScript",
|
|
3484
|
-
".jsx": "JavaScript/TypeScript",
|
|
3485
|
-
".mjs": "JavaScript/TypeScript",
|
|
3486
|
-
".cjs": "JavaScript/TypeScript",
|
|
3487
|
-
".ts": "JavaScript/TypeScript",
|
|
3488
|
-
".tsx": "JavaScript/TypeScript",
|
|
3489
|
-
".java": "Java",
|
|
3490
|
-
".kt": "Kotlin",
|
|
3491
|
-
".kts": "Kotlin",
|
|
3492
|
-
".go": "Go",
|
|
3493
|
-
".dart": "Dart",
|
|
3494
|
-
".rb": "Ruby",
|
|
3495
|
-
".php": "PHP",
|
|
3496
|
-
".rs": "Rust",
|
|
3497
|
-
".cs": "C#",
|
|
3498
|
-
".cpp": "C++",
|
|
3499
|
-
".cc": "C++",
|
|
3500
|
-
".c": "C",
|
|
3501
|
-
".h": "C/C++ Header",
|
|
3502
|
-
".swift": "Swift",
|
|
3503
|
-
".scala": "Scala",
|
|
3504
|
-
".yaml": "YAML",
|
|
3505
|
-
".yml": "YAML",
|
|
3506
|
-
".json": "JSON",
|
|
3507
|
-
".xml": "XML",
|
|
3508
|
-
".tf": "Terraform",
|
|
3509
|
-
".vue": "Vue",
|
|
3510
|
-
".svelte": "Svelte",
|
|
3511
|
-
".sql": "SQL",
|
|
3512
|
-
".sh": "Shell",
|
|
3513
|
-
".bash": "Shell",
|
|
3514
|
-
".zsh": "Shell",
|
|
3515
|
-
".ps1": "PowerShell",
|
|
3516
|
-
".gradle": "Gradle",
|
|
3517
|
-
".properties": "Properties",
|
|
3518
|
-
".toml": "TOML",
|
|
3519
|
-
".ini": "INI",
|
|
3520
|
-
".conf": "Config",
|
|
3521
|
-
".md": "Markdown"
|
|
3522
|
-
};
|
|
3523
|
-
function techStackFromFilePath(filePath) {
|
|
3524
|
-
const normalized = String(filePath ?? "").replace(/\\/g, "/");
|
|
3525
|
-
const base = import_node_path16.default.basename(normalized).toLowerCase();
|
|
3526
|
-
if (base === "dockerfile" || base.startsWith("dockerfile.")) return "Docker";
|
|
3527
|
-
const ext = import_node_path16.default.extname(normalized).toLowerCase();
|
|
3528
|
-
return TECH_STACK_BY_EXT[ext] ?? "Other";
|
|
3529
|
-
}
|
|
3530
|
-
function countFindingsByTechStack(findings) {
|
|
3531
|
-
const counts = /* @__PURE__ */ new Map();
|
|
3532
|
-
for (const row of findings) {
|
|
3533
|
-
const stack = techStackFromFilePath(String(row.file_path ?? ""));
|
|
3534
|
-
counts.set(stack, (counts.get(stack) ?? 0) + 1);
|
|
3535
|
-
}
|
|
3536
|
-
return Array.from(counts.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
|
3537
|
-
}
|
|
3538
3685
|
function suppressionReasonLabel(finding) {
|
|
3539
3686
|
const reason = String(finding.suppression_reason ?? "").trim();
|
|
3540
3687
|
if (reason) return reason;
|
|
@@ -3544,76 +3691,32 @@ function suppressionReasonLabel(finding) {
|
|
|
3544
3691
|
return "cognitive_suppressed";
|
|
3545
3692
|
}
|
|
3546
3693
|
function renderSuppressedFindingsSection(suppressed) {
|
|
3547
|
-
|
|
3548
|
-
if (suppressed.length === 0) return out;
|
|
3694
|
+
if (suppressed.length === 0) return [];
|
|
3549
3695
|
const byReason = /* @__PURE__ */ new Map();
|
|
3550
3696
|
for (const row of suppressed) {
|
|
3551
3697
|
const key = suppressionReasonLabel(row);
|
|
3552
|
-
|
|
3553
|
-
if (bucket) bucket.push(row);
|
|
3554
|
-
else byReason.set(key, [row]);
|
|
3698
|
+
byReason.set(key, (byReason.get(key) ?? 0) + 1);
|
|
3555
3699
|
}
|
|
3700
|
+
const out = [];
|
|
3556
3701
|
out.push("---");
|
|
3557
|
-
out.push(
|
|
3702
|
+
out.push("<details>");
|
|
3703
|
+
out.push(`<summary>Appendix: Cognitive suppression summary (${suppressed.length} items)</summary>`);
|
|
3558
3704
|
out.push("");
|
|
3559
3705
|
out.push(
|
|
3560
|
-
"
|
|
3706
|
+
"Findings below the primary confidence threshold (0.8) or nuclear-hard-dropped. They do not affect `X-RunSec-Verdict`."
|
|
3561
3707
|
);
|
|
3562
3708
|
out.push("");
|
|
3563
3709
|
out.push("| Suppression reason | Count |");
|
|
3564
3710
|
out.push("|---|---:|");
|
|
3565
|
-
const sortedReasons = Array.from(byReason.entries()).sort((a, b) => b[1]
|
|
3566
|
-
for (const [reason,
|
|
3567
|
-
out.push(`| \`${safeText(reason)}\` | ${
|
|
3568
|
-
}
|
|
3569
|
-
const maxFilesPerReason = 40;
|
|
3570
|
-
for (const [reason, rows] of sortedReasons) {
|
|
3571
|
-
out.push("");
|
|
3572
|
-
out.push(`### ${safeText(reason)} (${rows.length})`);
|
|
3573
|
-
out.push("");
|
|
3574
|
-
out.push("**Affected files:**");
|
|
3575
|
-
const locations = [
|
|
3576
|
-
...new Set(
|
|
3577
|
-
rows.map((row) => `${String(row.file_path || "unknown")}:${Number(row.line ?? 0)}`)
|
|
3578
|
-
)
|
|
3579
|
-
].sort();
|
|
3580
|
-
for (const loc of locations.slice(0, maxFilesPerReason)) {
|
|
3581
|
-
out.push(`- \`${safeText(loc)}\``);
|
|
3582
|
-
}
|
|
3583
|
-
if (locations.length > maxFilesPerReason) {
|
|
3584
|
-
out.push(`- _+${locations.length - maxFilesPerReason} additional locations omitted._`);
|
|
3585
|
-
}
|
|
3586
|
-
const sample = rows.find((r) => String(r.snippet ?? r.match_text ?? "").trim());
|
|
3587
|
-
const sampleText = String(sample?.snippet ?? sample?.match_text ?? "").trim();
|
|
3588
|
-
if (sampleText) {
|
|
3589
|
-
out.push("");
|
|
3590
|
-
out.push("**Example snippet:**");
|
|
3591
|
-
out.push("");
|
|
3592
|
-
out.push("```text");
|
|
3593
|
-
out.push(safeText(sampleText.slice(0, 400)));
|
|
3594
|
-
out.push("```");
|
|
3595
|
-
}
|
|
3711
|
+
const sortedReasons = Array.from(byReason.entries()).sort((a, b) => b[1] - a[1]);
|
|
3712
|
+
for (const [reason, count] of sortedReasons) {
|
|
3713
|
+
out.push(`| \`${safeText(reason)}\` | ${count} |`);
|
|
3596
3714
|
}
|
|
3597
|
-
return out;
|
|
3598
|
-
}
|
|
3599
|
-
function renderTechStackSummary(findings) {
|
|
3600
|
-
const rows = countFindingsByTechStack(findings);
|
|
3601
|
-
const out = [];
|
|
3602
|
-
out.push("### Findings by tech stack");
|
|
3603
3715
|
out.push("");
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
out.push("| Tech stack | Findings |");
|
|
3609
|
-
out.push("|---|---:|");
|
|
3610
|
-
for (const { tag, count } of rows.slice(0, 20)) {
|
|
3611
|
-
out.push(`| ${safeText(tag)} | ${count} |`);
|
|
3612
|
-
}
|
|
3613
|
-
if (rows.length > 20) {
|
|
3614
|
-
out.push("");
|
|
3615
|
-
out.push(`_+${rows.length - 20} additional stacks omitted._`);
|
|
3616
|
-
}
|
|
3716
|
+
out.push(
|
|
3717
|
+
"_Per-file listings omitted to reduce noise. Re-run with verbose telemetry or inspect engine JSON for full paths._"
|
|
3718
|
+
);
|
|
3719
|
+
out.push("</details>");
|
|
3617
3720
|
return out;
|
|
3618
3721
|
}
|
|
3619
3722
|
function groupFindingsByRule(findings) {
|
|
@@ -3626,8 +3729,22 @@ function groupFindingsByRule(findings) {
|
|
|
3626
3729
|
rule_id: ruleId,
|
|
3627
3730
|
description: String(f.description || ruleId),
|
|
3628
3731
|
severity: normalizeSeverity(f.severity),
|
|
3629
|
-
cwe:
|
|
3630
|
-
|
|
3732
|
+
cwe: resolveCweId({
|
|
3733
|
+
cwe: f.cwe,
|
|
3734
|
+
cwe_name: f.cwe_name,
|
|
3735
|
+
rule_id: ruleId,
|
|
3736
|
+
category: normalizeCategory(f),
|
|
3737
|
+
description: f.description,
|
|
3738
|
+
detector_name: f.detector_name
|
|
3739
|
+
}),
|
|
3740
|
+
cwe_name: resolveCweDisplay({
|
|
3741
|
+
cwe: f.cwe,
|
|
3742
|
+
cwe_name: f.cwe_name,
|
|
3743
|
+
rule_id: ruleId,
|
|
3744
|
+
category: normalizeCategory(f),
|
|
3745
|
+
description: f.description,
|
|
3746
|
+
detector_name: f.detector_name
|
|
3747
|
+
}),
|
|
3631
3748
|
remediation: String(f.remediation || "").trim(),
|
|
3632
3749
|
metadata: {
|
|
3633
3750
|
remediation_lang: f.metadata?.remediation_lang?.trim() || void 0
|
|
@@ -3705,7 +3822,7 @@ function countSeverity(findings) {
|
|
|
3705
3822
|
}
|
|
3706
3823
|
return { critical, high, medium, low };
|
|
3707
3824
|
}
|
|
3708
|
-
function renderFindingRows(findings) {
|
|
3825
|
+
function renderFindingRows(findings, category) {
|
|
3709
3826
|
const out = [];
|
|
3710
3827
|
if (findings.length === 0) {
|
|
3711
3828
|
out.push("_No findings in this category._");
|
|
@@ -3713,56 +3830,101 @@ function renderFindingRows(findings) {
|
|
|
3713
3830
|
}
|
|
3714
3831
|
for (const group of groupFindingsByRule(findings)) {
|
|
3715
3832
|
const metric = shortRuleLabel(group.rule_id);
|
|
3716
|
-
const conf = typeof group.confidence_score === "number" ? `
|
|
3833
|
+
const conf = typeof group.confidence_score === "number" ? ` (confidence ${group.confidence_score.toFixed(2)})` : "";
|
|
3717
3834
|
const cweLabel = safeText(group.cwe_name || group.cwe);
|
|
3718
|
-
const
|
|
3719
|
-
const
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
out.push(
|
|
3723
|
-
|
|
3724
|
-
);
|
|
3725
|
-
if (group.description.trim()) {
|
|
3835
|
+
const example = group.exampleOccurrence ?? group.occurrences[0];
|
|
3836
|
+
const primaryLoc = example ? `\`${safeText(example.file_path)}:${example.line}\`` : "`unknown:0`";
|
|
3837
|
+
out.push(`### ${normalizeSeverity(group.severity)} \u2014 \`${safeText(group.rule_id)}\` (${metric})${conf}`);
|
|
3838
|
+
out.push("");
|
|
3839
|
+
out.push(`**Target location:** ${primaryLoc}`);
|
|
3840
|
+
if (group.occurrences.length > 1) {
|
|
3726
3841
|
out.push("");
|
|
3727
|
-
out.push(
|
|
3842
|
+
out.push("**Additional locations:**");
|
|
3843
|
+
for (const occ of group.occurrences.slice(0, 12)) {
|
|
3844
|
+
out.push(`- \`${safeText(occ.file_path)}:${occ.line}\``);
|
|
3845
|
+
}
|
|
3846
|
+
if (group.occurrences.length > 12) {
|
|
3847
|
+
out.push(`- _+${group.occurrences.length - 12} more omitted_`);
|
|
3848
|
+
}
|
|
3728
3849
|
}
|
|
3729
3850
|
out.push("");
|
|
3730
|
-
out.push(
|
|
3731
|
-
`${conf}**CWE:** ${cweLabel}${stackInline ? ` ${stackInline}` : ""}`
|
|
3732
|
-
);
|
|
3733
|
-
if (cvssLine) {
|
|
3734
|
-
out.push(cvssLine);
|
|
3735
|
-
}
|
|
3736
|
-
if (asvsLine) {
|
|
3737
|
-
out.push(asvsLine);
|
|
3738
|
-
}
|
|
3851
|
+
out.push("**Problem description:**");
|
|
3739
3852
|
out.push("");
|
|
3740
|
-
out.push(
|
|
3741
|
-
|
|
3742
|
-
|
|
3853
|
+
out.push(safeText(group.description.trim() || group.rule_id));
|
|
3854
|
+
out.push("");
|
|
3855
|
+
out.push(`**CWE:** ${cweLabel}`);
|
|
3856
|
+
const stackInline = formatStackTagsInline(group.stackTags);
|
|
3857
|
+
if (stackInline) out.push(stackInline);
|
|
3858
|
+
if (typeof group.cvss_score === "number") {
|
|
3859
|
+
out.push(
|
|
3860
|
+
`**CVSS:** ${group.cvss_score.toFixed(1)}${group.cvss_vector ? ` (\`${safeText(group.cvss_vector)}\`)` : ""}`
|
|
3861
|
+
);
|
|
3862
|
+
}
|
|
3863
|
+
const asvsLevelText = formatAsvsLevelDisplay(group.asvsLevel);
|
|
3864
|
+
if (asvsLevelText) {
|
|
3865
|
+
out.push(
|
|
3866
|
+
`**ASVS:** ${asvsLevelText}${group.asvsTrace ? ` (${safeText(group.asvsTrace)})` : ""}`
|
|
3867
|
+
);
|
|
3743
3868
|
}
|
|
3744
3869
|
out.push("");
|
|
3745
|
-
|
|
3746
|
-
if (example) {
|
|
3870
|
+
if (example && isUsableSnippet(example.snippet)) {
|
|
3747
3871
|
const lang = snippetLanguage(example.file_path);
|
|
3748
|
-
out.push(
|
|
3872
|
+
out.push("**Code snippet:**");
|
|
3749
3873
|
out.push("");
|
|
3750
3874
|
out.push("```" + lang);
|
|
3751
3875
|
out.push(example.snippet);
|
|
3752
3876
|
out.push("```");
|
|
3753
3877
|
out.push("");
|
|
3754
3878
|
}
|
|
3879
|
+
if (category === "code") {
|
|
3880
|
+
const poc = buildProofOfConcept(group, example);
|
|
3881
|
+
if (poc) {
|
|
3882
|
+
out.push("**Proof of Concept (Instant Verification):**");
|
|
3883
|
+
out.push("");
|
|
3884
|
+
out.push("```bash");
|
|
3885
|
+
out.push(poc.bash);
|
|
3886
|
+
out.push("```");
|
|
3887
|
+
out.push("");
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3755
3890
|
const remediationText = remediationForGroup(group);
|
|
3756
3891
|
const remediationLang = remediationFenceLanguage(group);
|
|
3757
|
-
|
|
3892
|
+
const diffText = formatRemediationAsDiff(remediationText, example, group.description);
|
|
3893
|
+
out.push("**Contextual remediation:**");
|
|
3758
3894
|
out.push("");
|
|
3759
3895
|
out.push("```" + remediationLang);
|
|
3760
|
-
out.push(
|
|
3896
|
+
out.push(diffText);
|
|
3761
3897
|
out.push("```");
|
|
3762
3898
|
out.push("");
|
|
3763
3899
|
}
|
|
3764
3900
|
return out;
|
|
3765
3901
|
}
|
|
3902
|
+
function renderComplianceAuditorSection(standard, findings) {
|
|
3903
|
+
const out = [];
|
|
3904
|
+
out.push("### For Compliance Auditors");
|
|
3905
|
+
out.push("");
|
|
3906
|
+
const cweSet = new Set(findings.map((f) => resolveCweId({ cwe: f.cwe, rule_id: f.rule_id, category: f.category, description: f.description })));
|
|
3907
|
+
const hasSecrets = findings.some((f) => normalizeCategory(f) === "secrets");
|
|
3908
|
+
const hasInput = [...cweSet].some((c) => /CWE-(20|79|89|346|352|918)/.test(c));
|
|
3909
|
+
const hasCrypto = [...cweSet].some((c) => /CWE-(327|295|347)/.test(c));
|
|
3910
|
+
out.push("| Baseline | Applicable controls | Scan signal |");
|
|
3911
|
+
out.push("|---|---|---|");
|
|
3912
|
+
out.push(
|
|
3913
|
+
`| SOC 2 Trust Services Criteria (Security) | CC6.1 Logical access, CC7.2 System monitoring | ${hasSecrets ? "Secret exposure patterns detected" : "No primary secret findings"} |`
|
|
3914
|
+
);
|
|
3915
|
+
out.push(
|
|
3916
|
+
`| PCI-DSS v4.0 | Req. 6.2 Secure development, Req. 6.4 Public-facing vulnerabilities | ${hasInput ? "Input validation / injection class findings present" : "No primary injection-class findings"} |`
|
|
3917
|
+
);
|
|
3918
|
+
out.push(
|
|
3919
|
+
`| OWASP ASVS (mapped) | V5 Validation / V13 API / V14 Config | ${findings.length ? `${findings.length} primary finding(s) with ASVS traces where available` : "Clean primary log"} |`
|
|
3920
|
+
);
|
|
3921
|
+
if (hasCrypto) {
|
|
3922
|
+
out.push(`| Cryptographic controls | ASVS V13.2 / PCI 3.5 | Weak crypto or TLS findings in primary log |`);
|
|
3923
|
+
}
|
|
3924
|
+
out.push("");
|
|
3925
|
+
out.push(`_Audit standard selected for this run: **${safeText(standard)}**._`);
|
|
3926
|
+
return out;
|
|
3927
|
+
}
|
|
3766
3928
|
function buildServerSideReportMarkdown(standard, findings, metrics) {
|
|
3767
3929
|
const verdictLabel = metrics.verdict?.http_headers?.["X-RunSec-Verdict"] ?? metrics.verdict?.status ?? "PASS";
|
|
3768
3930
|
const allSev = countSeverity(findings);
|
|
@@ -3775,62 +3937,96 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
|
|
|
3775
3937
|
byCategory[normalizeCategory(row)].push(row);
|
|
3776
3938
|
}
|
|
3777
3939
|
const es = metrics.engine_summary;
|
|
3940
|
+
const suppressedRows = Array.isArray(metrics.findings_suppressed) ? metrics.findings_suppressed : [];
|
|
3941
|
+
const cognitiveSuppressed = Number(metrics.cognitive_suppressed_count ?? suppressedRows.length);
|
|
3942
|
+
const rawSecrets = es?.trufflehog?.finding_count ?? metrics.cognitive?.findings_total ?? 0;
|
|
3778
3943
|
const out = [];
|
|
3779
|
-
out.push(`# RunSec
|
|
3944
|
+
out.push(`# RunSec Security Report`);
|
|
3780
3945
|
out.push("");
|
|
3781
|
-
out.push(
|
|
3782
|
-
out.push(
|
|
3783
|
-
out.push(
|
|
3784
|
-
out.push(
|
|
3946
|
+
out.push("| | |");
|
|
3947
|
+
out.push("|---|---|");
|
|
3948
|
+
out.push(`| **X-RunSec-Verdict** | \`${safeText(verdictLabel)}\`${metrics.verdict?.is_safe === false && metrics.verdict.fail_reason ? ` \u2014 ${safeText(metrics.verdict.fail_reason)}` : ""} |`);
|
|
3949
|
+
out.push(`| **Generated at** | ${(/* @__PURE__ */ new Date()).toISOString()} |`);
|
|
3950
|
+
out.push(`| **MCP package** | \`@runsec/mcp@${safeText(RUNSEC_MCP_VERSION)}\` |`);
|
|
3951
|
+
out.push(`| **AI cognitive noise suppressed** | ${cognitiveSuppressed} |`);
|
|
3952
|
+
out.push(`| **Standard** | ${safeText(standard)} |`);
|
|
3785
3953
|
out.push(
|
|
3786
|
-
|
|
3954
|
+
`| **Scan** | ${Number(metrics.duration_ms || 0)}ms \xB7 ${Number(metrics.scanned_files_count || 0)} files \xB7 ${Number(metrics.total_rules || 0)} rules |`
|
|
3787
3955
|
);
|
|
3788
3956
|
if (metrics.engines) {
|
|
3789
3957
|
out.push(
|
|
3790
|
-
|
|
3958
|
+
`| **Engines** | Semgrep \`${safeText(metrics.engines.semgrep ?? "n/a")}\` \xB7 TruffleHog \`${safeText(metrics.engines.trufflehog ?? "n/a")}\` \xB7 Syft \`${safeText(metrics.engines.syft ?? "n/a")}\`${es?.concurrent_duration_ms != null ? ` \xB7 ${es.concurrent_duration_ms}ms wall` : ""} |`
|
|
3791
3959
|
);
|
|
3792
3960
|
}
|
|
3793
3961
|
if (es) {
|
|
3794
3962
|
out.push(
|
|
3795
|
-
|
|
3963
|
+
`| **Raw engine counts** | Code ${es.semgrep?.finding_count ?? 0} \xB7 Secrets ${rawEngineCountLabel("trufflehog", es)} \xB7 Dependencies ${rawEngineCountLabel("syft", es)} |`
|
|
3796
3964
|
);
|
|
3797
3965
|
}
|
|
3798
3966
|
out.push("");
|
|
3799
3967
|
out.push("---");
|
|
3800
|
-
out.push("## Executive
|
|
3801
|
-
out.push(
|
|
3802
|
-
`- **CRITICAL:** ${allSev.critical} | **HIGH:** ${allSev.high} | **MEDIUM:** ${allSev.medium} | **LOW:** ${allSev.low}`
|
|
3803
|
-
);
|
|
3968
|
+
out.push("## Executive Summary");
|
|
3969
|
+
out.push("");
|
|
3804
3970
|
out.push(
|
|
3805
|
-
|
|
3971
|
+
`**Primary severity:** CRITICAL ${allSev.critical} \xB7 HIGH ${allSev.high} \xB7 MEDIUM ${allSev.medium} \xB7 LOW ${allSev.low} \xB7 Primary findings ${findings.length}`
|
|
3806
3972
|
);
|
|
3807
|
-
const suppressedRows = Array.isArray(metrics.findings_suppressed) ? metrics.findings_suppressed : [];
|
|
3808
|
-
const rawSecrets = es?.trufflehog?.finding_count ?? metrics.cognitive?.findings_total ?? 0;
|
|
3809
3973
|
out.push(
|
|
3810
|
-
|
|
3974
|
+
`**By engine (primary log):** Code (Semgrep) ${byCategory.code.length} \xB7 Secrets (TruffleHog) ${executiveCategoryLabel("secrets", byCategory.secrets.length, es)} \xB7 Dependencies (Syft) ${executiveCategoryLabel("dependencies", byCategory.dependencies.length, es)}`
|
|
3811
3975
|
);
|
|
3812
3976
|
if (Number(rawSecrets) > 0 && findings.length === 0 && suppressedRows.length > 0) {
|
|
3813
|
-
out.push(
|
|
3814
|
-
`- **Raw TruffleHog hits:** ${rawSecrets} \u2014 see **Appendix: Cognitively suppressed findings** below.`
|
|
3815
|
-
);
|
|
3977
|
+
out.push(`**Note:** ${rawSecrets} raw TruffleHog hits were cognitively suppressed \u2014 see appendix.`);
|
|
3816
3978
|
}
|
|
3817
3979
|
out.push("");
|
|
3818
|
-
out.push(
|
|
3980
|
+
out.push("### For Developers (Actionable Top 3)");
|
|
3981
|
+
out.push("");
|
|
3982
|
+
const topActions = buildDeveloperTopActions(findings, 3);
|
|
3983
|
+
if (topActions.length === 0) {
|
|
3984
|
+
out.push("- No blocking primary findings \u2014 maintain secure defaults on the next change set.");
|
|
3985
|
+
} else {
|
|
3986
|
+
for (const item of topActions) {
|
|
3987
|
+
out.push(
|
|
3988
|
+
`- **[${item.severity}]** \`${safeText(item.file_path)}:${item.line}\` \u2014 ${safeText(item.description)} (\`${safeText(item.rule_id)}\`)`
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
out.push("");
|
|
3993
|
+
out.push("### For Tech Leads (Component Vulnerability Matrix)");
|
|
3994
|
+
out.push("");
|
|
3995
|
+
const matrix = buildComponentMatrix(findings, suppressedRows);
|
|
3996
|
+
if (matrix.length === 0) {
|
|
3997
|
+
out.push("_No component-level signal in the primary or suppressed logs._");
|
|
3998
|
+
} else {
|
|
3999
|
+
out.push("| Service/Component | CRITICAL | HIGH | MEDIUM | Suppressed Noise |");
|
|
4000
|
+
out.push("|---|---:|---:|---:|---:|");
|
|
4001
|
+
for (const row of matrix.slice(0, 24)) {
|
|
4002
|
+
out.push(
|
|
4003
|
+
`| ${safeText(row.component)} | ${row.critical} | ${row.high} | ${row.medium} | ${row.suppressed} |`
|
|
4004
|
+
);
|
|
4005
|
+
}
|
|
4006
|
+
if (matrix.length > 24) {
|
|
4007
|
+
out.push(`| _+${matrix.length - 24} components omitted_ | | | | |`);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
out.push("");
|
|
4011
|
+
out.push(...renderComplianceAuditorSection(standard, findings));
|
|
4012
|
+
out.push("");
|
|
4013
|
+
out.push("---");
|
|
4014
|
+
out.push("## Detailed Findings");
|
|
3819
4015
|
out.push("");
|
|
3820
4016
|
let sectionNum = 1;
|
|
3821
4017
|
for (const category of CATEGORY_ORDER) {
|
|
3822
4018
|
const rows = byCategory[category];
|
|
3823
4019
|
const engineTag = category === "code" ? "Semgrep" : category === "secrets" ? "TruffleHog" : "Syft SBOM";
|
|
3824
|
-
out.push(
|
|
3825
|
-
out.push(`## ${sectionNum}. ${REPORT_SECTION_TITLES[category]} (${engineTag})`);
|
|
4020
|
+
out.push(`### ${sectionNum}. ${REPORT_SECTION_TITLES[category]} (${engineTag})`);
|
|
3826
4021
|
out.push(...renderCategorySectionSummary(category, rows, es, metrics));
|
|
3827
4022
|
out.push("");
|
|
3828
|
-
out.push(...renderFindingRows(rows));
|
|
4023
|
+
out.push(...renderFindingRows(rows, category));
|
|
3829
4024
|
sectionNum += 1;
|
|
3830
4025
|
}
|
|
3831
4026
|
out.push(...renderSuppressedFindingsSection(suppressedRows));
|
|
3832
4027
|
out.push("---");
|
|
3833
|
-
out.push("<details><summary>Telemetry (machine)</summary
|
|
4028
|
+
out.push("<details><summary>Telemetry (machine-readable)</summary>");
|
|
4029
|
+
out.push("");
|
|
3834
4030
|
out.push("```json");
|
|
3835
4031
|
out.push(
|
|
3836
4032
|
JSON.stringify(
|
|
@@ -6066,6 +6262,19 @@ function localhostAlternateUploadUrl(url) {
|
|
|
6066
6262
|
return null;
|
|
6067
6263
|
}
|
|
6068
6264
|
}
|
|
6265
|
+
function hubUploadUrlCandidates(primaryUrl) {
|
|
6266
|
+
const urls = [];
|
|
6267
|
+
const add = (candidate) => {
|
|
6268
|
+
const trimmed = candidate?.trim();
|
|
6269
|
+
if (trimmed && !urls.includes(trimmed)) urls.push(trimmed);
|
|
6270
|
+
};
|
|
6271
|
+
add(primaryUrl);
|
|
6272
|
+
const direct = process.env.RUNSEC_HUB_DIRECT_ORIGIN?.trim();
|
|
6273
|
+
if (direct) add(appendUploadPath(direct));
|
|
6274
|
+
add(localhostAlternateUploadUrl(primaryUrl));
|
|
6275
|
+
add(dockerInternalAlternateUploadUrl(primaryUrl));
|
|
6276
|
+
return urls;
|
|
6277
|
+
}
|
|
6069
6278
|
function dockerInternalAlternateUploadUrl(url) {
|
|
6070
6279
|
if (!isDockerRuntime()) return null;
|
|
6071
6280
|
try {
|
|
@@ -6195,7 +6404,7 @@ function formatHubSyncErrorMessage(error, targetUrl, response, responseBody) {
|
|
|
6195
6404
|
message += `. Body: ${responseBody.trim().slice(0, 300)}`;
|
|
6196
6405
|
}
|
|
6197
6406
|
if (status === "n/a" && /fetch failed|ECONNRESET|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|CERT|SSL|TLS/i.test(err.message + code)) {
|
|
6198
|
-
message += ". Hint:
|
|
6407
|
+
message += ". Hint: if Hub uses Cloudflare Tunnel, set HTTP Host Header to runsec.io (not localhost:3000) in the tunnel public hostname settings, or set RUNSEC_HUB_DIRECT_ORIGIN=http://YOUR_SERVER_IP:3000 in MCP env to bypass the tunnel.";
|
|
6199
6408
|
}
|
|
6200
6409
|
return message;
|
|
6201
6410
|
}
|
|
@@ -6213,12 +6422,17 @@ function slimFindingForHubUpload(finding) {
|
|
|
6213
6422
|
suppression_reason: finding.suppression_reason
|
|
6214
6423
|
};
|
|
6215
6424
|
}
|
|
6216
|
-
function buildSuppressedSummaryForHub(findings) {
|
|
6425
|
+
function buildSuppressedSummaryForHub(findings, includeLocations) {
|
|
6217
6426
|
const byReason = {};
|
|
6218
|
-
const locations = [];
|
|
6219
6427
|
for (const f of findings) {
|
|
6220
6428
|
const reason = String(f.suppression_reason ?? "cognitive_suppressed");
|
|
6221
6429
|
byReason[reason] = (byReason[reason] ?? 0) + 1;
|
|
6430
|
+
}
|
|
6431
|
+
const summary = { total: findings.length, by_reason: byReason };
|
|
6432
|
+
if (!includeLocations) return summary;
|
|
6433
|
+
const locations = [];
|
|
6434
|
+
for (const f of findings) {
|
|
6435
|
+
const reason = String(f.suppression_reason ?? "cognitive_suppressed");
|
|
6222
6436
|
locations.push({
|
|
6223
6437
|
file_path: f.file_path,
|
|
6224
6438
|
line: f.line ?? 0,
|
|
@@ -6228,7 +6442,8 @@ function buildSuppressedSummaryForHub(findings) {
|
|
|
6228
6442
|
severity: f.severity
|
|
6229
6443
|
});
|
|
6230
6444
|
}
|
|
6231
|
-
|
|
6445
|
+
summary.locations = locations;
|
|
6446
|
+
return summary;
|
|
6232
6447
|
}
|
|
6233
6448
|
function slimEngineSummaryForHub(engineSummary) {
|
|
6234
6449
|
if (!engineSummary) return void 0;
|
|
@@ -6252,9 +6467,43 @@ function slimReportMetricsForHub(reportMetrics) {
|
|
|
6252
6467
|
engine_summary: slimEngineSummaryForHub(engine_summary)
|
|
6253
6468
|
};
|
|
6254
6469
|
}
|
|
6470
|
+
function minimalRunsecJsonForHubUpload(result, workspacePath, verdict, metrics, compliance, complianceASVS) {
|
|
6471
|
+
return {
|
|
6472
|
+
source: "runsec_mcp",
|
|
6473
|
+
transport: "minimal",
|
|
6474
|
+
standard: result.standard,
|
|
6475
|
+
workspace_path: workspacePath,
|
|
6476
|
+
verdict,
|
|
6477
|
+
metrics,
|
|
6478
|
+
compliance,
|
|
6479
|
+
complianceASVS,
|
|
6480
|
+
duration_ms: result.duration_ms,
|
|
6481
|
+
findings_count: result.findings_count,
|
|
6482
|
+
cognitive_suppressed_count: result.cognitive_suppressed_count,
|
|
6483
|
+
findings_suppressed_summary: buildSuppressedSummaryForHub(result.findings_suppressed, false),
|
|
6484
|
+
cognitive: result.cognitive,
|
|
6485
|
+
engine_summary: slimEngineSummaryForHub(result.engine_summary),
|
|
6486
|
+
verdict_detail: {
|
|
6487
|
+
status: result.verdict?.status,
|
|
6488
|
+
blocking_findings_count: result.verdict?.blocking_findings_count,
|
|
6489
|
+
primary_findings_count: result.verdict?.primary_findings_count
|
|
6490
|
+
}
|
|
6491
|
+
};
|
|
6492
|
+
}
|
|
6255
6493
|
function slimRunsecJsonForHubUpload(result, reportMetrics, workspacePath, verdict, metrics, compliance, complianceASVS) {
|
|
6494
|
+
if (process.env.RUNSEC_HUB_FULL_UPLOAD !== "1") {
|
|
6495
|
+
return minimalRunsecJsonForHubUpload(
|
|
6496
|
+
result,
|
|
6497
|
+
workspacePath,
|
|
6498
|
+
verdict,
|
|
6499
|
+
metrics,
|
|
6500
|
+
compliance,
|
|
6501
|
+
complianceASVS
|
|
6502
|
+
);
|
|
6503
|
+
}
|
|
6256
6504
|
return {
|
|
6257
6505
|
source: "runsec_mcp",
|
|
6506
|
+
transport: "full",
|
|
6258
6507
|
standard: result.standard,
|
|
6259
6508
|
workspace_path: workspacePath,
|
|
6260
6509
|
verdict,
|
|
@@ -6263,7 +6512,7 @@ function slimRunsecJsonForHubUpload(result, reportMetrics, workspacePath, verdic
|
|
|
6263
6512
|
complianceASVS,
|
|
6264
6513
|
report_metrics: slimReportMetricsForHub(reportMetrics),
|
|
6265
6514
|
findings: result.findings.map(slimFindingForHubUpload),
|
|
6266
|
-
findings_suppressed_summary: buildSuppressedSummaryForHub(result.findings_suppressed),
|
|
6515
|
+
findings_suppressed_summary: buildSuppressedSummaryForHub(result.findings_suppressed, true),
|
|
6267
6516
|
findings_suppressed_count: result.findings_suppressed.length,
|
|
6268
6517
|
duration_ms: result.duration_ms,
|
|
6269
6518
|
findings_count: result.findings_count,
|
|
@@ -6274,6 +6523,32 @@ function slimRunsecJsonForHubUpload(result, reportMetrics, workspacePath, verdic
|
|
|
6274
6523
|
verdict_detail: result.verdict
|
|
6275
6524
|
};
|
|
6276
6525
|
}
|
|
6526
|
+
function toMinimalHubPayload(payload) {
|
|
6527
|
+
const runsecJson = payload.runsecJson ?? {};
|
|
6528
|
+
const minimalJson = {
|
|
6529
|
+
source: "runsec_mcp",
|
|
6530
|
+
transport: "minimal",
|
|
6531
|
+
workspace_path: runsecJson.workspace_path,
|
|
6532
|
+
standard: runsecJson.standard,
|
|
6533
|
+
verdict: payload.verdict,
|
|
6534
|
+
metrics: payload.metrics,
|
|
6535
|
+
compliance: runsecJson.compliance,
|
|
6536
|
+
complianceASVS: runsecJson.complianceASVS,
|
|
6537
|
+
cognitive: runsecJson.cognitive,
|
|
6538
|
+
findings_suppressed_summary: runsecJson.findings_suppressed_summary,
|
|
6539
|
+
findings_suppressed_count: runsecJson.findings_suppressed_count,
|
|
6540
|
+
duration_ms: runsecJson.duration_ms,
|
|
6541
|
+
findings_count: runsecJson.findings_count,
|
|
6542
|
+
engine_summary: runsecJson.engine_summary,
|
|
6543
|
+
verdict_detail: runsecJson.verdict_detail
|
|
6544
|
+
};
|
|
6545
|
+
return {
|
|
6546
|
+
projectId: payload.projectId,
|
|
6547
|
+
verdict: payload.verdict,
|
|
6548
|
+
metrics: payload.metrics,
|
|
6549
|
+
runsecJson: minimalJson
|
|
6550
|
+
};
|
|
6551
|
+
}
|
|
6277
6552
|
function logHubSyncFailure(message, error, targetUrl) {
|
|
6278
6553
|
if (error != null && targetUrl) {
|
|
6279
6554
|
dumpHubNetworkError(error, targetUrl);
|
|
@@ -6427,39 +6702,44 @@ async function uploadScanResultsToHub(payload, apiKey) {
|
|
|
6427
6702
|
console.warn("[runsec] Hub telemetry skipped: API key missing");
|
|
6428
6703
|
return { success: false, message: "Sync failed: API key missing" };
|
|
6429
6704
|
}
|
|
6430
|
-
const
|
|
6431
|
-
const
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
const
|
|
6439
|
-
if (
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
} catch (err) {
|
|
6449
|
-
lastError = err;
|
|
6450
|
-
errors.push(err);
|
|
6451
|
-
dumpHubNetworkError(err, tryUrl);
|
|
6452
|
-
}
|
|
6453
|
-
}
|
|
6454
|
-
throw lastError;
|
|
6455
|
-
};
|
|
6705
|
+
const primaryUrl = resolveHubUploadUrl();
|
|
6706
|
+
const uploadUrls = hubUploadUrlCandidates(primaryUrl);
|
|
6707
|
+
const payloadVariants = [
|
|
6708
|
+
{ label: "minimal", body: payload },
|
|
6709
|
+
{ label: "minimal-fallback", body: toMinimalHubPayload(payload) }
|
|
6710
|
+
];
|
|
6711
|
+
const seenPayloads = /* @__PURE__ */ new Set();
|
|
6712
|
+
const uniqueVariants = payloadVariants.filter((variant) => {
|
|
6713
|
+
const key = JSON.stringify(variant.body);
|
|
6714
|
+
if (seenPayloads.has(key)) return false;
|
|
6715
|
+
seenPayloads.add(key);
|
|
6716
|
+
return true;
|
|
6717
|
+
});
|
|
6718
|
+
console.error(
|
|
6719
|
+
`[runsec] Hub telemetry upload targets: ${uploadUrls.join(" \u2192 ")} (${uniqueVariants[0] ? Buffer.byteLength(JSON.stringify(uniqueVariants[0].body), "utf8") : 0} bytes, mode=${String(uniqueVariants[0]?.body.runsecJson.transport ?? "minimal")})`
|
|
6720
|
+
);
|
|
6721
|
+
let lastNetworkError = new Error("No upload attempts made");
|
|
6722
|
+
let lastTryUrl = primaryUrl;
|
|
6456
6723
|
try {
|
|
6457
6724
|
let response;
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6725
|
+
outer: for (const variant of uniqueVariants) {
|
|
6726
|
+
for (const tryUrl of uploadUrls) {
|
|
6727
|
+
lastTryUrl = tryUrl;
|
|
6728
|
+
const bytes = Buffer.byteLength(JSON.stringify(variant.body), "utf8");
|
|
6729
|
+
console.error(`[runsec] Hub upload attempt (${variant.label}, ${bytes} bytes) \u2192 ${tryUrl}`);
|
|
6730
|
+
try {
|
|
6731
|
+
response = await postHubUpload(tryUrl, trimmedKey, variant.body);
|
|
6732
|
+
break outer;
|
|
6733
|
+
} catch (err) {
|
|
6734
|
+
lastNetworkError = err;
|
|
6735
|
+
dumpHubNetworkError(err, tryUrl);
|
|
6736
|
+
if (!isRetryableHubNetworkError(err)) break;
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
if (!response) {
|
|
6741
|
+
const message = formatHubSyncErrorMessage(lastNetworkError, lastTryUrl);
|
|
6742
|
+
logHubSyncFailure(message, lastNetworkError, lastTryUrl);
|
|
6463
6743
|
return { success: false, message };
|
|
6464
6744
|
}
|
|
6465
6745
|
if (!response.ok) {
|
|
@@ -6469,11 +6749,11 @@ async function uploadScanResultsToHub(payload, apiKey) {
|
|
|
6469
6749
|
const httpMessage = detailSnippet || statusLabel;
|
|
6470
6750
|
const message = formatHubSyncErrorMessage(
|
|
6471
6751
|
new Error(httpMessage),
|
|
6472
|
-
|
|
6752
|
+
lastTryUrl,
|
|
6473
6753
|
response,
|
|
6474
6754
|
detailSnippet
|
|
6475
6755
|
);
|
|
6476
|
-
logHubSyncFailure(message, new Error(httpMessage),
|
|
6756
|
+
logHubSyncFailure(message, new Error(httpMessage), lastTryUrl);
|
|
6477
6757
|
return {
|
|
6478
6758
|
success: false,
|
|
6479
6759
|
message
|
|
@@ -6492,8 +6772,8 @@ async function uploadScanResultsToHub(payload, apiKey) {
|
|
|
6492
6772
|
projectId
|
|
6493
6773
|
};
|
|
6494
6774
|
} catch (error) {
|
|
6495
|
-
const message = formatHubSyncErrorMessage(error,
|
|
6496
|
-
logHubSyncFailure(message, error,
|
|
6775
|
+
const message = formatHubSyncErrorMessage(error, lastTryUrl);
|
|
6776
|
+
logHubSyncFailure(message, error, lastTryUrl);
|
|
6497
6777
|
return { success: false, message };
|
|
6498
6778
|
}
|
|
6499
6779
|
}
|