diff --git a/src/security/skill-scanner.test.ts b/src/security/skill-scanner.test.ts index 06ed2e361a1..93984e7611a 100644 --- a/src/security/skill-scanner.test.ts +++ b/src/security/skill-scanner.test.ts @@ -164,6 +164,14 @@ exec(cmd); source: ` const cp = require("child_process"); cp.spawn("node", ["server.js"]); +`, + expected: { ruleId: "dangerous-exec", severity: "critical" as const }, + }, + { + name: "detects child_process namespaced exec usage", + source: ` +const cp = require("child_process"); +cp.exec("node server.js"); `, expected: { ruleId: "dangerous-exec", severity: "critical" as const }, }, @@ -247,6 +255,16 @@ const options: ExecOptions = { timeout: 5000 }; expect(findings.some((f) => f.ruleId === "dangerous-exec")).toBe(false); }); + it("does not flag RegExp.exec when child_process appears elsewhere", () => { + const source = ` +import type { ExecOptions } from "child_process"; +const options: ExecOptions = {}; +const match = /^keychain:(.+)$/.exec(value); +`; + const findings = scanSource(source, "plugin.ts"); + expect(findings.some((f) => f.ruleId === "dangerous-exec")).toBe(false); + }); + it("returns empty array for clean plugin code", () => { const source = ` export function greet(name: string): string { diff --git a/src/security/skill-scanner.ts b/src/security/skill-scanner.ts index bfd23d7d6de..4a9d22fd908 100644 --- a/src/security/skill-scanner.ts +++ b/src/security/skill-scanner.ts @@ -219,6 +219,20 @@ function truncateEvidence(evidence: string, maxLen = 120): string { return `${evidence.slice(0, maxLen)}…`; } +function isBenignMemberExecMatch(line: string, match: RegExpExecArray): boolean { + const command = match[1]; + if (command !== "exec") { + return false; + } + + const matchIndex = match.index; + if (matchIndex <= 0 || line[matchIndex - 1] !== ".") { + return false; + } + + return !/\b(?:cp|childProcess|child_process)\s*\.\s*exec\s*\(/.test(line); +} + export function scanSource(source: string, filePath: string): SkillScanFinding[] { const findings: SkillScanFinding[] = []; const lines = source.split("\n"); @@ -242,6 +256,10 @@ export function scanSource(source: string, filePath: string): SkillScanFinding[] continue; } + if (rule.ruleId === "dangerous-exec" && isBenignMemberExecMatch(line, match)) { + continue; + } + // Special handling for suspicious-network: check port if (rule.ruleId === "suspicious-network") { const port = Number.parseInt(match[1], 10);