@runsec/mcp 1.0.84 → 1.0.87

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 +215 -37
  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,18 @@ 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_WRITE_CHUNK_BYTES = 16 * 1024;
6133
+ var HUB_HTTPS_AGENT = new import_node_https.default.Agent({ keepAlive: false, maxSockets: 2, timeout: HUB_UPLOAD_TIMEOUT_MS });
6134
+ function sleepMs(ms) {
6135
+ return new Promise((resolve) => setTimeout(resolve, ms));
6136
+ }
6137
+ function isRetryableHubNetworkError(error) {
6138
+ const code = networkErrorCode(error);
6139
+ const message = error instanceof Error ? error.message : String(error);
6140
+ const blob = `${code} ${message}`;
6141
+ return /ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|ENOTFOUND|ECONNABORTED|socket hang up|network/i.test(blob);
6142
+ }
6061
6143
  function hubAuthHeaders(apiKey) {
6062
6144
  return {
6063
6145
  Authorization: `Bearer ${apiKey}`,
@@ -6112,11 +6194,86 @@ function formatHubSyncErrorMessage(error, targetUrl, response, responseBody) {
6112
6194
  if (responseBody?.trim()) {
6113
6195
  message += `. Body: ${responseBody.trim().slice(0, 300)}`;
6114
6196
  }
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).";
6197
+ 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).";
6117
6199
  }
6118
6200
  return message;
6119
6201
  }
