@runsec/mcp 1.0.12 → 1.0.14
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 +88 -55
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -61,18 +61,53 @@ var STANDARD_TOOL_MAP = {
|
|
|
61
61
|
};
|
|
62
62
|
var cachedRegistry = null;
|
|
63
63
|
var cachedAllRules = null;
|
|
64
|
+
var CWE_ID_REGEX = /(CWE-\d+)/i;
|
|
65
|
+
function parseCweFromRaw(raw) {
|
|
66
|
+
if (raw == null) return "UNKNOWN";
|
|
67
|
+
if (Array.isArray(raw)) {
|
|
68
|
+
for (const item of raw) {
|
|
69
|
+
const id = parseCweFromString(String(item));
|
|
70
|
+
if (id !== "UNKNOWN") return id;
|
|
71
|
+
}
|
|
72
|
+
return "UNKNOWN";
|
|
73
|
+
}
|
|
74
|
+
return parseCweFromString(String(raw));
|
|
75
|
+
}
|
|
76
|
+
function parseCweFromString(text) {
|
|
77
|
+
const m = text.match(CWE_ID_REGEX);
|
|
78
|
+
return m?.[1] ? m[1].toUpperCase() : "UNKNOWN";
|
|
79
|
+
}
|
|
80
|
+
function extractCweFromRule(row, complianceEntry) {
|
|
81
|
+
const meta = row.metadata;
|
|
82
|
+
if (meta && typeof meta === "object") {
|
|
83
|
+
const rawCwe = meta.cwe;
|
|
84
|
+
const fromMeta = parseCweFromRaw(rawCwe);
|
|
85
|
+
if (fromMeta !== "UNKNOWN") return fromMeta;
|
|
86
|
+
}
|
|
87
|
+
const msg = String(row.message || "").trim();
|
|
88
|
+
const fromMsg = parseCweFromString(msg);
|
|
89
|
+
if (fromMsg !== "UNKNOWN") return fromMsg;
|
|
90
|
+
const desc = String(row.description || "").trim();
|
|
91
|
+
const fromDesc = parseCweFromString(desc);
|
|
92
|
+
if (fromDesc !== "UNKNOWN") return fromDesc;
|
|
93
|
+
if (complianceEntry?.cwe?.length) {
|
|
94
|
+
const fromMap = parseCweFromString(String(complianceEntry.cwe[0]));
|
|
95
|
+
if (fromMap !== "UNKNOWN") return fromMap;
|
|
96
|
+
}
|
|
97
|
+
return "UNKNOWN";
|
|
98
|
+
}
|
|
64
99
|
function toSeverity(value) {
|
|
65
100
|
const upper = String(value || "").trim().toUpperCase();
|
|
66
101
|
if (upper === "ERROR" || upper === "CRITICAL") return "critical";
|
|
67
|
-
if (upper === "WARNING") return "
|
|
68
|
-
if (upper === "
|
|
69
|
-
if (upper === "LOW"
|
|
70
|
-
const lower = String(value || "
|
|
102
|
+
if (upper === "WARNING" || upper === "HIGH") return "high";
|
|
103
|
+
if (upper === "INFO") return "low";
|
|
104
|
+
if (upper === "LOW") return "low";
|
|
105
|
+
const lower = String(value || "").toLowerCase();
|
|
71
106
|
if (lower === "critical" || lower === "error") return "critical";
|
|
72
|
-
if (lower === "warning") return "
|
|
73
|
-
if (lower === "
|
|
74
|
-
if (lower === "low"
|
|
75
|
-
return "
|
|
107
|
+
if (lower === "warning" || lower === "high") return "high";
|
|
108
|
+
if (lower === "info") return "low";
|
|
109
|
+
if (lower === "low") return "low";
|
|
110
|
+
return "high";
|
|
76
111
|
}
|
|
77
112
|
function escapeRegexLiteral(text) {
|
|
78
113
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -123,13 +158,12 @@ function parseSemgrepRuleFiles() {
|
|
|
123
158
|
const id = String(row.id || "").trim();
|
|
124
159
|
if (!id) continue;
|
|
125
160
|
const message = String(row.message || "").trim();
|
|
126
|
-
const
|
|
127
|
-
const metricId = extractMetricId(id, message || descField);
|
|
161
|
+
const metricId = extractMetricId(id, message);
|
|
128
162
|
const map = compliance[metricId] || {};
|
|
129
|
-
const cwe =
|
|
163
|
+
const cwe = extractCweFromRule(row, map);
|
|
130
164
|
const patterns = collectRulePatterns(row);
|
|
131
165
|
if (patterns.length === 0) continue;
|
|
132
|
-
const description =
|
|
166
|
+
const description = message || id;
|
|
133
167
|
out.push({
|
|
134
168
|
id,
|
|
135
169
|
metricId,
|
|
@@ -334,20 +368,11 @@ function findLineByOffset(content, offset) {
|
|
|
334
368
|
}
|
|
335
369
|
return line;
|
|
336
370
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
const parts = [];
|
|
343
|
-
if (lineIdx > 0) parts.push(lines[lineIdx - 1] ?? "");
|
|
344
|
-
parts.push(lines[lineIdx] ?? "");
|
|
345
|
-
if (lineIdx + 1 < lines.length) parts.push(lines[lineIdx + 1] ?? "");
|
|
346
|
-
let snippet = parts.join("\n");
|
|
347
|
-
if (snippet.length > SNIPPET_MAX_LEN) {
|
|
348
|
-
snippet = `${snippet.slice(0, SNIPPET_MAX_LEN - 1)}\u2026`;
|
|
349
|
-
}
|
|
350
|
-
return snippet;
|
|
371
|
+
function extractSnippetByLine(fileContent, line) {
|
|
372
|
+
const lines = fileContent.split("\n");
|
|
373
|
+
const start = Math.max(0, line - 2);
|
|
374
|
+
const end = Math.min(lines.length, line + 1);
|
|
375
|
+
return lines.slice(start, end).map((l) => l.replace(/\r$/, "")).join("\n").trim();
|
|
351
376
|
}
|
|
352
377
|
function scanContentWithRules(content, file, workspacePath, rules) {
|
|
353
378
|
const localFindings = [];
|
|
@@ -363,7 +388,7 @@ function scanContentWithRules(content, file, workspacePath, rules) {
|
|
|
363
388
|
while ((match = regex.exec(content)) !== null) {
|
|
364
389
|
const line = findLineByOffset(content, match.index);
|
|
365
390
|
const snippetMatch = match[0] || "";
|
|
366
|
-
const snippet =
|
|
391
|
+
const snippet = extractSnippetByLine(content, line);
|
|
367
392
|
localFindings.push({
|
|
368
393
|
rule_id: rule.id,
|
|
369
394
|
cwe: rule.cwe,
|
|
@@ -436,13 +461,14 @@ async function executeAudit(toolName, args) {
|
|
|
436
461
|
}
|
|
437
462
|
|
|
438
463
|
// src/engine/reportFormatter.ts
|
|
439
|
-
function
|
|
464
|
+
function severityBucket(value) {
|
|
440
465
|
const s = (value || "").toLowerCase();
|
|
441
466
|
if (s === "critical" || s === "error") return "critical";
|
|
442
|
-
if (s === "high") return "high";
|
|
443
|
-
if (s === "warning") return "medium";
|
|
444
467
|
if (s === "low" || s === "info") return "low";
|
|
445
|
-
return "
|
|
468
|
+
return "high";
|
|
469
|
+
}
|
|
470
|
+
function displaySeverityLabel(value) {
|
|
471
|
+
return severityBucket(value).toUpperCase();
|
|
446
472
|
}
|
|
447
473
|
function fenceLang(filePath) {
|
|
448
474
|
const ext = (filePath.split(".").pop() || "text").toLowerCase();
|
|
@@ -452,6 +478,17 @@ function fenceLang(filePath) {
|
|
|
452
478
|
function snippetForMarkdownFence(snippet) {
|
|
453
479
|
return snippet.replace(/```/g, "``\\`");
|
|
454
480
|
}
|
|
481
|
+
function appendSnippetBlock(out, finding) {
|
|
482
|
+
const rawSnippet = String(finding.snippet || finding.match_text || "").trim() || "(no snippet)";
|
|
483
|
+
const fenced = snippetForMarkdownFence(rawSnippet);
|
|
484
|
+
const lang = fenceLang(finding.file_path || "");
|
|
485
|
+
out.push("> **PoC Snippet:**");
|
|
486
|
+
out.push(`> \`\`\`${lang}`);
|
|
487
|
+
for (const ln of fenced.split("\n")) {
|
|
488
|
+
out.push(`> ${ln}`);
|
|
489
|
+
}
|
|
490
|
+
out.push("> ```");
|
|
491
|
+
}
|
|
455
492
|
function safeText(value) {
|
|
456
493
|
return String(value ?? "").replace(/`/g, "'");
|
|
457
494
|
}
|
|
@@ -469,12 +506,11 @@ You MUST automatically process them and output a final report to the user follow
|
|
|
469
506
|
function generateMarkdownReport(standard, findings, metrics) {
|
|
470
507
|
const rows = Array.isArray(findings) ? findings : [];
|
|
471
508
|
const m = metrics || {};
|
|
472
|
-
const critical = rows.filter((f) =>
|
|
473
|
-
const high = rows.filter((f) =>
|
|
474
|
-
const
|
|
475
|
-
const low = rows.filter((f) => normalizeSeverity(f.severity) === "low");
|
|
509
|
+
const critical = rows.filter((f) => severityBucket(f.severity) === "critical");
|
|
510
|
+
const high = rows.filter((f) => severityBucket(f.severity) === "high");
|
|
511
|
+
const low = rows.filter((f) => severityBucket(f.severity) === "low");
|
|
476
512
|
const severe = [...critical, ...high];
|
|
477
|
-
const soft = [...
|
|
513
|
+
const soft = [...low];
|
|
478
514
|
const cweCounts = m.cwe_counts || {};
|
|
479
515
|
const skippedFiles = Number(m.skipped_files || 0);
|
|
480
516
|
const telemetry = {
|
|
@@ -500,7 +536,7 @@ function generateMarkdownReport(standard, findings, metrics) {
|
|
|
500
536
|
out.push("---");
|
|
501
537
|
out.push("#### 2. Compliance Matrix");
|
|
502
538
|
out.push(
|
|
503
|
-
`- **
|
|
539
|
+
`- **CRITICAL:** ${critical.length} | **HIGH:** ${high.length} | **LOW:** ${low.length}`
|
|
504
540
|
);
|
|
505
541
|
out.push(`- **Files Scanned:** ${Number(m.scanned_files_count || 0)} | **Skipped:** ${skippedFiles}`);
|
|
506
542
|
out.push("- **\u{1F6E1}\uFE0F False Positives Dropped by AI:** [Count] *(LLM: replace [Count] after triage; keep this line in the Compliance Matrix.)*");
|
|
@@ -516,19 +552,11 @@ function generateMarkdownReport(standard, findings, metrics) {
|
|
|
516
552
|
const file = safeText(finding.file_path || "unknown_file");
|
|
517
553
|
const line = Number(finding.line || 0);
|
|
518
554
|
const description = safeText(finding.description || "No description");
|
|
519
|
-
const sev =
|
|
520
|
-
const rawSnippet = String(finding.snippet || finding.match_text || "").trim() || "(no snippet)";
|
|
521
|
-
const fenced = snippetForMarkdownFence(rawSnippet);
|
|
522
|
-
const lang = fenceLang(finding.file_path || "");
|
|
555
|
+
const sev = displaySeverityLabel(finding.severity);
|
|
523
556
|
out.push(`**[${cwe}] ${rule} in \`${file}:${line}\`**`);
|
|
524
557
|
out.push(`- **Severity:** ${sev}`);
|
|
525
558
|
out.push(`- **Description:** ${description}`);
|
|
526
|
-
out
|
|
527
|
-
out.push(`> \`\`\`${lang}`);
|
|
528
|
-
for (const ln of fenced.split("\n")) {
|
|
529
|
-
out.push(`> ${ln}`);
|
|
530
|
-
}
|
|
531
|
-
out.push("> ```");
|
|
559
|
+
appendSnippetBlock(out, finding);
|
|
532
560
|
out.push(
|
|
533
561
|
`- \u{1F4A1} *Prompt to fix:* \`@RunSec Fix the ${rule} vulnerability in ${file}. Ensure it complies with ${safeText(standard)}.\``
|
|
534
562
|
);
|
|
@@ -536,23 +564,28 @@ function generateMarkdownReport(standard, findings, metrics) {
|
|
|
536
564
|
}
|
|
537
565
|
}
|
|
538
566
|
out.push("---");
|
|
539
|
-
out.push("#### 4.
|
|
567
|
+
out.push("#### 4. LOW severity findings");
|
|
540
568
|
if (soft.length === 0) {
|
|
541
|
-
out.push("_No
|
|
569
|
+
out.push("_No LOW / INFO severity findings in this bucket._");
|
|
542
570
|
} else {
|
|
543
571
|
for (const finding of soft) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
);
|
|
572
|
+
const cwe = safeText(finding.cwe || "UNKNOWN");
|
|
573
|
+
const rule = safeText(finding.rule_id || "unknown_rule");
|
|
574
|
+
const file = safeText(finding.file_path || "unknown_file");
|
|
575
|
+
const line = Number(finding.line || 0);
|
|
576
|
+
const description = safeText(finding.description || "No description");
|
|
577
|
+
out.push(`**[${cwe}] ${rule} in \`${file}:${line}\`**`);
|
|
578
|
+
out.push(`- **Severity:** ${displaySeverityLabel(finding.severity)}`);
|
|
579
|
+
out.push(`- **Description:** ${description}`);
|
|
580
|
+
appendSnippetBlock(out, finding);
|
|
581
|
+
out.push("");
|
|
549
582
|
}
|
|
550
583
|
}
|
|
551
584
|
out.push("");
|
|
552
585
|
out.push("---");
|
|
553
586
|
out.push("#### 5. Remediation Roadmap");
|
|
554
587
|
out.push('- **Iteration 1 (Immediate):** Copy the "Prompt to fix" commands above for Critical issues.');
|
|
555
|
-
out.push("- **Iteration 2:** Review and address
|
|
588
|
+
out.push("- **Iteration 2:** Review and address LOW / INFO findings.");
|
|
556
589
|
out.push("");
|
|
557
590
|
out.push("<details>");
|
|
558
591
|
out.push("<summary>System Telemetry</summary>");
|