@runsec/mcp 1.0.26 → 1.0.28
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.
|
@@ -18,4 +18,4 @@ rules:
|
|
|
18
18
|
- pattern: child_process.exec($X, ...)
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Map to MITRE **T1059.007** (JavaScript) / **T1204** where applicable; combine with
|
|
21
|
+
Map to MITRE **T1059.007** (JavaScript) / **T1204** where applicable; combine with RunSec cognitive scoring in the Hub pipeline when matches are constant-only.
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
|
29
29
|
|
|
30
30
|
// src/engine/ruleEngine.ts
|
|
31
31
|
var import_node_fs2 = require("fs");
|
|
32
|
+
var import_node_fs3 = require("fs");
|
|
32
33
|
var import_node_path2 = __toESM(require("path"));
|
|
33
34
|
var import_ignore = __toESM(require("ignore"));
|
|
34
35
|
|
|
@@ -250,7 +251,7 @@ async function buildIgnoreMatcher(workspaceRoot) {
|
|
|
250
251
|
]);
|
|
251
252
|
const runsecIgnorePath = import_node_path2.default.join(workspaceRoot, RUNSEC_IGNORE_FILE);
|
|
252
253
|
try {
|
|
253
|
-
const content = await
|
|
254
|
+
const content = await import_node_fs3.promises.readFile(runsecIgnorePath, "utf-8");
|
|
254
255
|
matcher.add(content);
|
|
255
256
|
} catch {
|
|
256
257
|
}
|
|
@@ -265,7 +266,7 @@ async function collectFilesWithStats(workspacePath, targetFiles) {
|
|
|
265
266
|
let skippedByIgnore = 0;
|
|
266
267
|
let stat;
|
|
267
268
|
try {
|
|
268
|
-
stat = await
|
|
269
|
+
stat = await import_node_fs3.promises.stat(root);
|
|
269
270
|
} catch {
|
|
270
271
|
stat = null;
|
|
271
272
|
}
|
|
@@ -282,7 +283,7 @@ async function collectFilesWithStats(workspacePath, targetFiles) {
|
|
|
282
283
|
continue;
|
|
283
284
|
}
|
|
284
285
|
try {
|
|
285
|
-
const s = await
|
|
286
|
+
const s = await import_node_fs3.promises.stat(candidate);
|
|
286
287
|
if (s.isFile()) out.push(candidate);
|
|
287
288
|
} catch {
|
|
288
289
|
}
|
|
@@ -293,7 +294,7 @@ async function collectFilesWithStats(workspacePath, targetFiles) {
|
|
|
293
294
|
const stack = [root];
|
|
294
295
|
while (stack.length) {
|
|
295
296
|
const dir = stack.pop();
|
|
296
|
-
const entries = await
|
|
297
|
+
const entries = await import_node_fs3.promises.readdir(dir, { withFileTypes: true });
|
|
297
298
|
for (const entry of entries) {
|
|
298
299
|
const full = import_node_path2.default.join(dir, entry.name);
|
|
299
300
|
const relative = normalizeRelativePath(import_node_path2.default.relative(root, full));
|
|
@@ -329,14 +330,16 @@ function findLineByOffset(content, offset) {
|
|
|
329
330
|
}
|
|
330
331
|
return line;
|
|
331
332
|
}
|
|
332
|
-
function
|
|
333
|
+
function extractSnippetFromDisk(absoluteFilePath, line) {
|
|
333
334
|
try {
|
|
334
|
-
const
|
|
335
|
+
const actualContent = (0, import_node_fs2.readFileSync)(absoluteFilePath, "utf-8");
|
|
336
|
+
const lines = actualContent.split("\n");
|
|
335
337
|
const start = Math.max(0, line - 2);
|
|
336
338
|
const end = Math.min(lines.length, line + 2);
|
|
337
|
-
return lines.slice(start, end).join("\n").trim() || "
|
|
338
|
-
} catch {
|
|
339
|
-
|
|
339
|
+
return lines.slice(start, end).join("\n").trim() || "SNIPPET_IS_EMPTY";
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error(`Failed to read snippet for ${absoluteFilePath}`, error);
|
|
342
|
+
return "ERROR_READING_FILE";
|
|
340
343
|
}
|
|
341
344
|
}
|
|
342
345
|
function scanContentWithRules(content, file, workspacePath, rules) {
|
|
@@ -353,7 +356,7 @@ function scanContentWithRules(content, file, workspacePath, rules) {
|
|
|
353
356
|
while ((match = regex.exec(content)) !== null) {
|
|
354
357
|
const line = findLineByOffset(content, match.index);
|
|
355
358
|
const snippetMatch = match[0] || "";
|
|
356
|
-
const snippet =
|
|
359
|
+
const snippet = extractSnippetFromDisk(file, line);
|
|
357
360
|
localFindings.push({
|
|
358
361
|
rule_id: rule.id,
|
|
359
362
|
cwe: rule.cwe,
|
|
@@ -384,7 +387,7 @@ async function executeAudit(toolName, args) {
|
|
|
384
387
|
fileBatch.map(async (file) => {
|
|
385
388
|
let fileStat;
|
|
386
389
|
try {
|
|
387
|
-
fileStat = await
|
|
390
|
+
fileStat = await import_node_fs3.promises.stat(file);
|
|
388
391
|
} catch {
|
|
389
392
|
return [];
|
|
390
393
|
}
|
|
@@ -394,7 +397,7 @@ async function executeAudit(toolName, args) {
|
|
|
394
397
|
}
|
|
395
398
|
let content = "";
|
|
396
399
|
try {
|
|
397
|
-
content = await
|
|
400
|
+
content = await import_node_fs3.promises.readFile(file, "utf-8");
|
|
398
401
|
} catch {
|
|
399
402
|
return [];
|
|
400
403
|
}
|
|
@@ -426,17 +429,19 @@ async function executeAudit(toolName, args) {
|
|
|
426
429
|
}
|
|
427
430
|
|
|
428
431
|
// src/engine/reportFormatter.ts
|
|
429
|
-
var
|
|
432
|
+
var import_node_fs4 = __toESM(require("fs"));
|
|
430
433
|
var import_node_path3 = __toESM(require("path"));
|
|
431
434
|
function safeText(value) {
|
|
432
435
|
return String(value ?? "").replace(/`/g, "'");
|
|
433
436
|
}
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
437
|
+
function cleanReportFindings(findings) {
|
|
438
|
+
return findings.filter((f) => {
|
|
439
|
+
const ruleId = String(f.rule_id || "").toLowerCase();
|
|
440
|
+
const fp = String(f.file_path || "").replace(/\\/g, "/");
|
|
441
|
+
const isK8sInPython = ruleId.includes("k8s") && fp.toLowerCase().endsWith(".py");
|
|
442
|
+
const isTestOrDoc = fp.includes("SECURITY_AUDIT") || fp.includes("tests/");
|
|
443
|
+
return !isK8sInPython && !isTestOrDoc;
|
|
444
|
+
});
|
|
440
445
|
}
|
|
441
446
|
function tierCriticalHighLow(severity) {
|
|
442
447
|
const x = (severity || "HIGH").toUpperCase();
|
|
@@ -448,7 +453,7 @@ function escapeSnippetForBlockquoteFenced(snippet) {
|
|
|
448
453
|
return snippet.replace(/```/g, "```");
|
|
449
454
|
}
|
|
450
455
|
function buildServerSideReportMarkdown(standard, findings, metrics) {
|
|
451
|
-
const rows = findings
|
|
456
|
+
const rows = cleanReportFindings(findings);
|
|
452
457
|
let critical = 0;
|
|
453
458
|
let high = 0;
|
|
454
459
|
let low = 0;
|
|
@@ -467,7 +472,7 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
|
|
|
467
472
|
out.push("---");
|
|
468
473
|
out.push("### Compliance Matrix");
|
|
469
474
|
out.push(`- **CRITICAL:** ${critical} | **HIGH:** ${high} | **LOW:** ${low}`);
|
|
470
|
-
out.push(`- **Reported findings (after excluding SECURITY_AUDIT
|
|
475
|
+
out.push(`- **Reported findings (after excluding K8s-on-.py, SECURITY_AUDIT, tests/):** ${rows.length}`);
|
|
471
476
|
out.push("");
|
|
472
477
|
out.push("---");
|
|
473
478
|
out.push("### Findings");
|
|
@@ -481,7 +486,7 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
|
|
|
481
486
|
const sev = safeText(finding.severity || "HIGH");
|
|
482
487
|
const cwe = safeText(finding.cwe || "UNKNOWN");
|
|
483
488
|
const desc = String(finding.description || "").trim();
|
|
484
|
-
const rawSnippet = String(finding.snippet
|
|
489
|
+
const rawSnippet = String(finding.snippet ?? "").trim() || "SNIPPET_IS_EMPTY";
|
|
485
490
|
const sn = escapeSnippetForBlockquoteFenced(rawSnippet);
|
|
486
491
|
out.push(`#### \`${fp}:${line}\` \u2014 ${rule}`);
|
|
487
492
|
if (desc) out.push(safeText(desc));
|
|
@@ -520,7 +525,7 @@ function generateMarkdownReport(standard, findings, metrics, workspacePath) {
|
|
|
520
525
|
const rows = Array.isArray(findings) ? findings : [];
|
|
521
526
|
const reportContent = buildServerSideReportMarkdown(standard, rows, m);
|
|
522
527
|
const reportPath = import_node_path3.default.join(process.cwd(), "runsec-report.md");
|
|
523
|
-
|
|
528
|
+
import_node_fs4.default.writeFileSync(reportPath, reportContent, "utf-8");
|
|
524
529
|
console.error(`[runsec] wrote server-side report to: ${reportPath}`);
|
|
525
530
|
return `
|
|
526
531
|
<system_directive>
|