@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.
Files changed (2) hide show
  1. package/dist/index.js +501 -221
  2. 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
- if (sev === "CRITICAL" || sev === "ERROR") score = 0.92;
969
- else if (sev === "WARNING" || sev === "HIGH") score = 0.84;
970
- else score = 0.55;
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") return "Unknown CWE";
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}: ${UNKNOWN_CWE_TYPE_LABEL}`;
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 cwe = semgrepCwe !== "UNKNOWN" ? semgrepCwe : registryRule?.cwe && registryRule.cwe !== "UNKNOWN" ? registryRule.cwe : fallbackMeta.cwe;
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: formatCweDisplay(cwe),
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: `runsec.secrets.trufflehog.${detector.toLowerCase().replace(/\s+/g, "-")}`,
1877
- cwe: "CWE-798",
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") || rule.includes("secrets")) return "secrets";
3390
- if (rule.includes("syft") || rule.includes("dependencies")) return "dependencies";
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
- const out = [];
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
- const bucket = byReason.get(key);
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(`## Appendix: Cognitively suppressed findings (${suppressed.length})`);
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
- "These TruffleHog/Semgrep hits were processed by the cognitive engine and scored below the primary report threshold (0.8). They do **not** change `X-RunSec-Verdict`."
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].length - a[1].length);
3566
- for (const [reason, rows] of sortedReasons) {
3567
- out.push(`| \`${safeText(reason)}\` | ${rows.length} |`);
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
- if (rows.length === 0) {
3605
- out.push("_No findings to summarize by file type._");
3606
- return out;
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: f.cwe != null && String(f.cwe).trim() !== "" ? String(f.cwe) : "UNKNOWN",
3630
- cwe_name: String(f.cwe_name || f.cwe || "Unknown CWE"),
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" ? `Confidence: ${group.confidence_score.toFixed(2)} | ` : "";
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 stackInline = formatStackTagsInline(group.stackTags);
3719
- const cvssLine = typeof group.cvss_score === "number" ? `**CVSS:** ${group.cvss_score.toFixed(1)}${group.cvss_vector ? ` (\`${safeText(group.cvss_vector)}\`)` : ""}` : "";
3720
- const asvsLevelText = formatAsvsLevelDisplay(group.asvsLevel);
3721
- const asvsLine = asvsLevelText && group.asvsTrace ? `**ASVS:** ${asvsLevelText} (${safeText(group.asvsTrace)})` : asvsLevelText ? `**ASVS:** ${asvsLevelText}` : group.asvsTrace ? `**ASVS:** ${safeText(group.asvsTrace)}` : "";
3722
- out.push(
3723
- `**[${normalizeSeverity(group.severity)}]** \`${safeText(group.rule_id)}\` (${metric})`
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(safeText(group.description));
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("**Affected Files:**");
3741
- for (const occ of group.occurrences) {
3742
- out.push(`- \`${safeText(occ.file_path)}:${occ.line}\``);
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
- const example = group.exampleOccurrence;
3746
- if (example) {
3870
+ if (example && isUsableSnippet(example.snippet)) {
3747
3871
  const lang = snippetLanguage(example.file_path);
3748
- out.push(`**Example Snippet** (from \`${safeText(example.file_path)}:${example.line}\`):`);
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
- out.push("### Remediation");
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(remediationText);
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 Unified Security Report`);
3944
+ out.push(`# RunSec Security Report`);
3780
3945
  out.push("");
3781
- out.push(`**Generated at:** ${(/* @__PURE__ */ new Date()).toISOString()}`);
3782
- out.push(`**MCP package:** \`@runsec/mcp@${safeText(RUNSEC_MCP_VERSION)}\``);
3783
- out.push(`**Standard:** ${safeText(standard)}`);
3784
- out.push(`**X-RunSec-Verdict:** \`${safeText(verdictLabel)}\`${metrics.verdict?.is_safe === false && metrics.verdict.fail_reason ? ` \u2014 ${safeText(metrics.verdict.fail_reason)}` : ""}`);
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
- `**Scan time:** ${Number(metrics.duration_ms || 0)}ms | **Files scanned:** ${Number(metrics.scanned_files_count || 0)} | **Rules:** ${Number(metrics.total_rules || 0)}`
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
- `**Engines (concurrent):** Semgrep \`${safeText(metrics.engines.semgrep ?? "n/a")}\` | TruffleHog \`${safeText(metrics.engines.trufflehog ?? "n/a")}\` | Syft \`${safeText(metrics.engines.syft ?? "n/a")}\`${es?.concurrent_duration_ms != null ? ` | **Wall time:** ${es.concurrent_duration_ms}ms` : ""}`
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
- `**Raw engine counts (pre-cognitive):** Code ${es.semgrep?.finding_count ?? 0} | Secrets ${rawEngineCountLabel("trufflehog", es)} | Dependencies ${rawEngineCountLabel("syft", es)}`
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 summary");
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
- `- **Code Vulnerabilities:** ${byCategory.code.length} | **Exposed Secrets:** ${executiveCategoryLabel("secrets", byCategory.secrets.length, es)} | **Vulnerable Dependencies:** ${executiveCategoryLabel("dependencies", byCategory.dependencies.length, es)}`
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
- `- **Primary findings (cognitive-filtered):** ${findings.length} | **Suppressed (.runsecignore):** ${Number(metrics.suppressed_fp_count || 0)} | **Suppressed (cognitive):** ${Number(metrics.cognitive_suppressed_count ?? suppressedRows.length)}`
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(...renderTechStackSummary([...findings, ...suppressedRows]));
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>\n");
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: check network/VPN/firewall, retry the scan, or set RUNSEC_HUB_URL to your Hub origin (NEXT_PUBLIC_APP_URL).";
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
- return { total: findings.length, by_reason: byReason, locations };
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 url = resolveHubUploadUrl();
6431
- const payloadBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
6432
- console.error(`[runsec] Hub telemetry upload \u2192 ${url} (${payloadBytes} bytes)`);
6433
- const attemptUpload = async (targetUrl) => {
6434
- const errors = [];
6435
- const urls = [targetUrl];
6436
- const localhostAlt = localhostAlternateUploadUrl(targetUrl);
6437
- if (localhostAlt && localhostAlt !== targetUrl) urls.push(localhostAlt);
6438
- const dockerAlt = dockerInternalAlternateUploadUrl(targetUrl);
6439
- if (dockerAlt && !urls.includes(dockerAlt)) urls.push(dockerAlt);
6440
- let lastError = new Error("No upload attempts made");
6441
- for (let i = 0; i < urls.length; i++) {
6442
- const tryUrl = urls[i];
6443
- if (i > 0) {
6444
- console.error(`[runsec] Hub telemetry retry \u2192 ${tryUrl}`);
6445
- }
6446
- try {
6447
- return await postHubUpload(tryUrl, trimmedKey, payload);
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
- try {
6459
- response = await attemptUpload(url);
6460
- } catch (networkError) {
6461
- const message = formatHubSyncErrorMessage(networkError, url);
6462
- logHubSyncFailure(message, networkError, url);
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
- url,
6752
+ lastTryUrl,
6473
6753
  response,
6474
6754
  detailSnippet
6475
6755
  );
6476
- logHubSyncFailure(message, new Error(httpMessage), url);
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, url);
6496
- logHubSyncFailure(message, error, url);
6775
+ const message = formatHubSyncErrorMessage(error, lastTryUrl);
6776
+ logHubSyncFailure(message, error, lastTryUrl);
6497
6777
  return { success: false, message };
6498
6778
  }
6499
6779
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runsec/mcp",
3
- "version": "1.0.87",
3
+ "version": "1.0.89",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "package.json",