agent-security-scanner-mcp 3.7.0 → 3.8.0
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/README.md +42 -8
- package/analyzer.py +22 -5
- package/cross_file_analyzer.py +216 -0
- package/daemon.py +179 -0
- package/index.js +279 -3
- package/package.json +19 -5
- package/packages/npm-bloom.json +1 -0
- package/pattern_matcher.py +1 -0
- package/regex_fallback.py +199 -1
- package/requirements.txt +1 -0
- package/rules/prompt-injection.security.yaml +273 -41
- package/scripts/postinstall.js +60 -0
- package/skills/openclaw/SKILL.md +102 -0
- package/skills/security-review.md +139 -0
- package/skills/security-scan-batch.md +107 -0
- package/skills/security-scanner.md +76 -0
- package/src/cli/doctor.js +29 -1
- package/src/cli/init.js +93 -0
- package/src/cli/report.js +444 -0
- package/src/config.js +247 -0
- package/src/context.js +289 -0
- package/src/daemon-client.js +233 -0
- package/src/dedup.js +129 -0
- package/src/fix-patterns.js +76 -19
- package/src/history.js +159 -0
- package/src/tools/check-package.js +36 -12
- package/src/tools/fix-security.js +32 -5
- package/src/tools/import-resolver.js +249 -0
- package/src/tools/project-context.js +365 -0
- package/src/tools/scan-action.js +489 -0
- package/src/tools/scan-mcp.js +588 -0
- package/src/tools/scan-project.js +16 -4
- package/src/tools/scan-prompt.js +292 -527
- package/src/tools/scan-security.js +37 -6
- package/src/typosquat.js +210 -0
- package/src/utils.js +215 -8
- package/templates/gitlab-ci-security.yml +225 -0
- package/templates/pre-commit-hook.sh +233 -0
package/src/fix-patterns.js
CHANGED
|
@@ -63,24 +63,68 @@ export const FIX_TEMPLATES = {
|
|
|
63
63
|
// COMMAND INJECTION
|
|
64
64
|
// ===========================================
|
|
65
65
|
"child-process-exec": {
|
|
66
|
-
description: "Use execFile()
|
|
67
|
-
fix: (line) =>
|
|
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) -> comment with guidance (cannot safely auto-fix)
|
|
79
|
+
const varMatch = line.match(/\bexec\s*\(\s*(\w+)\s*\)/);
|
|
80
|
+
if (varMatch) {
|
|
81
|
+
return '// SECURITY: Replace exec() with execFile(command, [args], {shell: false})\n// ' + line.trim();
|
|
82
|
+
}
|
|
83
|
+
// Fallback: comment with guidance
|
|
84
|
+
return '// SECURITY: Use execFile(command, [args]) instead of exec() - ' + line.trim();
|
|
85
|
+
}
|
|
68
86
|
},
|
|
69
87
|
"spawn-shell": {
|
|
70
88
|
description: "Use spawn with shell: false",
|
|
71
89
|
fix: (line) => line.replace(/shell\s*:\s*true/i, 'shell: false')
|
|
72
90
|
},
|
|
73
91
|
"dangerous-subprocess": {
|
|
74
|
-
description: "Use subprocess.run with list arguments",
|
|
75
|
-
fix: (line) =>
|
|
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
|
+
}
|
|
76
103
|
},
|
|
77
104
|
"dangerous-system-call": {
|
|
78
105
|
description: "Use subprocess.run instead of os.system",
|
|
79
|
-
fix: (line) =>
|
|
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
|
+
}
|
|
80
116
|
},
|
|
81
117
|
"command-injection-exec": {
|
|
82
|
-
description: "Use exec.Command with separate arguments",
|
|
83
|
-
fix: (line) =>
|
|
118
|
+
description: "Use exec.Command with separate arguments (no shell)",
|
|
119
|
+
fix: (line) => {
|
|
120
|
+
// Match: exec.Command("sh", "-c", "echo "+input) -> exec.Command("echo", input)
|
|
121
|
+
const shellMatch = line.match(/exec\.Command\s*\(\s*["']sh["']\s*,\s*["']-c["']\s*,\s*["'](\w+)\s+["']\s*\+\s*(\w+)/);
|
|
122
|
+
if (shellMatch) {
|
|
123
|
+
return line.replace(/exec\.Command\s*\(\s*["']sh["']\s*,\s*["']-c["']\s*,\s*["'](\w+)\s+["']\s*\+\s*(\w+)\s*\)/, 'exec.Command("$1", $2)');
|
|
124
|
+
}
|
|
125
|
+
// Match: exec.Command("cmd " + arg) -> exec.Command("cmd", arg)
|
|
126
|
+
return line.replace(/exec\.Command\s*\(\s*["'](\w+)\s+["']\s*\+\s*(\w+)\s*\)/, 'exec.Command("$1", $2)');
|
|
127
|
+
}
|
|
84
128
|
},
|
|
85
129
|
"runtime-exec": {
|
|
86
130
|
description: "Use ProcessBuilder with separate arguments",
|
|
@@ -197,8 +241,11 @@ export const FIX_TEMPLATES = {
|
|
|
197
241
|
// INSECURE DESERIALIZATION
|
|
198
242
|
// ===========================================
|
|
199
243
|
"pickle": {
|
|
200
|
-
description: "Use JSON instead of pickle",
|
|
201
|
-
fix: (line) =>
|
|
244
|
+
description: "Use JSON instead of pickle for untrusted data",
|
|
245
|
+
fix: (line) => {
|
|
246
|
+
const fixed = line.replace(/pickle\.(load|loads)\s*\(/, 'json.$1(');
|
|
247
|
+
return fixed + ' # NOTE: data must be JSON-formatted';
|
|
248
|
+
}
|
|
202
249
|
},
|
|
203
250
|
"yaml-load": {
|
|
204
251
|
description: "Use yaml.safe_load()",
|
|
@@ -209,8 +256,8 @@ export const FIX_TEMPLATES = {
|
|
|
209
256
|
fix: (line) => line.replace(/marshal\.(load|loads)\s*\(/, 'json.$1(')
|
|
210
257
|
},
|
|
211
258
|
"shelve": {
|
|
212
|
-
description: "Use JSON or SQLite instead of shelve",
|
|
213
|
-
fix: (line) =>
|
|
259
|
+
description: "Use JSON or SQLite instead of shelve for safe storage",
|
|
260
|
+
fix: (line) => '# SECURITY: Replace shelve with json or sqlite3 for safe storage\n# ' + line.trim()
|
|
214
261
|
},
|
|
215
262
|
"node-serialize": {
|
|
216
263
|
description: "Use JSON.parse instead of node-serialize",
|
|
@@ -257,12 +304,12 @@ export const FIX_TEMPLATES = {
|
|
|
257
304
|
// PATH TRAVERSAL
|
|
258
305
|
// ===========================================
|
|
259
306
|
"path-traversal": {
|
|
260
|
-
description: "
|
|
307
|
+
description: "Resolve real path and validate prefix to prevent traversal",
|
|
261
308
|
fix: (line, lang) => {
|
|
262
|
-
if (lang === 'python') return line.replace(/open\s*\(\s*(\w+)/, 'open(os.path.
|
|
263
|
-
if (lang === 'go') return line.replace(/os\.Open\s*\(\s*(\w+)/, 'os.Open(filepath.
|
|
264
|
-
if (lang === 'java') return line.replace(/new File\s*\(\s*(\w+)/, 'new File(
|
|
265
|
-
return line.replace(/readFileSync\s*\(\s*(\w+)/, 'readFileSync(path.
|
|
309
|
+
if (lang === 'python') return line.replace(/open\s*\(\s*(\w+)/, 'open(os.path.realpath($1) # TODO: validate path prefix');
|
|
310
|
+
if (lang === 'go') return line.replace(/os\.Open\s*\(\s*(\w+)/, 'os.Open(filepath.Clean($1) // TODO: validate path prefix');
|
|
311
|
+
if (lang === 'java') return line.replace(/new File\s*\(\s*(\w+)/, 'new File($1).getCanonicalFile( // TODO: validate path prefix');
|
|
312
|
+
return line.replace(/readFileSync\s*\(\s*(\w+)/, 'readFileSync(path.resolve($1) // TODO: validate path prefix');
|
|
266
313
|
}
|
|
267
314
|
},
|
|
268
315
|
|
|
@@ -407,7 +454,14 @@ export const FIX_TEMPLATES = {
|
|
|
407
454
|
// ===========================================
|
|
408
455
|
"prototype-pollution": {
|
|
409
456
|
description: "Validate object keys before assignment",
|
|
410
|
-
fix: (line) =>
|
|
457
|
+
fix: (line) => {
|
|
458
|
+
// Only fix simple single-line assignments like: obj[key] = value
|
|
459
|
+
// Reject lines with multiple bracket accesses or chained assignments
|
|
460
|
+
if (/^\s*\w+\[\w+\]\s*=\s*[^[=]+$/.test(line)) {
|
|
461
|
+
return line.replace(/(\w+)\[(\w+)\]\s*=/, 'if (!["__proto__", "constructor", "prototype"].includes($2)) $1[$2] =');
|
|
462
|
+
}
|
|
463
|
+
return '// SECURITY: Validate key is not __proto__/constructor/prototype before assignment\n// ' + line.trim();
|
|
464
|
+
}
|
|
411
465
|
},
|
|
412
466
|
|
|
413
467
|
// ===========================================
|
|
@@ -443,7 +497,7 @@ export const FIX_TEMPLATES = {
|
|
|
443
497
|
// ===========================================
|
|
444
498
|
"helmet-missing": {
|
|
445
499
|
description: "Add helmet middleware for security headers",
|
|
446
|
-
fix: (line) => 'app.use(helmet())
|
|
500
|
+
fix: (line) => '// TODO: Add app.use(helmet()) after Express app initialization\n' + line
|
|
447
501
|
},
|
|
448
502
|
|
|
449
503
|
// ===========================================
|
|
@@ -495,7 +549,10 @@ export const FIX_TEMPLATES = {
|
|
|
495
549
|
},
|
|
496
550
|
"run-shell-form": {
|
|
497
551
|
description: "Use exec form for RUN commands",
|
|
498
|
-
fix: (line) => line.replace(/RUN\s+(.+)$/,
|
|
552
|
+
fix: (line) => line.replace(/RUN\s+(.+)$/, (_, cmd) => {
|
|
553
|
+
const escaped = cmd.replace(/"/g, '\\"');
|
|
554
|
+
return `RUN ["/bin/sh", "-c", "${escaped}"]`;
|
|
555
|
+
})
|
|
499
556
|
},
|
|
500
557
|
"sudo-in-dockerfile": {
|
|
501
558
|
description: "Avoid sudo in Dockerfile - use USER directive",
|
package/src/history.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/history.js — Scan history tracking for team dashboard.
|
|
2
|
+
// Stores results in .scanner/results/ within the scanned project directory.
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'fs';
|
|
5
|
+
import { join, basename } from 'path';
|
|
6
|
+
|
|
7
|
+
const RESULTS_DIR = '.scanner/results';
|
|
8
|
+
|
|
9
|
+
// Format a Date as YYYY-MM-DDTHH-MM-SS (filesystem-safe ISO timestamp)
|
|
10
|
+
function formatTimestamp(date) {
|
|
11
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
12
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}-${pad(date.getMinutes())}-${pad(date.getSeconds())}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Parse a YYYY-MM-DDTHH-MM-SS filename back into a Date
|
|
16
|
+
function parseTimestamp(filename) {
|
|
17
|
+
// Extract timestamp from filename like "2024-01-15T10-30-45.json"
|
|
18
|
+
const name = basename(filename, '.json');
|
|
19
|
+
const match = name.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})$/);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
const [, year, month, day, hour, min, sec] = match.map(Number);
|
|
22
|
+
return new Date(year, month - 1, day, hour, min, sec);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Save a scan result to .scanner/results/YYYY-MM-DDTHH-MM-SS.json
|
|
27
|
+
*
|
|
28
|
+
* @param {string} dirPath - The scanned project directory
|
|
29
|
+
* @param {object} scanResult - The scan result object from scanProject (parsed JSON output)
|
|
30
|
+
* @returns {string} Path to the saved result file
|
|
31
|
+
*/
|
|
32
|
+
export function saveResult(dirPath, scanResult) {
|
|
33
|
+
const resultsDir = join(dirPath, RESULTS_DIR);
|
|
34
|
+
mkdirSync(resultsDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const now = new Date();
|
|
37
|
+
const timestamp = formatTimestamp(now);
|
|
38
|
+
const filename = `${timestamp}.json`;
|
|
39
|
+
const filePath = join(resultsDir, filename);
|
|
40
|
+
|
|
41
|
+
const historyEntry = {
|
|
42
|
+
timestamp: now.toISOString(),
|
|
43
|
+
directory: scanResult.directory || dirPath,
|
|
44
|
+
grade: scanResult.grade || 'A',
|
|
45
|
+
files_scanned: scanResult.files_scanned || 0,
|
|
46
|
+
issues_count: scanResult.issues_count || scanResult.total || 0,
|
|
47
|
+
by_severity: scanResult.by_severity || { error: 0, warning: 0, info: 0 },
|
|
48
|
+
issues: scanResult.issues || [],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
writeFileSync(filePath, JSON.stringify(historyEntry, null, 2) + '\n');
|
|
52
|
+
return filePath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load scan results from .scanner/results/ within the last N days.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} dirPath - The scanned project directory
|
|
59
|
+
* @param {number} days - Number of days to look back (default: 90)
|
|
60
|
+
* @returns {object[]} Array of history entries, sorted oldest-first
|
|
61
|
+
*/
|
|
62
|
+
export function loadHistory(dirPath, days = 90) {
|
|
63
|
+
const resultsDir = join(dirPath, RESULTS_DIR);
|
|
64
|
+
if (!existsSync(resultsDir)) return [];
|
|
65
|
+
|
|
66
|
+
const cutoff = new Date();
|
|
67
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
68
|
+
|
|
69
|
+
let files;
|
|
70
|
+
try {
|
|
71
|
+
files = readdirSync(resultsDir).filter(f => f.endsWith('.json'));
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const fileDate = parseTimestamp(file);
|
|
79
|
+
if (!fileDate || fileDate < cutoff) continue;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(join(resultsDir, file), 'utf-8');
|
|
83
|
+
const entry = JSON.parse(content);
|
|
84
|
+
results.push(entry);
|
|
85
|
+
} catch {
|
|
86
|
+
// Skip corrupt files
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Sort oldest-first by timestamp
|
|
92
|
+
results.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get trend data from scan history.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} dirPath - The scanned project directory
|
|
100
|
+
* @param {number} days - Number of days to look back (default: 90)
|
|
101
|
+
* @returns {{ grades: Array<{date: string, grade: string}>, issues: Array<{date: string, total: number, critical: number, warning: number, info: number}> }}
|
|
102
|
+
*/
|
|
103
|
+
export function getTrends(dirPath, days = 90) {
|
|
104
|
+
const history = loadHistory(dirPath, days);
|
|
105
|
+
|
|
106
|
+
const grades = history.map(entry => ({
|
|
107
|
+
date: entry.timestamp,
|
|
108
|
+
grade: entry.grade,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const issues = history.map(entry => {
|
|
112
|
+
const severity = entry.by_severity || {};
|
|
113
|
+
return {
|
|
114
|
+
date: entry.timestamp,
|
|
115
|
+
total: entry.issues_count || 0,
|
|
116
|
+
critical: severity.error || 0,
|
|
117
|
+
warning: severity.warning || 0,
|
|
118
|
+
info: severity.info || 0,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return { grades, issues };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compare two scan results to find new, fixed, and unchanged issues.
|
|
127
|
+
* Issues are compared by ruleId + file + line.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} current - Current scan result (must have .issues array)
|
|
130
|
+
* @param {object} previous - Previous scan result (must have .issues array)
|
|
131
|
+
* @returns {{ new_issues: object[], fixed_issues: object[], unchanged: number }}
|
|
132
|
+
*/
|
|
133
|
+
export function diffResults(current, previous) {
|
|
134
|
+
const currentIssues = current.issues || [];
|
|
135
|
+
const previousIssues = previous.issues || [];
|
|
136
|
+
|
|
137
|
+
// Build a key for each issue: ruleId + file + line
|
|
138
|
+
function issueKey(issue) {
|
|
139
|
+
return `${issue.ruleId || ''}::${issue.file || ''}::${issue.line || 0}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const currentKeys = new Set(currentIssues.map(issueKey));
|
|
143
|
+
const previousKeys = new Set(previousIssues.map(issueKey));
|
|
144
|
+
|
|
145
|
+
// New issues: in current but not in previous
|
|
146
|
+
const newIssues = currentIssues.filter(i => !previousKeys.has(issueKey(i)));
|
|
147
|
+
|
|
148
|
+
// Fixed issues: in previous but not in current
|
|
149
|
+
const fixedIssues = previousIssues.filter(i => !currentKeys.has(issueKey(i)));
|
|
150
|
+
|
|
151
|
+
// Unchanged: in both
|
|
152
|
+
const unchanged = currentIssues.filter(i => previousKeys.has(issueKey(i))).length;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
new_issues: newIssues,
|
|
156
|
+
fixed_issues: fixedIssues,
|
|
157
|
+
unchanged,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -4,6 +4,7 @@ import { join, dirname } from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import bloomFilters from "bloom-filters";
|
|
6
6
|
const { BloomFilter } = bloomFilters;
|
|
7
|
+
import { findSimilarPackages, checkDependencyConfusion } from '../typosquat.js';
|
|
7
8
|
|
|
8
9
|
// Handle both ESM and CJS bundling (Smithery bundles to CJS)
|
|
9
10
|
let __dirname;
|
|
@@ -149,21 +150,44 @@ export async function checkPackage({ package_name, ecosystem }) {
|
|
|
149
150
|
const confidence = result.bloomFilter ? "medium" : "high";
|
|
150
151
|
const totalPackages = getTotalPackages(ecosystem);
|
|
151
152
|
|
|
153
|
+
// Enhanced response with typosquatting and dependency confusion checks
|
|
154
|
+
const response = {
|
|
155
|
+
package: package_name,
|
|
156
|
+
ecosystem,
|
|
157
|
+
legitimate: exists,
|
|
158
|
+
hallucinated: !exists,
|
|
159
|
+
confidence,
|
|
160
|
+
bloom_filter: !!result.bloomFilter,
|
|
161
|
+
total_known_packages: totalPackages,
|
|
162
|
+
recommendation: exists
|
|
163
|
+
? "Package exists in registry - safe to use"
|
|
164
|
+
: "POTENTIAL HALLUCINATION - Package not found in registry. Verify before using!"
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// If package not found, check for typosquatting
|
|
168
|
+
if (!exists) {
|
|
169
|
+
const similar = findSimilarPackages(package_name, ecosystem);
|
|
170
|
+
if (similar.length > 0) {
|
|
171
|
+
response.typosquatting = {
|
|
172
|
+
similar_packages: similar,
|
|
173
|
+
warning: `Package '${package_name}' is similar to known packages. This could be a typosquatting attack.`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for dependency confusion risk regardless of existence
|
|
179
|
+
const confusionCheck = checkDependencyConfusion(package_name);
|
|
180
|
+
if (confusionCheck.risk) {
|
|
181
|
+
response.dependency_confusion = {
|
|
182
|
+
risk: true,
|
|
183
|
+
warning: confusionCheck.warning
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
152
187
|
return {
|
|
153
188
|
content: [{
|
|
154
189
|
type: "text",
|
|
155
|
-
text: JSON.stringify(
|
|
156
|
-
package: package_name,
|
|
157
|
-
ecosystem,
|
|
158
|
-
legitimate: exists,
|
|
159
|
-
hallucinated: !exists,
|
|
160
|
-
confidence,
|
|
161
|
-
bloom_filter: !!result.bloomFilter,
|
|
162
|
-
total_known_packages: totalPackages,
|
|
163
|
-
recommendation: exists
|
|
164
|
-
? "Package exists in registry - safe to use"
|
|
165
|
-
: "⚠️ POTENTIAL HALLUCINATION - Package not found in registry. Verify before using!"
|
|
166
|
-
}, null, 2)
|
|
190
|
+
text: JSON.stringify(response, null, 2)
|
|
167
191
|
}]
|
|
168
192
|
};
|
|
169
193
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// src/tools/fix-security.js
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
|
-
import { detectLanguage,
|
|
4
|
+
import { detectLanguage, runAnalyzerAsync, 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';
|
|
5
8
|
|
|
6
9
|
export const fixSecuritySchema = {
|
|
7
10
|
file_path: z.string().describe("Path to the file to fix"),
|
|
@@ -47,24 +50,48 @@ export async function fixSecurity({ file_path, verbosity }) {
|
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
// Load project configuration
|
|
54
|
+
const config = loadConfig(file_path);
|
|
51
55
|
|
|
52
|
-
|
|
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 = await runAnalyzerAsync(file_path);
|
|
64
|
+
|
|
65
|
+
if (rawIssues.error || !Array.isArray(rawIssues) || rawIssues.length === 0) {
|
|
53
66
|
return {
|
|
54
67
|
content: [{
|
|
55
68
|
type: "text",
|
|
56
69
|
text: JSON.stringify({
|
|
57
|
-
message:
|
|
58
|
-
details:
|
|
70
|
+
message: rawIssues.error ? "Error scanning file" : "No security issues found",
|
|
71
|
+
details: rawIssues
|
|
59
72
|
})
|
|
60
73
|
}]
|
|
61
74
|
};
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
// Cross-engine deduplication
|
|
78
|
+
const dedupedIssues = deduplicateFindings(rawIssues);
|
|
79
|
+
|
|
64
80
|
// Read and fix the file
|
|
65
81
|
const content = readFileSync(file_path, 'utf-8');
|
|
66
82
|
const lines = content.split('\n');
|
|
67
83
|
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
|
+
|
|
68
95
|
const fixes = [];
|
|
69
96
|
|
|
70
97
|
// Apply fixes (process in reverse order to preserve line numbers)
|