@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.
- package/dist/index.js +215 -37
- 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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
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
|
|
6182
|
-
|
|
6183
|
-
|
|
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
|
|
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
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6407
|
+
const runsecJson = slimRunsecJsonForHubUpload(
|
|
6408
|
+
result,
|
|
6409
|
+
reportMetrics,
|
|
6410
|
+
workspacePath,
|
|
6225
6411
|
verdict,
|
|
6226
6412
|
metrics,
|
|
6227
6413
|
compliance,
|
|
6228
|
-
complianceASVS
|
|
6229
|
-
|
|
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
|
-
|
|
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];
|