agent-security-scanner-mcp 3.4.0 → 3.5.1

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/src/cli/init.js CHANGED
@@ -73,12 +73,6 @@ const CLIENT_CONFIGS = {
73
73
  configKey: 'mcpServers',
74
74
  configPath: () => join(vscodeBase(), 'Code', 'User', 'globalStorage', 'sourcegraph.cody-ai', 'mcp_settings.json'),
75
75
  buildEntry: () => ({ ...MCP_SERVER_ENTRY })
76
- },
77
- 'openclaw': {
78
- name: 'OpenClaw',
79
- isSkillBased: true, // OpenClaw uses skills, not MCP config
80
- skillPath: () => join(homedir(), '.openclaw', 'workspace', 'skills', 'security-scanner'),
81
- configPath: () => join(homedir(), '.openclaw', 'workspace', 'skills', 'security-scanner', 'SKILL.md')
82
76
  }
83
77
  };
84
78
 
@@ -156,87 +150,6 @@ function printInitUsage() {
156
150
  console.log(' npx agent-security-scanner-mcp init cline --force --name my-scanner\n');
157
151
  }
158
152
 
159
- // Special installer for OpenClaw (skill-based)
160
- async function installOpenClawSkill(client, flags) {
161
- const skillDir = client.skillPath();
162
- const skillFile = client.configPath();
163
-
164
- // Find the source skill file (bundled with the package)
165
- const __dirname = dirname(new URL(import.meta.url).pathname);
166
- const sourceSkill = join(__dirname, '..', '..', 'skills', 'openclaw', 'SKILL.md');
167
-
168
- console.log(`\n Client: ${client.name}`);
169
- console.log(` Skill: ${skillDir}`);
170
- console.log(` OS: ${platform()} (${process.arch})\n`);
171
-
172
- // Check if OpenClaw workspace exists
173
- const openclawDir = join(homedir(), '.openclaw');
174
- if (!existsSync(openclawDir)) {
175
- console.log(` OpenClaw not found at ${openclawDir}`);
176
- console.log(` Please install OpenClaw first: https://openclaw.ai\n`);
177
- process.exit(1);
178
- }
179
-
180
- // Check if source skill exists
181
- if (!existsSync(sourceSkill)) {
182
- console.error(` ERROR: Skill source not found at ${sourceSkill}`);
183
- console.error(` This may be a packaging issue. Please reinstall the package.\n`);
184
- process.exit(1);
185
- }
186
-
187
- // Check if skill already exists
188
- if (existsSync(skillFile)) {
189
- const existing = readFileSync(skillFile, 'utf-8');
190
- const source = readFileSync(sourceSkill, 'utf-8');
191
- if (existing === source) {
192
- console.log(` Security scanner skill is already installed (identical).`);
193
- console.log(` Nothing to do.\n`);
194
- process.exit(0);
195
- }
196
-
197
- console.log(` Security scanner skill exists but differs.`);
198
- if (!flags.force) {
199
- if (flags.yes) {
200
- console.log(` Skipping (use --force to overwrite).\n`);
201
- process.exit(0);
202
- }
203
- const rl = createInterface({ input: process.stdin, output: process.stdout });
204
- const answer = await new Promise((resolve) => {
205
- rl.question(' Overwrite? (y/N): ', (a) => { rl.close(); resolve(a); });
206
- });
207
- if (answer.toLowerCase() !== 'y') {
208
- console.log(' Aborted.\n');
209
- process.exit(0);
210
- }
211
- }
212
- }
213
-
214
- // Dry-run mode
215
- if (flags.dryRun) {
216
- console.log(` [dry-run] Would create directory: ${skillDir}`);
217
- console.log(` [dry-run] Would copy skill from: ${sourceSkill}`);
218
- console.log(` [dry-run] Would write to: ${skillFile}`);
219
- console.log(` No changes made.\n`);
220
- process.exit(0);
221
- }
222
-
223
- // Create skill directory
224
- if (!existsSync(skillDir)) {
225
- mkdirSync(skillDir, { recursive: true });
226
- console.log(` Created directory: ${skillDir}`);
227
- }
228
-
229
- // Copy skill file
230
- copyFileSync(sourceSkill, skillFile);
231
- console.log(` Installed skill: ${skillFile}`);
232
-
233
- console.log(`\n OpenClaw security scanner skill installed successfully!`);
234
- console.log(`\n Usage in OpenClaw:`);
235
- console.log(` - The skill will be auto-discovered by OpenClaw`);
236
- console.log(` - Use /security-scanner to invoke it`);
237
- console.log(` - Or ask: "scan this prompt for security issues"\n`);
238
- }
239
-
240
153
  export async function runInit(args) {
241
154
  const flags = parseInitFlags(args);
242
155
  let clientName = flags.client;
@@ -258,12 +171,6 @@ export async function runInit(args) {
258
171
  process.exit(1);
259
172
  }
260
173
 
261
- // Special handling for OpenClaw (skill-based, not MCP config)
262
- if (client.isSkillBased) {
263
- await installOpenClawSkill(client, flags);
264
- return;
265
- }
266
-
267
174
  const configPath = flags.path || client.configPath();
268
175
  const serverName = flags.name;
269
176
  const entry = client.buildEntry();
@@ -63,56 +63,20 @@ export const FIX_TEMPLATES = {
63
63
  // COMMAND INJECTION
64
64
  // ===========================================
65
65
  "child-process-exec": {
66
- description: "Use execFile() with separate command and arguments array",
67
- fix: (line) => {
68
- // Match: exec("cmd " + arg) -> execFile("cmd", [arg])
69
- const concatMatch = line.match(/\bexec\s*\(\s*["'](\S+)\s+["']\s*\+\s*(\w+)/);
70
- if (concatMatch) {
71
- return line.replace(/\bexec\s*\(\s*["'](\S+)\s+["']\s*\+\s*(\w+)\s*\)/, 'execFile("$1", [$2])');
72
- }
73
- // Match: exec(`cmd ${arg}`) -> execFile("cmd", [arg])
74
- const templateMatch = line.match(/\bexec\s*\(\s*`(\S+)\s+\$\{(\w+)\}`/);
75
- if (templateMatch) {
76
- return line.replace(/\bexec\s*\(\s*`(\S+)\s+\$\{(\w+)\}`\s*\)/, 'execFile("$1", [$2])');
77
- }
78
- // Match: exec(variable) -> execFile with guidance
79
- const varMatch = line.match(/\bexec\s*\(\s*(\w+)\s*\)/);
80
- if (varMatch) {
81
- return line.replace(/\bexec\s*\(\s*(\w+)\s*\)/, 'execFile($1.split(" ")[0], $1.split(" ").slice(1))');
82
- }
83
- // Fallback: comment with guidance
84
- return '// SECURITY: Use execFile(command, [args]) instead of exec() - ' + line.trim();
85
- }
66
+ description: "Use execFile() or spawn() with shell: false",
67
+ fix: (line) => line.replace(/\bexec\s*\(/, 'execFile(')
86
68
  },
87
69
  "spawn-shell": {
88
70
  description: "Use spawn with shell: false",
89
71
  fix: (line) => line.replace(/shell\s*:\s*true/i, 'shell: false')
90
72
  },
91
73
  "dangerous-subprocess": {
92
- description: "Use subprocess.run with list arguments and shell=False",
93
- fix: (line) => {
94
- // Replace shell=True with shell=False
95
- let fixed = line.replace(/shell\s*=\s*True/, 'shell=False');
96
- // Replace string command with shlex.split() for safe list form
97
- fixed = fixed.replace(
98
- /subprocess\.(call|run|Popen)\s*\(\s*["'](.+?)["']/,
99
- 'subprocess.$1(shlex.split("$2")'
100
- );
101
- return fixed;
102
- }
74
+ description: "Use subprocess.run with list arguments",
75
+ fix: (line) => line.replace(/subprocess\.(call|run|Popen)\s*\(\s*["'](.+?)["']\s*,\s*shell\s*=\s*True/, 'subprocess.$1(["$2".split()], shell=False')
103
76
  },
104
77
  "dangerous-system-call": {
105
78
  description: "Use subprocess.run instead of os.system",
106
- fix: (line) => {
107
- const match = line.match(/os\.system\s*\(\s*(.+?)\s*\)/);
108
- if (match) {
109
- return line.replace(
110
- /os\.system\s*\(\s*(.+?)\s*\)/,
111
- 'subprocess.run(shlex.split($1), shell=False)'
112
- );
113
- }
114
- return '# SECURITY: Replace os.system() with subprocess.run(shlex.split(cmd), shell=False)\n# ' + line.trim();
115
- }
79
+ fix: (line) => line.replace(/os\.system\s*\(/, 'subprocess.run([')
116
80
  },
117
81
  "command-injection-exec": {
118
82
  description: "Use exec.Command with separate arguments",
@@ -233,11 +197,8 @@ export const FIX_TEMPLATES = {
233
197
  // INSECURE DESERIALIZATION
234
198
  // ===========================================
235
199
  "pickle": {
236
- description: "Use JSON instead of pickle for untrusted data",
237
- fix: (line) => {
238
- const fixed = line.replace(/pickle\.(load|loads)\s*\(/, 'json.$1(');
239
- return fixed + ' # NOTE: data must be JSON-formatted';
240
- }
200
+ description: "Use JSON instead of pickle",
201
+ fix: (line) => line.replace(/pickle\.(load|loads)\s*\(/, 'json.$1(')
241
202
  },
242
203
  "yaml-load": {
243
204
  description: "Use yaml.safe_load()",
@@ -248,8 +209,8 @@ export const FIX_TEMPLATES = {
248
209
  fix: (line) => line.replace(/marshal\.(load|loads)\s*\(/, 'json.$1(')
249
210
  },
250
211
  "shelve": {
251
- description: "Use JSON or SQLite instead of shelve for safe storage",
252
- fix: (line) => '# SECURITY: Replace shelve with json or sqlite3 for safe storage\n# ' + line.trim()
212
+ description: "Use JSON or SQLite instead of shelve",
213
+ fix: (line) => line.replace(/shelve\.open\s*\(/, 'json.load(open(')
253
214
  },
254
215
  "node-serialize": {
255
216
  description: "Use JSON.parse instead of node-serialize",
@@ -296,12 +257,12 @@ export const FIX_TEMPLATES = {
296
257
  // PATH TRAVERSAL
297
258
  // ===========================================
298
259
  "path-traversal": {
299
- description: "Resolve real path and validate prefix to prevent traversal",
260
+ description: "Sanitize file paths and use basename",
300
261
  fix: (line, lang) => {
301
- if (lang === 'python') return line.replace(/open\s*\(\s*(\w+)/, 'open(os.path.realpath($1) # TODO: validate path prefix');
302
- if (lang === 'go') return line.replace(/os\.Open\s*\(\s*(\w+)/, 'os.Open(filepath.Clean($1) // TODO: validate path prefix');
303
- if (lang === 'java') return line.replace(/new File\s*\(\s*(\w+)/, 'new File($1).getCanonicalFile( // TODO: validate path prefix');
304
- return line.replace(/readFileSync\s*\(\s*(\w+)/, 'readFileSync(path.resolve($1) // TODO: validate path prefix');
262
+ if (lang === 'python') return line.replace(/open\s*\(\s*(\w+)/, 'open(os.path.basename($1)');
263
+ if (lang === 'go') return line.replace(/os\.Open\s*\(\s*(\w+)/, 'os.Open(filepath.Base($1)');
264
+ if (lang === 'java') return line.replace(/new File\s*\(\s*(\w+)/, 'new File(new File($1).getName()');
265
+ return line.replace(/readFileSync\s*\(\s*(\w+)/, 'readFileSync(path.basename($1)');
305
266
  }
306
267
  },
307
268
 
@@ -446,14 +407,7 @@ export const FIX_TEMPLATES = {
446
407
  // ===========================================
447
408
  "prototype-pollution": {
448
409
  description: "Validate object keys before assignment",
449
- fix: (line) => {
450
- // Only fix simple single-line assignments like: obj[key] = value
451
- // Reject lines with multiple bracket accesses or chained assignments
452
- if (/^\s*\w+\[\w+\]\s*=\s*[^[=]+$/.test(line)) {
453
- return line.replace(/(\w+)\[(\w+)\]\s*=/, 'if (!["__proto__", "constructor", "prototype"].includes($2)) $1[$2] =');
454
- }
455
- return '// SECURITY: Validate key is not __proto__/constructor/prototype before assignment\n// ' + line.trim();
456
- }
410
+ fix: (line) => line.replace(/(\w+)\[(\w+)\]\s*=/, 'if (!["__proto__", "constructor", "prototype"].includes($2)) $1[$2] =')
457
411
  },
458
412
 
459
413
  // ===========================================
@@ -489,7 +443,7 @@ export const FIX_TEMPLATES = {
489
443
  // ===========================================
490
444
  "helmet-missing": {
491
445
  description: "Add helmet middleware for security headers",
492
- fix: (line) => '// TODO: Add app.use(helmet()) after Express app initialization\n' + line
446
+ fix: (line) => 'app.use(helmet()); // Add security headers\n' + line
493
447
  },
494
448
 
495
449
  // ===========================================
@@ -541,10 +495,7 @@ export const FIX_TEMPLATES = {
541
495
  },
542
496
  "run-shell-form": {
543
497
  description: "Use exec form for RUN commands",
544
- fix: (line) => line.replace(/RUN\s+(.+)$/, (_, cmd) => {
545
- const escaped = cmd.replace(/"/g, '\\"');
546
- return `RUN ["/bin/sh", "-c", "${escaped}"]`;
547
- })
498
+ fix: (line) => line.replace(/RUN\s+(.+)$/, 'RUN ["/bin/sh", "-c", "$1"]')
548
499
  },
549
500
  "sudo-in-dockerfile": {
550
501
  description: "Avoid sudo in Dockerfile - use USER directive",
@@ -2,9 +2,6 @@
2
2
  import { z } from "zod";
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import { detectLanguage, runAnalyzer, generateFix } from '../utils.js';
5
- import { deduplicateFindings } from '../dedup.js';
6
- import { applyContextFilter, detectFrameworks, applyFrameworkAdjustments } from '../context.js';
7
- import { loadConfig, shouldExcludeFile, applyConfig } from '../config.js';
8
5
 
9
6
  export const fixSecuritySchema = {
10
7
  file_path: z.string().describe("Path to the file to fix"),
@@ -50,48 +47,24 @@ export async function fixSecurity({ file_path, verbosity }) {
50
47
  };
51
48
  }
52
49
 
53
- // Load project configuration
54
- const config = loadConfig(file_path);
50
+ const issues = runAnalyzer(file_path);
55
51
 
56
- // Check file exclusion
57
- if (shouldExcludeFile(file_path, config)) {
58
- return {
59
- content: [{ type: "text", text: JSON.stringify({ file: file_path, message: "File excluded by configuration", fixes_applied: 0 }) }]
60
- };
61
- }
62
-
63
- const rawIssues = runAnalyzer(file_path);
64
-
65
- if (rawIssues.error || !Array.isArray(rawIssues) || rawIssues.length === 0) {
52
+ if (issues.error || !Array.isArray(issues) || issues.length === 0) {
66
53
  return {
67
54
  content: [{
68
55
  type: "text",
69
56
  text: JSON.stringify({
70
- message: rawIssues.error ? "Error scanning file" : "No security issues found",
71
- details: rawIssues
57
+ message: issues.error ? "Error scanning file" : "No security issues found",
58
+ details: issues
72
59
  })
73
60
  }]
74
61
  };
75
62
  }
76
63
 
77
- // Cross-engine deduplication
78
- const dedupedIssues = deduplicateFindings(rawIssues);
79
-
80
64
  // Read and fix the file
81
65
  const content = readFileSync(file_path, 'utf-8');
82
66
  const lines = content.split('\n');
83
67
  const language = detectLanguage(file_path);
84
-
85
- // Context-aware filtering (suppress known module imports)
86
- const contextFiltered = applyContextFilter(dedupedIssues, file_path, language);
87
-
88
- // Framework-aware severity adjustment
89
- const frameworks = detectFrameworks(file_path, language);
90
- const frameworkAdjusted = applyFrameworkAdjustments(contextFiltered, frameworks);
91
-
92
- // Apply .scannerrc configuration (rule suppression, severity/confidence thresholds)
93
- const issues = applyConfig(frameworkAdjusted, file_path, config);
94
-
95
68
  const fixes = [];
96
69
 
97
70
  // Apply fixes (process in reverse order to preserve line numbers)
@@ -39,12 +39,6 @@ const CATEGORY_WEIGHTS = {
39
39
  "prompt-injection-privilege": 0.85,
40
40
  "prompt-injection-multi-turn": 0.7,
41
41
  "prompt-injection-output": 0.9,
42
- // OpenClaw-specific categories
43
- "data_exfiltration": 1.0,
44
- "messaging_abuse": 0.95,
45
- "credential_theft": 1.0,
46
- "autonomous_harm": 0.9,
47
- "service_attack": 0.95,
48
42
  "unknown": 0.5
49
43
  };
50
44
 
@@ -195,69 +189,6 @@ function loadPromptInjectionRules() {
195
189
  }
196
190
  }
197
191
 
198
- // Load OpenClaw-specific rules
199
- function loadOpenClawRules() {
200
- try {
201
- const rulesPath = join(__dirname, '..', '..', 'rules', 'openclaw.security.yaml');
202
- if (!existsSync(rulesPath)) {
203
- return [];
204
- }
205
-
206
- const yaml = readFileSync(rulesPath, 'utf-8');
207
- const rules = [];
208
-
209
- const ruleBlocks = yaml.split(/^ - id:/m).slice(1);
210
-
211
- for (const block of ruleBlocks) {
212
- const lines = (' - id:' + block).split('\n');
213
- const rule = {
214
- id: '',
215
- severity: 'WARNING',
216
- message: '',
217
- patterns: [],
218
- metadata: {}
219
- };
220
-
221
- let inPatterns = false;
222
-
223
- for (const line of lines) {
224
- if (line.match(/^\s+- id:\s*/)) {
225
- rule.id = line.replace(/^\s+- id:\s*/, '').trim();
226
- } else if (line.match(/^\s+severity:\s*/)) {
227
- rule.severity = line.replace(/^\s+severity:\s*/, '').trim();
228
- } else if (line.match(/^\s+category:\s*/)) {
229
- rule.metadata.category = line.replace(/^\s+category:\s*/, '').trim();
230
- } else if (line.match(/^\s+action:\s*/)) {
231
- rule.metadata.action = line.replace(/^\s+action:\s*/, '').trim();
232
- } else if (line.match(/^\s+message:\s*/)) {
233
- rule.message = line.replace(/^\s+message:\s*["']?/, '').replace(/["']$/, '').trim();
234
- } else if (line.match(/^\s+patterns:\s*$/)) {
235
- inPatterns = true;
236
- } else if (inPatterns && line.match(/^\s+- /)) {
237
- let pattern = line.replace(/^\s+- /, '').trim();
238
- pattern = pattern.replace(/^["']|["']$/g, '');
239
- pattern = pattern.replace(/\\\\/g, '\\');
240
- if (pattern) rule.patterns.push(pattern);
241
- } else if (line.match(/^\s+\w+:/) && !line.match(/^\s+- /)) {
242
- inPatterns = false;
243
- }
244
- }
245
-
246
- if (rule.id && rule.patterns.length > 0) {
247
- // Set confidence and risk score based on severity
248
- rule.metadata.confidence = rule.severity === 'CRITICAL' ? 'HIGH' : 'MEDIUM';
249
- rule.metadata.risk_score = rule.severity === 'CRITICAL' ? '90' : '70';
250
- rules.push(rule);
251
- }
252
- }
253
-
254
- return rules;
255
- } catch (error) {
256
- console.error("Error loading OpenClaw rules:", error.message);
257
- return [];
258
- }
259
- }
260
-
261
192
  // Calculate risk score from findings
262
193
  function calculateRiskScore(findings, context) {
263
194
  if (findings.length === 0) return 0;
@@ -446,8 +377,7 @@ export async function scanAgentPrompt({ prompt_text, context, verbosity }) {
446
377
  // Load rules
447
378
  const agentRules = loadAgentAttackRules();
448
379
  const promptRules = loadPromptInjectionRules();
449
- const openclawRules = loadOpenClawRules();
450
- const allRules = [...agentRules, ...promptRules, ...openclawRules];
380
+ const allRules = [...agentRules, ...promptRules];
451
381
 
452
382
  // 2.7: Extract content from code blocks and append to scan text
453
383
  let expandedText = prompt_text;
@@ -2,15 +2,11 @@
2
2
  import { z } from "zod";
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import { detectLanguage, runAnalyzer, generateFix, toSarif } from '../utils.js';
5
- import { deduplicateFindings } from '../dedup.js';
6
- import { applyContextFilter, detectFrameworks, applyFrameworkAdjustments } from '../context.js';
7
- import { loadConfig, shouldExcludeFile, applyConfig } from '../config.js';
8
5
 
9
6
  export const scanSecuritySchema = {
10
7
  file_path: z.string().describe("Path to the file to scan"),
11
8
  output_format: z.enum(['json', 'sarif']).optional().describe("Output format: 'json' (default) or 'sarif' for GitHub/GitLab integration"),
12
- verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level: 'minimal' (counts only), 'compact' (default, actionable info), 'full' (complete metadata)"),
13
- engine: z.enum(['auto', 'ast', 'regex']).optional().describe("Analysis engine: 'auto' (default, AST with regex fallback), 'ast' (tree-sitter only), 'regex' (regex only)")
9
+ verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level: 'minimal' (counts only), 'compact' (default, actionable info), 'full' (complete metadata)")
14
10
  };
15
11
 
16
12
  // Verbosity formatters
@@ -39,7 +35,6 @@ function formatCompact(file_path, language, issues) {
39
35
  line: i.line + 1,
40
36
  ruleId: i.ruleId,
41
37
  severity: i.severity,
42
- confidence: i.confidence || 'MEDIUM',
43
38
  message: i.message,
44
39
  fix: i.suggested_fix?.fixed ? i.suggested_fix.fixed.trim() : null
45
40
  }))
@@ -55,49 +50,26 @@ function formatFull(file_path, language, issues) {
55
50
  };
56
51
  }
57
52
 
58
- export async function scanSecurity({ file_path, output_format, verbosity, engine }) {
53
+ export async function scanSecurity({ file_path, output_format, verbosity }) {
59
54
  if (!existsSync(file_path)) {
60
55
  return {
61
56
  content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
62
57
  };
63
58
  }
64
59
 
65
- // Load project configuration
66
- const config = loadConfig(file_path);
60
+ const issues = runAnalyzer(file_path);
67
61
 
68
- // Check file exclusion
69
- if (shouldExcludeFile(file_path, config)) {
62
+ if (issues.error) {
70
63
  return {
71
- content: [{ type: "text", text: JSON.stringify({ file: file_path, message: "File excluded by configuration", issues_count: 0 }) }]
64
+ content: [{ type: "text", text: JSON.stringify(issues) }]
72
65
  };
73
66
  }
74
67
 
75
- const rawIssues = runAnalyzer(file_path, engine || 'auto');
76
-
77
- if (rawIssues.error) {
78
- return {
79
- content: [{ type: "text", text: JSON.stringify(rawIssues) }]
80
- };
81
- }
82
-
83
- // Cross-engine deduplication
84
- const dedupedIssues = deduplicateFindings(rawIssues);
85
-
86
68
  // Read file content for fix suggestions
87
69
  const content = readFileSync(file_path, 'utf-8');
88
70
  const lines = content.split('\n');
89
71
  const language = detectLanguage(file_path);
90
72
 
91
- // Context-aware filtering (suppress known module imports)
92
- const contextFiltered = applyContextFilter(dedupedIssues, file_path, language);
93
-
94
- // Framework-aware severity adjustment
95
- const frameworks = detectFrameworks(file_path, language);
96
- const frameworkAdjusted = applyFrameworkAdjustments(contextFiltered, frameworks);
97
-
98
- // Apply .scannerrc configuration (rule suppression, severity/confidence thresholds)
99
- const issues = applyConfig(frameworkAdjusted, file_path, config);
100
-
101
73
  // Enhance issues with fix suggestions
102
74
  const enhancedIssues = issues.map(issue => {
103
75
  const line = lines[issue.line] || '';
package/src/utils.js CHANGED
@@ -36,14 +36,10 @@ export function detectLanguage(filePath) {
36
36
  }
37
37
 
38
38
  // Run the Python analyzer
39
- export function runAnalyzer(filePath, engine = 'auto') {
39
+ export function runAnalyzer(filePath) {
40
40
  try {
41
41
  const analyzerPath = join(__dirname, '..', 'analyzer.py');
42
- const args = [analyzerPath, filePath];
43
- if (engine !== 'auto') {
44
- args.push('--engine', engine);
45
- }
46
- const result = execFileSync('python3', args, {
42
+ const result = execFileSync('python3', [analyzerPath, filePath], {
47
43
  encoding: 'utf-8',
48
44
  timeout: 30000
49
45
  });
@@ -53,62 +49,17 @@ export function runAnalyzer(filePath, engine = 'auto') {
53
49
  }
54
50
  }
55
51
 
56
- // Validate that a fix produces syntactically reasonable output
57
- export function validateFix(original, fixed) {
58
- if (!fixed || fixed === original) return false;
59
-
60
- // Strip escaped quotes for bracket/quote counting
61
- const unescaped = fixed.replace(/\\["'`]/g, '');
62
-
63
- // Check balanced quotes (single pass)
64
- const singleQ = (unescaped.match(/'/g) || []).length;
65
- const doubleQ = (unescaped.match(/"/g) || []).length;
66
- const backtickQ = (unescaped.match(/`/g) || []).length;
67
- if (singleQ % 2 !== 0 || doubleQ % 2 !== 0 || backtickQ % 2 !== 0) return false;
68
-
69
- // Check balanced brackets
70
- const brackets = { '(': 0, '[': 0, '{': 0 };
71
- const closers = { ')': '(', ']': '[', '}': '{' };
72
- for (const char of unescaped) {
73
- if (brackets[char] !== undefined) brackets[char]++;
74
- if (closers[char]) {
75
- brackets[closers[char]]--;
76
- if (brackets[closers[char]] < 0) return false;
77
- }
78
- }
79
- if (Object.values(brackets).some(v => v !== 0)) return false;
80
-
81
- return true;
82
- }
83
-
84
52
  // Generate fix suggestion for an issue
85
53
  export function generateFix(issue, line, language) {
86
54
  const ruleId = issue.ruleId.toLowerCase();
87
55
 
88
56
  for (const [pattern, template] of Object.entries(FIX_TEMPLATES)) {
89
57
  if (ruleId.includes(pattern)) {
90
- try {
91
- const fixed = template.fix(line, language);
92
- // Validate the fix produces reasonable output
93
- if (fixed && !validateFix(line, fixed)) {
94
- return {
95
- description: template.description + " (manual fix required)",
96
- original: line,
97
- fixed: null
98
- };
99
- }
100
- return {
101
- description: template.description,
102
- original: line,
103
- fixed: fixed
104
- };
105
- } catch {
106
- return {
107
- description: template.description + " (manual fix required)",
108
- original: line,
109
- fixed: null
110
- };
111
- }
58
+ return {
59
+ description: template.description,
60
+ original: line,
61
+ fixed: template.fix(line, language)
62
+ };
112
63
  }
113
64
  }
114
65
 
@@ -119,26 +70,6 @@ export function generateFix(issue, line, language) {
119
70
  };
120
71
  }
121
72
 
122
- // Run cross-file taint analysis
123
- export function runCrossFileAnalyzer(filePaths) {
124
- try {
125
- const analyzerPath = join(__dirname, '..', 'cross_file_analyzer.py');
126
- if (!existsSync(analyzerPath)) return [];
127
- const result = execFileSync('python3', [analyzerPath, ...filePaths], {
128
- encoding: 'utf-8',
129
- timeout: 120000,
130
- maxBuffer: 10 * 1024 * 1024
131
- });
132
- const parsed = JSON.parse(result);
133
- // Return only cross-file warnings (per-file findings are handled by scanSecurity)
134
- return Array.isArray(parsed)
135
- ? parsed.filter(f => f.ruleId === 'cross-file-taint-warning')
136
- : [];
137
- } catch {
138
- return [];
139
- }
140
- }
141
-
142
73
  // Convert issues to SARIF 2.1.0 format
143
74
  export function toSarif(file_path, language, issues) {
144
75
  const severityToLevel = {