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.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/index.js +153 -8
  3. 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
- fix: (line) => line.replace(/["']([^"']*)\s*["']\s*\+\s*(\w+)/, '"$1?", [$2]')
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 [pattern, template] of Object.entries(FIX_TEMPLATES)) {
773
- if (ruleId.includes(pattern)) {
774
- return {
775
- description: template.description,
776
- original: line,
777
- fixed: template.fix(line, language)
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.5",
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",