@runsec/mcp 1.0.88 → 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 +377 -181
- 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)}\` | ${
|
|
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} |`);
|
|
3568
3714
|
}
|
|
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
|
-
}
|
|
3596
|
-
}
|
|
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(
|