@runsec/mcp 1.0.84 → 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 +154 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3285,7 +3285,8 @@ function buildAuditReportMetrics(result) {
3285
3285
  raw_engines: result.raw_engines,
3286
3286
  cwe_counts: cweCounts,
3287
3287
  verdict: result.verdict,
3288
- cognitive: result.cognitive
3288
+ cognitive: result.cognitive,
3289
+ findings_suppressed: result.findings_suppressed
3289
3290
  };
3290
3291
  }
3291
3292
  async function executeAudit(toolName, args) {
@@ -3534,6 +3535,67 @@ function countFindingsByTechStack(findings) {
3534
3535
  }
3535
3536
  return Array.from(counts.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
3536
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
+ }
3537
3599
  function renderTechStackSummary(findings) {
3538
3600
  const rows = countFindingsByTechStack(findings);
3539
3601
  const out = [];
@@ -3742,11 +3804,18 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
3742
3804
  out.push(
3743
3805
  `- **Code Vulnerabilities:** ${byCategory.code.length} | **Exposed Secrets:** ${executiveCategoryLabel("secrets", byCategory.secrets.length, es)} | **Vulnerable Dependencies:** ${executiveCategoryLabel("dependencies", byCategory.dependencies.length, es)}`
3744
3806
  );
3807
+ const suppressedRows = Array.isArray(metrics.findings_suppressed) ? metrics.findings_suppressed : [];
3808
+ const rawSecrets = es?.trufflehog?.finding_count ?? metrics.cognitive?.findings_total ?? 0;
3745
3809
  out.push(
3746
- `- **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)}`
3747
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
+ }
3748
3817
  out.push("");
3749
- out.push(...renderTechStackSummary(findings));
3818
+ out.push(...renderTechStackSummary([...findings, ...suppressedRows]));
3750
3819
  out.push("");
3751
3820
  let sectionNum = 1;
3752
3821
  for (const category of CATEGORY_ORDER) {
@@ -3759,6 +3828,7 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
3759
3828
  out.push(...renderFindingRows(rows));
3760
3829
  sectionNum += 1;
3761
3830
  }
3831
+ out.push(...renderSuppressedFindingsSection(suppressedRows));
3762
3832
  out.push("---");
3763
3833
  out.push("<details><summary>Telemetry (machine)</summary>\n");
3764
3834
  out.push("```json");
@@ -6058,6 +6128,17 @@ function buildHubComplianceBlock(result) {
6058
6128
 
6059
6129
  // src/telemetryClient.ts
6060
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
+ }
6061
6142
  function hubAuthHeaders(apiKey) {
6062
6143
  return {
6063
6144
  Authorization: `Bearer ${apiKey}`,
@@ -6112,11 +6193,47 @@ function formatHubSyncErrorMessage(error, targetUrl, response, responseBody) {
6112
6193
  if (responseBody?.trim()) {
6113
6194
  message += `. Body: ${responseBody.trim().slice(0, 300)}`;
6114
6195
  }
6115
- if (status === "n/a" && /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|CERT|SSL|TLS/i.test(err.message + code)) {
6116
- 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).";
6117
6198
  }
6118
6199
  return message;
6119
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
+ }
6120
6237
  function logHubSyncFailure(message, error, targetUrl) {
6121
6238
  if (error != null && targetUrl) {
6122
6239
  dumpHubNetworkError(error, targetUrl);
@@ -6146,7 +6263,8 @@ function postHubUploadNodeHttp(url, apiKey, payload) {
6146
6263
  path: `${parsed.pathname}${parsed.search}`,
6147
6264
  method: "POST",
6148
6265
  headers,
6149
- timeout: HUB_UPLOAD_TIMEOUT_MS
6266
+ timeout: HUB_UPLOAD_TIMEOUT_MS,
6267
+ agent: lib === import_node_https.default ? HUB_HTTPS_AGENT : void 0
6150
6268
  },
6151
6269
  (res) => {
6152
6270
  const chunks = [];
@@ -6174,11 +6292,12 @@ function postHubUploadNodeHttp(url, apiKey, payload) {
6174
6292
  });
6175
6293
  }
6176
6294
  function preferNodeHttpUpload() {
6177
- if (process.platform === "win32") return true;
6295
+ if (process.env.VITEST === "true" || process.env.RUNSEC_HUB_USE_FETCH === "1") return false;
6178
6296
  if (process.env.RUNSEC_HUB_USE_NODE_HTTP === "1") return true;
6297
+ if (process.platform === "win32") return true;
6179
6298
  return false;
6180
6299
  }
6181
- async function postHubUpload(url, apiKey, payload) {
6300
+ async function postHubUploadOnce(url, apiKey, payload) {
6182
6301
  if (preferNodeHttpUpload()) {
6183
6302
  return postHubUploadNodeHttp(url, apiKey, payload);
6184
6303
  }
@@ -6195,6 +6314,25 @@ async function postHubUpload(url, apiKey, payload) {
6195
6314
  return postHubUploadNodeHttp(url, apiKey, payload);
6196
6315
  }
6197
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
+ }
6198
6336
  function countSeverityMetrics(findings) {
6199
6337
  const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
6200
6338
  for (const f of findings) {
@@ -6218,24 +6356,15 @@ function buildHubUploadPayload(result, reportMetrics, workspacePath, projectId)
6218
6356
  const metrics = countSeverityMetrics(result.findings);
6219
6357
  const verdict = resolveScanVerdict(result);
6220
6358
  const { compliance, complianceASVS } = buildHubComplianceBlock(result);
6221
- const runsecJson = {
6222
- source: "runsec_mcp",
6223
- standard: result.standard,
6224
- workspace_path: workspacePath,
6359
+ const runsecJson = slimRunsecJsonForHubUpload(
6360
+ result,
6361
+ reportMetrics,
6362
+ workspacePath,
6225
6363
  verdict,
6226
6364
  metrics,
6227
6365
  compliance,
6228
- complianceASVS,
6229
- audit: result,
6230
- report_metrics: reportMetrics,
6231
- findings: result.findings,
6232
- findings_suppressed: result.findings_suppressed,
6233
- duration_ms: result.duration_ms,
6234
- findings_count: result.findings_count,
6235
- engines: result.engines,
6236
- engine_summary: result.engine_summary,
6237
- cognitive: result.cognitive
6238
- };
6366
+ complianceASVS
6367
+ );
6239
6368
  const resolvedProjectId = projectId?.trim() || process.env.RUNSEC_PROJECT_ID?.trim() || void 0;
6240
6369
  return {
6241
6370
  projectId: resolvedProjectId,
@@ -6251,7 +6380,8 @@ async function uploadScanResultsToHub(payload, apiKey) {
6251
6380
  return { success: false, message: "Sync failed: API key missing" };
6252
6381
  }
6253
6382
  const url = resolveHubUploadUrl();
6254
- 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)`);
6255
6385
  const attemptUpload = async (targetUrl) => {
6256
6386
  const errors = [];
6257
6387
  const urls = [targetUrl];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runsec/mcp",
3
- "version": "1.0.84",
3
+ "version": "1.0.85",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "package.json",