@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.
Files changed (2) hide show
  1. package/dist/index.js +88 -55
  2. 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 "medium";
68
- if (upper === "HIGH") return "high";
69
- if (upper === "LOW" || upper === "INFO") return "low";
70
- const lower = String(value || "medium").toLowerCase();
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 "medium";
73
- if (lower === "high") return "high";
74
- if (lower === "low" || lower === "info") return "low";
75
- return "medium";
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 descField = String(row.description || "").trim();
127
- const metricId = extractMetricId(id, message || descField);
161
+ const metricId = extractMetricId(id, message);
128
162
  const map = compliance[metricId] || {};
129
- const cwe = Array.isArray(map.cwe) && map.cwe.length ? map.cwe[0] : "UNKNOWN";
163
+ const cwe = extractCweFromRule(row, map);
130
164
  const patterns = collectRulePatterns(row);
131
165
  if (patterns.length === 0) continue;
132
- const description = (message || descField || `RunSec detection ${metricId}`).trim();
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
- var SNIPPET_MAX_LEN = 150;
338
- function buildMatchSnippet(content, matchIndex) {
339
- const lines = content.split(/\r?\n/);
340
- const lineNo1Based = findLineByOffset(content, matchIndex);
341
- const lineIdx = lineNo1Based - 1;
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 = buildMatchSnippet(content, match.index);
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 normalizeSeverity(value) {
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 "medium";
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) => normalizeSeverity(f.severity) === "critical");
473
- const high = rows.filter((f) => normalizeSeverity(f.severity) === "high");
474
- const medium = rows.filter((f) => normalizeSeverity(f.severity) === "medium");
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 = [...medium, ...low];
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
- `- **Critical:** ${critical.length} | **High:** ${high.length} | **Medium:** ${medium.length} | **Low:** ${low.length}`
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 = safeText((finding.severity || "unknown").toUpperCase());
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.push("> **PoC Snippet:**");
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. Medium & Low Risks");
567
+ out.push("#### 4. LOW severity findings");
540
568
  if (soft.length === 0) {
541
- out.push("_No medium or low risks detected._");
569
+ out.push("_No LOW / INFO severity findings in this bucket._");
542
570
  } else {
543
571
  for (const finding of soft) {
544
- out.push(
545
- `- \`${safeText(finding.file_path || "unknown_file")}:${Number(finding.line || 0)}\` ${safeText(
546
- finding.rule_id || "unknown_rule"
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 Medium risks.");
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>");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runsec/mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist",