@runsec/mcp 1.0.83 → 1.0.85

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 +155 -34
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1239,7 +1239,6 @@ function applyCognitivePipeline(workspaceRoot, findings) {
1239
1239
  (f) => f.suppressed || isNuclearSuppressedFinding(f) || !f.primary_log_eligible
1240
1240
  )
1241
1241
  ).concat(duplicates);
1242
- const allScored = [...primary, ...suppressed];
1243
1242
  console.error(
1244
1243
  `[runsec] cognitive: raw=${findings.length} nuclear=${nuclearSuppressed.length} primary=${primary.length} suppressed=${suppressed.length}`
1245
1244
  );
@@ -1249,7 +1248,7 @@ function applyCognitivePipeline(workspaceRoot, findings) {
1249
1248
  summary: {
1250
1249
  version: "v1.0",
1251
1250
  primary_log_threshold: PRIMARY_LOG_THRESHOLD,
1252
- findings_total: allScored.length,
1251
+ findings_total: findings.length,
1253
1252
  findings_primary: primary.length,
1254
1253
  findings_suppressed: suppressed.length,
1255
1254
  false_positive_filtering: true
@@ -1871,14 +1870,6 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
1871
1870
  const rawSecret = String(raw.Raw ?? "").trim();
1872
1871
  const display = redacted || rawSecret || "[secret redacted]";
1873
1872
  const description = `TruffleHog: exposed ${detector}${verified ? " (verified)" : ""}`;
1874
- if (!isTrufflehogVerified(verified, description)) {
1875
- const blob = `${display} ${rawSecret} ${description}`;
1876
- if (isLockfileOrModulesPath(rel) || isStaticLayoutDumpPath(rel)) continue;
1877
- if (hasEnvironmentInterpolation(blob)) continue;
1878
- if (blobHasDevDatabaseSecret(blob)) continue;
1879
- if (isHexChecksumBlob(display) || isHexChecksumBlob(rawSecret)) continue;
1880
- if (isUnverifiedTrufflehogNoiseDetector(detector)) continue;
1881
- }
1882
1873
  const severity = severityForSecret(detector, verified);
1883
1874
  findings.push({
1884
1875
  category: "secrets",
@@ -3294,7 +3285,8 @@ function buildAuditReportMetrics(result) {
3294
3285
  raw_engines: result.raw_engines,
3295
3286
  cwe_counts: cweCounts,
3296
3287
  verdict: result.verdict,
3297
- cognitive: result.cognitive
3288
+ cognitive: result.cognitive,
3289
+ findings_suppressed: result.findings_suppressed
3298
3290
  };
3299
3291
  }
3300
3292
  async function executeAudit(toolName, args) {
@@ -3543,6 +3535,67 @@ function countFindingsByTechStack(findings) {
3543
3535
  }
3544
3536
  return Array.from(counts.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
3545
3537
  }
3538
+ function suppressionReasonLabel(finding) {
3539
+ const reason = String(finding.suppression_reason ?? "").trim();
3540
+ if (reason) return reason;
3541
+ const conf = finding.confidence_score;
3542
+ if (typeof conf === "number" && conf <= 0.01) return "nuclear_hard_drop";
3543
+ if (typeof conf === "number" && conf < 0.8) return "cognitive_below_threshold";
3544
+ return "cognitive_suppressed";
3545
+ }
3546
+ function renderSuppressedFindingsSection(suppressed) {
3547
+ const out = [];
3548
+ if (suppressed.length === 0) return out;
3549
+ const byReason = /* @__PURE__ */ new Map();
3550
+ for (const row of suppressed) {
3551
+ const key = suppressionReasonLabel(row);
3552
+ const bucket = byReason.get(key);
3553
+ if (bucket) bucket.push(row);
3554
+ else byReason.set(key, [row]);
3555
+ }
3556
+ out.push("---");
3557
+ out.push(`## Appendix: Cognitively suppressed findings (${suppressed.length})`);
3558
+ out.push("");
3559
+ 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`."
3561
+ );
3562
+ out.push("");
3563
+ out.push("| Suppression reason | Count |");
3564
+ 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
+ }
3596
+ }
3597
+ return out;
3598
+ }
3546
3599
  function renderTechStackSummary(findings) {
3547
3600
  const rows = countFindingsByTechStack(findings);
3548
3601
  const out = [];
@@ -3751,11 +3804,18 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
3751
3804
  out.push(
3752
3805
  `- **Code Vulnerabilities:** ${byCategory.code.length} | **Exposed Secrets:** ${executiveCategoryLabel("secrets", byCategory.secrets.length, es)} | **Vulnerable Dependencies:** ${executiveCategoryLabel("dependencies", byCategory.dependencies.length, es)}`
3753
3806
  );
3807
+ const suppressedRows = Array.isArray(metrics.findings_suppressed) ? metrics.findings_suppressed : [];
3808
+ const rawSecrets = es?.trufflehog?.finding_count ?? metrics.cognitive?.findings_total ?? 0;
3754
3809
  out.push(
3755
- `- **Primary findings (cognitive-filtered):** ${findings.length} | **Suppressed (.runsecignore):** ${Number(metrics.suppressed_fp_count || 0)} | **Suppressed (cognitive):** ${Number(metrics.cognitive_suppressed_count || 0)}`
3810
+ `- **Primary findings (cognitive-filtered):** ${findings.length} | **Suppressed (.runsecignore):** ${Number(metrics.suppressed_fp_count || 0)} | **Suppressed (cognitive):** ${Number(metrics.cognitive_suppressed_count ?? suppressedRows.length)}`
3756
3811
  );
3812
+ 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
+ );
3816
+ }
3757
3817
  out.push("");
3758
- out.push(...renderTechStackSummary(findings));
3818
+ out.push(...renderTechStackSummary([...findings, ...suppressedRows]));
3759
3819
  out.push("");
3760
3820
  let sectionNum = 1;
3761
3821
  for (const category of CATEGORY_ORDER) {
@@ -3768,6 +3828,7 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
3768
3828
  out.push(...renderFindingRows(rows));
3769
3829
  sectionNum += 1;
3770
3830
  }
3831
+ out.push(...renderSuppressedFindingsSection(suppressedRows));
3771
3832
  out.push("---");
3772
3833
  out.push("<details><summary>Telemetry (machine)</summary>\n");
3773
3834
  out.push("```json");
@@ -6067,6 +6128,17 @@ function buildHubComplianceBlock(result) {
6067
6128
 
6068
6129
  // src/telemetryClient.ts
6069
6130
  var HUB_UPLOAD_TIMEOUT_MS = 12e4;
6131
+ var HUB_UPLOAD_MAX_RETRIES = 4;
6132
+ var HUB_HTTPS_AGENT = new import_node_https.default.Agent({ keepAlive: true, maxSockets: 4, timeout: HUB_UPLOAD_TIMEOUT_MS });
6133
+ function sleepMs(ms) {
6134
+ return new Promise((resolve) => setTimeout(resolve, ms));
6135
+ }
6136
+ function isRetryableHubNetworkError(error) {
6137
+ const code = networkErrorCode(error);
6138
+ const message = error instanceof Error ? error.message : String(error);
6139
+ const blob = `${code} ${message}`;
6140
+ return /ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|ENOTFOUND|ECONNABORTED|socket hang up|network/i.test(blob);
6141
+ }
6070
6142
  function hubAuthHeaders(apiKey) {
6071
6143
  return {
6072
6144
  Authorization: `Bearer ${apiKey}`,
@@ -6121,11 +6193,47 @@ function formatHubSyncErrorMessage(error, targetUrl, response, responseBody) {
6121
6193
  if (responseBody?.trim()) {
6122
6194
  message += `. Body: ${responseBody.trim().slice(0, 300)}`;
6123
6195
  }
6124
- if (status === "n/a" && /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|CERT|SSL|TLS/i.test(err.message + code)) {
6125
- message += ". Hint: set RUNSEC_HUB_URL to your production Hub origin (same value as NEXT_PUBLIC_APP_URL on the Hub server).";
6196
+ if (status === "n/a" && /fetch failed|ECONNRESET|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|CERT|SSL|TLS/i.test(err.message + code)) {
6197
+ message += ". Hint: check network/VPN/firewall, retry the scan, or set RUNSEC_HUB_URL to your Hub origin (NEXT_PUBLIC_APP_URL).";
6126
6198
  }
6127
6199
  return message;
6128
6200
  }
6201
+ function slimFindingForHubUpload(finding) {
6202
+ return {
6203
+ category: finding.category,
6204
+ rule_id: finding.rule_id,
6205
+ severity: finding.severity,
6206
+ file_path: finding.file_path,
6207
+ line: finding.line,
6208
+ detector_name: finding.detector_name,
6209
+ description: finding.description?.slice(0, 240),
6210
+ match_text: finding.match_text?.slice(0, 120),
6211
+ confidence_score: finding.confidence_score,
6212
+ suppressed: finding.suppressed,
6213
+ suppression_reason: finding.suppression_reason
6214
+ };
6215
+ }
6216
+ function slimRunsecJsonForHubUpload(result, reportMetrics, workspacePath, verdict, metrics, compliance, complianceASVS) {
6217
+ return {
6218
+ source: "runsec_mcp",
6219
+ standard: result.standard,
6220
+ workspace_path: workspacePath,
6221
+ verdict,
6222
+ metrics,
6223
+ compliance,
6224
+ complianceASVS,
6225
+ report_metrics: reportMetrics,
6226
+ findings: result.findings.map(slimFindingForHubUpload),
6227
+ findings_suppressed: result.findings_suppressed.map(slimFindingForHubUpload),
6228
+ duration_ms: result.duration_ms,
6229
+ findings_count: result.findings_count,
6230
+ cognitive_suppressed_count: result.cognitive_suppressed_count,
6231
+ engines: result.engines,
6232
+ engine_summary: result.engine_summary,
6233
+ cognitive: result.cognitive,
6234
+ verdict_detail: result.verdict
6235
+ };
6236
+ }
6129
6237
  function logHubSyncFailure(message, error, targetUrl) {
6130
6238
  if (error != null && targetUrl) {
6131
6239
  dumpHubNetworkError(error, targetUrl);
@@ -6155,7 +6263,8 @@ function postHubUploadNodeHttp(url, apiKey, payload) {
6155
6263
  path: `${parsed.pathname}${parsed.search}`,
6156
6264
  method: "POST",
6157
6265
  headers,
6158
- timeout: HUB_UPLOAD_TIMEOUT_MS
6266
+ timeout: HUB_UPLOAD_TIMEOUT_MS,
6267
+ agent: lib === import_node_https.default ? HUB_HTTPS_AGENT : void 0
6159
6268
  },
6160
6269
  (res) => {
6161
6270
  const chunks = [];
@@ -6183,11 +6292,12 @@ function postHubUploadNodeHttp(url, apiKey, payload) {
6183
6292
  });
6184
6293
  }
6185
6294
  function preferNodeHttpUpload() {
6186
- if (process.platform === "win32") return true;
6295
+ if (process.env.VITEST === "true" || process.env.RUNSEC_HUB_USE_FETCH === "1") return false;
6187
6296
  if (process.env.RUNSEC_HUB_USE_NODE_HTTP === "1") return true;
6297
+ if (process.platform === "win32") return true;
6188
6298
  return false;
6189
6299
  }
6190
- async function postHubUpload(url, apiKey, payload) {
6300
+ async function postHubUploadOnce(url, apiKey, payload) {
6191
6301
  if (preferNodeHttpUpload()) {
6192
6302
  return postHubUploadNodeHttp(url, apiKey, payload);
6193
6303
  }
@@ -6204,6 +6314,25 @@ async function postHubUpload(url, apiKey, payload) {
6204
6314
  return postHubUploadNodeHttp(url, apiKey, payload);
6205
6315
  }
6206
6316
  }
6317
+ async function postHubUpload(url, apiKey, payload) {
6318
+ let lastError = new Error("Hub upload failed");
6319
+ for (let attempt = 0; attempt < HUB_UPLOAD_MAX_RETRIES; attempt++) {
6320
+ try {
6321
+ return await postHubUploadOnce(url, apiKey, payload);
6322
+ } catch (error) {
6323
+ lastError = error;
6324
+ if (attempt >= HUB_UPLOAD_MAX_RETRIES - 1 || !isRetryableHubNetworkError(error)) {
6325
+ throw error;
6326
+ }
6327
+ const delayMs = 1e3 * 2 ** attempt;
6328
+ console.error(
6329
+ `[runsec] Hub upload retry ${attempt + 2}/${HUB_UPLOAD_MAX_RETRIES} in ${delayMs}ms (${networkErrorCode(error) || "network"})`
6330
+ );
6331
+ await sleepMs(delayMs);
6332
+ }
6333
+ }
6334
+ throw lastError;
6335
+ }
6207
6336
  function countSeverityMetrics(findings) {
6208
6337
  const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
6209
6338
  for (const f of findings) {
@@ -6227,24 +6356,15 @@ function buildHubUploadPayload(result, reportMetrics, workspacePath, projectId)
6227
6356
  const metrics = countSeverityMetrics(result.findings);
6228
6357
  const verdict = resolveScanVerdict(result);
6229
6358
  const { compliance, complianceASVS } = buildHubComplianceBlock(result);
6230
- const runsecJson = {
6231
- source: "runsec_mcp",
6232
- standard: result.standard,
6233
- workspace_path: workspacePath,
6359
+ const runsecJson = slimRunsecJsonForHubUpload(
6360
+ result,
6361
+ reportMetrics,
6362
+ workspacePath,
6234
6363
  verdict,
6235
6364
  metrics,
6236
6365
  compliance,
6237
- complianceASVS,
6238
- audit: result,
6239
- report_metrics: reportMetrics,
6240
- findings: result.findings,
6241
- findings_suppressed: result.findings_suppressed,
6242
- duration_ms: result.duration_ms,
6243
- findings_count: result.findings_count,
6244
- engines: result.engines,
6245
- engine_summary: result.engine_summary,
6246
- cognitive: result.cognitive
6247
- };
6366
+ complianceASVS
6367
+ );
6248
6368
  const resolvedProjectId = projectId?.trim() || process.env.RUNSEC_PROJECT_ID?.trim() || void 0;
6249
6369
  return {
6250
6370
  projectId: resolvedProjectId,
@@ -6260,7 +6380,8 @@ async function uploadScanResultsToHub(payload, apiKey) {
6260
6380
  return { success: false, message: "Sync failed: API key missing" };
6261
6381
  }
6262
6382
  const url = resolveHubUploadUrl();
6263
- console.error(`[runsec] Hub telemetry upload \u2192 ${url}`);
6383
+ const payloadBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
6384
+ console.error(`[runsec] Hub telemetry upload \u2192 ${url} (${payloadBytes} bytes)`);
6264
6385
  const attemptUpload = async (targetUrl) => {
6265
6386
  const errors = [];
6266
6387
  const urls = [targetUrl];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runsec/mcp",
3
- "version": "1.0.83",
3
+ "version": "1.0.85",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "package.json",