6202
+ function slimFindingForHubUpload(finding) {
6203
+ return {
6204
+ category: finding.category,
6205
+ rule_id: finding.rule_id,
6206
+ severity: finding.severity,
6207
+ file_path: finding.file_path,
6208
+ line: finding.line,
6209
+ detector_name: finding.detector_name,
6210
+ description: finding.description?.slice(0, 160),
6211
+ confidence_score: finding.confidence_score,
6212
+ suppressed: finding.suppressed,
6213
+ suppression_reason: finding.suppression_reason
6214
+ };
6215
+ }
6216
+ function buildSuppressedSummaryForHub(findings) {
6217
+ const byReason = {};
6218
+ const locations = [];
6219
+ for (const f of findings) {
6220
+ const reason = String(f.suppression_reason ?? "cognitive_suppressed");
6221
+ byReason[reason] = (byReason[reason] ?? 0) + 1;
6222
+ locations.push({
6223
+ file_path: f.file_path,
6224
+ line: f.line ?? 0,
6225
+ rule_id: f.rule_id,
6226
+ detector_name: f.detector_name,
6227
+ suppression_reason: reason,
6228
+ severity: f.severity
6229
+ });
6230
+ }
6231
+ return { total: findings.length, by_reason: byReason, locations };
6232
+ }
6233
+ function slimEngineSummaryForHub(engineSummary) {
6234
+ if (!engineSummary) return void 0;
6235
+ const pick = (row) => row ? {
6236
+ engine: row.engine,
6237
+ status: row.status,
6238
+ finding_count: row.finding_count,
6239
+ duration_ms: row.duration_ms
6240
+ } : void 0;
6241
+ return {
6242
+ concurrent_duration_ms: engineSummary.concurrent_duration_ms,
6243
+ semgrep: pick(engineSummary.semgrep),
6244
+ trufflehog: pick(engineSummary.trufflehog),
6245
+ syft: pick(engineSummary.syft)
6246
+ };
6247
+ }
6248
+ function slimReportMetricsForHub(reportMetrics) {
6249
+ const { findings_suppressed: _fs, raw_engines: _raw, engine_summary, ...rest } = reportMetrics;
6250
+ return {
6251
+ ...rest,
6252
+ engine_summary: slimEngineSummaryForHub(engine_summary)
6253
+ };
6254
+ }
6255
+ function slimRunsecJsonForHubUpload(result, reportMetrics, workspacePath, verdict, metrics, compliance, complianceASVS) {
6256
+ return {
6257
+ source: "runsec_mcp",
6258
+ standard: result.standard,
6259
+ workspace_path: workspacePath,
6260
+ verdict,
6261
+ metrics,
6262
+ compliance,
6263
+ complianceASVS,
6264
+ report_metrics: slimReportMetricsForHub(reportMetrics),
6265
+ findings: result.findings.map(slimFindingForHubUpload),
6266
+ findings_suppressed_summary: buildSuppressedSummaryForHub(result.findings_suppressed),
6267
+ findings_suppressed_count: result.findings_suppressed.length,
6268
+ duration_ms: result.duration_ms,
6269
+ findings_count: result.findings_count,
6270
+ cognitive_suppressed_count: result.cognitive_suppressed_count,
6271
+ engines: result.engines,
6272
+ engine_summary: slimEngineSummaryForHub(result.engine_summary),
6273
+ cognitive: result.cognitive,
6274
+ verdict_detail: result.verdict
6275
+ };
6276
+ }
6120
6277
  function logHubSyncFailure(message, error, targetUrl) {
6121
6278
  if (error != null && targetUrl) {
6122
6279
  dumpHubNetworkError(error, targetUrl);
@@ -6133,9 +6290,11 @@ function postHubUploadNodeHttp(url, apiKey, payload) {
6133
6290
  reject(err);
6134
6291
  return;
6135
6292
  }
6293
+ const bodyBuffer = Buffer.from(body, "utf8");
6136
6294
  const headers = {
6137
6295
  ...hubAuthHeaders(apiKey),
6138
- "Content-Length": String(Buffer.byteLength(body))
6296
+ "Content-Length": String(bodyBuffer.length),
6297
+ Connection: "close"
6139
6298
  };
6140
6299
  const lib = parsed.protocol === "https:" ? import_node_https.default : import_node_http.default;
6141
6300
  const port = parsed.port !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
@@ -6146,7 +6305,8 @@ function postHubUploadNodeHttp(url, apiKey, payload) {
6146
6305
  path: `${parsed.pathname}${parsed.search}`,
6147
6306
  method: "POST",
6148
6307
  headers,
6149
- timeout: HUB_UPLOAD_TIMEOUT_MS
6308
+ timeout: HUB_UPLOAD_TIMEOUT_MS,
6309
+ agent: lib === import_node_https.default ? HUB_HTTPS_AGENT : void 0
6150
6310
  },
6151
6311
  (res) => {
6152
6312
  const chunks = [];
@@ -6169,32 +6329,58 @@ function postHubUploadNodeHttp(url, apiKey, payload) {
6169
6329
  req.destroy();
6170
6330
  reject(Object.assign(new Error("Hub upload request timeout"), { code: "ETIMEDOUT" }));
6171
6331
  });
6172
- req.write(body);
6332
+ for (let offset = 0; offset < bodyBuffer.length; offset += HUB_WRITE_CHUNK_BYTES) {
6333
+ req.write(bodyBuffer.subarray(offset, offset + HUB_WRITE_CHUNK_BYTES));
6334
+ }
6173
6335
  req.end();
6174
6336
  });
6175
6337
  }
