agent-security-scanner-mcp 2.0.5 → 2.0.6
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 +9 -0
- package/index.js +153 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,6 +65,15 @@ The scanner works without tree-sitter using regex-based detection, but AST analy
|
|
|
65
65
|
|
|
66
66
|
---
|
|
67
67
|
|
|
68
|
+
## What's New in v2.0.6
|
|
69
|
+
|
|
70
|
+
- **fix_security reliability overhaul** - Fixes now validated before applying to prevent malformed code output
|
|
71
|
+
- **Python f-string SQL injection** - Now detects AND fixes `f"SELECT...{var}"` patterns
|
|
72
|
+
- **Python .format() SQL injection** - Now fixes `"SELECT...{}".format(var)` patterns
|
|
73
|
+
- **JavaScript template literal SQL injection** - Now fixes `` `SELECT...${var}` `` patterns
|
|
74
|
+
- **Multi-pattern fix engine** - Each vulnerability type can have multiple language-specific fix patterns
|
|
75
|
+
- **Syntax validation** - Rejects fixes with unbalanced quotes, brackets, or obvious syntax errors
|
|
76
|
+
|
|
68
77
|
## What's New in v2.0.5
|
|
69
78
|
|
|
70
79
|
- **Claude Code per-project fix** - `init claude-code` now uses `claude mcp add` CLI for reliable per-project configuration
|
package/index.js
CHANGED
|
@@ -28,7 +28,73 @@ const FIX_TEMPLATES = {
|
|
|
28
28
|
// ===========================================
|
|
29
29
|
"sql-injection": {
|
|
30
30
|
description: "Use parameterized queries instead of string concatenation",
|
|
31
|
-
|
|
31
|
+
patterns: [
|
|
32
|
+
// Python f-strings: cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
|
|
33
|
+
{
|
|
34
|
+
match: /f["'].*(?:SELECT|INSERT|UPDATE|DELETE).*\{(\w+)\}.*["']/i,
|
|
35
|
+
fix: (line) => {
|
|
36
|
+
// Extract the query and variable
|
|
37
|
+
const match = line.match(/(\w+\.(?:execute|query|run))\s*\(\s*f(["'])(.*?)(?:SELECT|INSERT|UPDATE|DELETE)(.*?)\{(\w+)\}(.*?)\2/i);
|
|
38
|
+
if (match) {
|
|
39
|
+
const [, method, quote, prefix, queryStart, varName, suffix] = match;
|
|
40
|
+
// Reconstruct as parameterized query
|
|
41
|
+
const cleanPrefix = prefix.replace(/\{[^}]+\}/g, '?');
|
|
42
|
+
const cleanSuffix = suffix.replace(/\{[^}]+\}/g, '?');
|
|
43
|
+
return line.replace(
|
|
44
|
+
/f(["']).*\1/,
|
|
45
|
+
`"${cleanPrefix}${queryStart.trim().toUpperCase()}${cleanSuffix}?", (${varName},)`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
// Simpler fallback for f-strings
|
|
49
|
+
return line.replace(/f(["'])(.*?)\{(\w+)\}(.*?)\1/, '"$2?$4", ($3,)');
|
|
50
|
+
},
|
|
51
|
+
languages: ['python']
|
|
52
|
+
},
|
|
53
|
+
// Python .format(): "SELECT ... WHERE id = {}".format(user_id)
|
|
54
|
+
{
|
|
55
|
+
match: /["'].*(?:SELECT|INSERT|UPDATE|DELETE).*\{\}.*["']\.format\s*\(/i,
|
|
56
|
+
fix: (line) => {
|
|
57
|
+
return line.replace(
|
|
58
|
+
/(["'])(.*?)\{\}(.*?)\1\.format\s*\(\s*(\w+)\s*\)/,
|
|
59
|
+
'"$2?$3", [$4]'
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
languages: ['python']
|
|
63
|
+
},
|
|
64
|
+
// Python % formatting: "SELECT ... WHERE id = %s" % user_id
|
|
65
|
+
{
|
|
66
|
+
match: /["'].*(?:SELECT|INSERT|UPDATE|DELETE).*%s.*["']\s*%\s*\(/i,
|
|
67
|
+
fix: (line) => {
|
|
68
|
+
return line.replace(
|
|
69
|
+
/(["'])(.*?)%s(.*?)\1\s*%\s*\(\s*(\w+)\s*,?\s*\)/,
|
|
70
|
+
'"$2?$3", [$4]'
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
languages: ['python']
|
|
74
|
+
},
|
|
75
|
+
// JS template literals: `SELECT * FROM users WHERE id = ${userId}`
|
|
76
|
+
{
|
|
77
|
+
match: /`.*(?:SELECT|INSERT|UPDATE|DELETE).*\$\{.*\}.*`/i,
|
|
78
|
+
fix: (line) => {
|
|
79
|
+
return line.replace(
|
|
80
|
+
/`(.*?)\$\{(\w+)\}(.*?)`/,
|
|
81
|
+
'"$1?$3", [$2]'
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
languages: ['javascript', 'typescript']
|
|
85
|
+
},
|
|
86
|
+
// Simple concatenation (no quotes inside): "SELECT ... WHERE id = " + userId
|
|
87
|
+
{
|
|
88
|
+
match: /["'](?:SELECT|INSERT|UPDATE|DELETE)[^"']+["']\s*\+\s*\w+(?!\s*\+\s*["'])/i,
|
|
89
|
+
fix: (line) => {
|
|
90
|
+
return line.replace(
|
|
91
|
+
/(["'])((?:SELECT|INSERT|UPDATE|DELETE)[^"']+)\1\s*\+\s*(\w+)/i,
|
|
92
|
+
'"$2?", [$3]'
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
languages: ['javascript', 'python', 'java', 'go', 'ruby', 'php']
|
|
96
|
+
}
|
|
97
|
+
]
|
|
32
98
|
},
|
|
33
99
|
"nosql-injection": {
|
|
34
100
|
description: "Sanitize MongoDB query inputs",
|
|
@@ -765,17 +831,96 @@ function runAnalyzer(filePath) {
|
|
|
765
831
|
}
|
|
766
832
|
}
|
|
767
833
|
|
|
834
|
+
// Validate that a fix produces valid syntax
|
|
835
|
+
function validateFix(original, fixed, language) {
|
|
836
|
+
// Rule 1: Fix must be different from original
|
|
837
|
+
if (fixed === original || !fixed) {
|
|
838
|
+
return { valid: false, reason: 'no_change' };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Rule 2: Balanced quotes (ignore escaped quotes)
|
|
842
|
+
const unescaped = fixed.replace(/\\["'`]/g, '');
|
|
843
|
+
const singleQuotes = (unescaped.match(/'/g) || []).length;
|
|
844
|
+
const doubleQuotes = (unescaped.match(/"/g) || []).length;
|
|
845
|
+
const backticks = (unescaped.match(/`/g) || []).length;
|
|
846
|
+
if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0) {
|
|
847
|
+
return { valid: false, reason: 'unbalanced_quotes' };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Rule 3: Balanced brackets
|
|
851
|
+
const brackets = { '(': 0, '[': 0, '{': 0 };
|
|
852
|
+
const closers = { ')': '(', ']': '[', '}': '{' };
|
|
853
|
+
for (const char of unescaped) {
|
|
854
|
+
if (brackets[char] !== undefined) brackets[char]++;
|
|
855
|
+
if (closers[char]) brackets[closers[char]]--;
|
|
856
|
+
}
|
|
857
|
+
if (Object.values(brackets).some(v => v !== 0)) {
|
|
858
|
+
return { valid: false, reason: 'unbalanced_brackets' };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Rule 4: No obvious syntax errors
|
|
862
|
+
const badPatterns = [
|
|
863
|
+
/""[^,\s\]);}]/, // empty string followed by unexpected char
|
|
864
|
+
/\+\s*[)\]}]/, // + followed by closing bracket
|
|
865
|
+
/,\s*\+/, // comma followed by +
|
|
866
|
+
/\(\s*\+/, // open paren followed by +
|
|
867
|
+
];
|
|
868
|
+
for (const pattern of badPatterns) {
|
|
869
|
+
if (pattern.test(fixed)) {
|
|
870
|
+
return { valid: false, reason: 'syntax_error' };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return { valid: true };
|
|
875
|
+
}
|
|
876
|
+
|
|
768
877
|
// Generate fix suggestion for an issue
|
|
769
878
|
function generateFix(issue, line, language) {
|
|
770
879
|
const ruleId = issue.ruleId.toLowerCase();
|
|
771
880
|
|
|
772
|
-
for (const [
|
|
773
|
-
if (ruleId.includes(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
881
|
+
for (const [templateId, template] of Object.entries(FIX_TEMPLATES)) {
|
|
882
|
+
if (!ruleId.includes(templateId)) continue;
|
|
883
|
+
|
|
884
|
+
// New: handle patterns array
|
|
885
|
+
if (template.patterns && Array.isArray(template.patterns)) {
|
|
886
|
+
for (const pattern of template.patterns) {
|
|
887
|
+
// Skip if language doesn't match
|
|
888
|
+
if (pattern.languages && !pattern.languages.includes(language)) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Skip if pattern doesn't match the line
|
|
893
|
+
if (!pattern.match.test(line)) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Try the fix
|
|
898
|
+
const candidate = pattern.fix(line, language);
|
|
899
|
+
const validation = validateFix(line, candidate, language);
|
|
900
|
+
|
|
901
|
+
if (validation.valid) {
|
|
902
|
+
return {
|
|
903
|
+
description: template.description,
|
|
904
|
+
original: line,
|
|
905
|
+
fixed: candidate
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
// If invalid, try next pattern
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Fallback: old-style single fix function (backward compatible)
|
|
913
|
+
if (template.fix && typeof template.fix === 'function') {
|
|
914
|
+
const candidate = template.fix(line, language);
|
|
915
|
+
const validation = validateFix(line, candidate, language);
|
|
916
|
+
|
|
917
|
+
if (validation.valid) {
|
|
918
|
+
return {
|
|
919
|
+
description: template.description,
|
|
920
|
+
original: line,
|
|
921
|
+
fixed: candidate
|
|
922
|
+
};
|
|
923
|
+
}
|
|
779
924
|
}
|
|
780
925
|
}
|
|
781
926
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
5
5
|
"description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 359 vulnerability rules with auto-fix. For Claude Code, Cursor, Windsurf, Cline.",
|
|
6
6
|
"main": "index.js",
|