6176
- function preferNodeHttpUpload() {
6177
- if (process.platform === "win32") return true;
6178
- if (process.env.RUNSEC_HUB_USE_NODE_HTTP === "1") return true;
6179
- return false;
6338
+ async function postHubUploadFetch(url, apiKey, payload) {
6339
+ return await fetch(url, {
6340
+ method: "POST",
6341
+ headers: hubAuthHeaders(apiKey),
6342
+ body: JSON.stringify(payload),
6343
+ signal: AbortSignal.timeout(HUB_UPLOAD_TIMEOUT_MS)
6344
+ });
6180
6345
  }
6181
- async function postHubUpload(url, apiKey, payload) {
6182
- if (preferNodeHttpUpload()) {
6183
- return postHubUploadNodeHttp(url, apiKey, payload);
6346
+ async function postHubUploadOnce(url, apiKey, payload) {
6347
+ const useNodeFirst = process.env.RUNSEC_HUB_USE_NODE_HTTP === "1";
6348
+ if (useNodeFirst) {
6349
+ try {
6350
+ return await postHubUploadNodeHttp(url, apiKey, payload);
6351
+ } catch (nodeError) {
6352
+ console.error("[runsec] node:http(s) upload failed \u2014 retrying with fetch()");
6353
+ dumpHubNetworkError(nodeError, url);
6354
+ return postHubUploadFetch(url, apiKey, payload);
6355
+ }
6184
6356
  }
6185
6357
  try {
6186
- return await fetch(url, {
6187
- method: "POST",
6188
- headers: hubAuthHeaders(apiKey),
6189
- body: JSON.stringify(payload),
6190
- signal: AbortSignal.timeout(HUB_UPLOAD_TIMEOUT_MS)
6191
- });
6358
+ return await postHubUploadFetch(url, apiKey, payload);
6192
6359
  } catch (fetchError) {
6193
6360
  console.error("[runsec] fetch() failed \u2014 retrying Hub upload via node:http(s)");
6194
6361
  dumpHubNetworkError(fetchError, url);
6195
6362
  return postHubUploadNodeHttp(url, apiKey, payload);
6196
6363
  }
6197
6364
  }
6365
+ async function postHubUpload(url, apiKey, payload) {
6366
+ let lastError = new Error("Hub upload failed");
6367
+ for (let attempt = 0; attempt < HUB_UPLOAD_MAX_RETRIES; attempt++) {
6368
+ try {
6369
+ return await postHubUploadOnce(url, apiKey, payload);
6370
+ } catch (error) {
6371
+ lastError = error;
6372
+ if (attempt >= HUB_UPLOAD_MAX_RETRIES - 1 || !isRetryableHubNetworkError(error)) {
6373
+ throw error;
6374
+ }
6375
+ const delayMs = 1e3 * 2 ** attempt;
6376
+ console.error(
6377
+ `[runsec] Hub upload retry ${attempt + 2}/${HUB_UPLOAD_MAX_RETRIES} in ${delayMs}ms (${networkErrorCode(error) || "network"})`
6378
+ );
6379
+ await sleepMs(delayMs);
6380
+ }
6381
+ }
6382
+ throw lastError;
6383
+ }
6198
6384
  function countSeverityMetrics(findings) {
6199
6385
  const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
6200
6386
  for (const f of findings) {
@@ -6218,24 +6404,15 @@ function buildHubUploadPayload(result, reportMetrics, workspacePath, projectId)
6218
6404
  const metrics = countSeverityMetrics(result.findings);
6219
6405
  const verdict = resolveScanVerdict(result);
6220
6406
  const { compliance, complianceASVS } = buildHubComplianceBlock(result);
6221
- const runsecJson = {
6222
- source: "runsec_mcp",
6223
- standard: result.standard,
6224
- workspace_path: workspacePath,
6407
+ const runsecJson = slimRunsecJsonForHubUpload(
6408
+ result,
6409
+ reportMetrics,
6410
+ workspacePath,
6225
6411
  verdict,
6226
6412
  metrics,
6227
6413
  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
- };
6414
+ complianceASVS
6415
+ );
6239
6416
  const resolvedProjectId = projectId?.trim() || process.env.RUNSEC_PROJECT_ID?.trim() || void 0;
6240
6417
  return {
6241
6418
  projectId: resolvedProjectId,
@@ -6251,7 +6428,8 @@ async function uploadScanResultsToHub(payload, apiKey) {
6251
6428
  return { success: false, message: "Sync failed: API key missing" };
6252
6429
  }
6253
6430
  const url = resolveHubUploadUrl();
6254
- console.error(`[runsec] Hub telemetry upload \u2192 ${url}`);
6431
+ const payloadBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
6432
+ console.error(`[runsec] Hub telemetry upload \u2192 ${url} (${payloadBytes} bytes)`);
6255
6433
  const attemptUpload = async (targetUrl) => {
6256
6434
  const errors = [];
6257
6435
  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.87",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "package.json",