agent-security-scanner-mcp 2.0.5 → 2.0.7

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 +106 -1
  2. package/index.js +279 -10
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -65,6 +65,22 @@ 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.7
69
+
70
+ - **SARIF output format** - `scan_security` now supports `output_format: 'sarif'` for GitHub/GitLab Security tab integration
71
+ - **GitHub Code Scanning** - Upload results directly to GitHub Advanced Security
72
+ - **GitLab SAST** - Compatible with GitLab's security dashboard
73
+ - **Full SARIF 2.1.0 compliance** - Includes rules, locations, fix suggestions, CWE/OWASP metadata
74
+
75
+ ## What's New in v2.0.6
76
+
77
+ - **fix_security reliability overhaul** - Fixes now validated before applying to prevent malformed code output
78
+ - **Python f-string SQL injection** - Now detects AND fixes `f"SELECT...{var}"` patterns
79
+ - **Python .format() SQL injection** - Now fixes `"SELECT...{}".format(var)` patterns
80
+ - **JavaScript template literal SQL injection** - Now fixes `` `SELECT...${var}` `` patterns
81
+ - **Multi-pattern fix engine** - Each vulnerability type can have multiple language-specific fix patterns
82
+ - **Syntax validation** - Rejects fixes with unbalanced quotes, brackets, or obvious syntax errors
83
+
68
84
  ## What's New in v2.0.5
69
85
 
70
86
  - **Claude Code per-project fix** - `init claude-code` now uses `claude mcp add` CLI for reliable per-project configuration
@@ -359,6 +375,7 @@ Scan a file for security vulnerabilities and return issues with suggested fixes.
359
375
  ```
360
376
  Parameters:
361
377
  file_path (string): Absolute path to the file to scan
378
+ output_format (string, optional): 'json' (default) or 'sarif' for GitHub/GitLab integration
362
379
 
363
380
  Returns:
364
381
  - List of security issues
@@ -368,7 +385,7 @@ Returns:
368
385
  - Suggested fixes
369
386
  ```
370
387
 
371
- **Example output:**
388
+ **Example output (JSON - default):**
372
389
  ```json
373
390
  {
374
391
  "file": "/path/to/file.js",
@@ -394,6 +411,36 @@ Returns:
394
411
  }
395
412
  ```
396
413
 
414
+ **Example output (SARIF - for GitHub/GitLab):**
415
+ ```json
416
+ {
417
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
418
+ "version": "2.1.0",
419
+ "runs": [{
420
+ "tool": {
421
+ "driver": {
422
+ "name": "agent-security-scanner-mcp",
423
+ "version": "2.0.7",
424
+ "rules": [...]
425
+ }
426
+ },
427
+ "results": [
428
+ {
429
+ "ruleId": "sql-injection",
430
+ "level": "error",
431
+ "message": { "text": "SQL Injection detected" },
432
+ "locations": [{
433
+ "physicalLocation": {
434
+ "artifactLocation": { "uri": "file.js" },
435
+ "region": { "startLine": 15 }
436
+ }
437
+ }]
438
+ }
439
+ ]
440
+ }]
441
+ }
442
+ ```
443
+
397
444
  ### `fix_security`
398
445
 
399
446
  Automatically fix all security issues in a file.
@@ -631,6 +678,64 @@ Package lists are sourced from [garak-llm](https://huggingface.co/garak-llm) Hug
631
678
 
632
679
  ---
633
680
 
681
+ ## CI/CD Integration (SARIF)
682
+
683
+ Upload scan results to GitHub Security tab or GitLab Security Dashboard using SARIF format.
684
+
685
+ ### GitHub Actions Example
686
+
687
+ ```yaml
688
+ name: Security Scan
689
+ on: [push, pull_request]
690
+
691
+ jobs:
692
+ security:
693
+ runs-on: ubuntu-latest
694
+ steps:
695
+ - uses: actions/checkout@v4
696
+
697
+ - name: Setup Node.js
698
+ uses: actions/setup-node@v4
699
+ with:
700
+ node-version: '20'
701
+
702
+ - name: Run Security Scanner
703
+ run: |
704
+ npx agent-security-scanner-mcp scan src/ --format sarif --output results.sarif
705
+
706
+ - name: Upload SARIF to GitHub
707
+ uses: github/codeql-action/upload-sarif@v3
708
+ with:
709
+ sarif_file: results.sarif
710
+ ```
711
+
712
+ ### GitLab CI Example
713
+
714
+ ```yaml
715
+ security_scan:
716
+ stage: test
717
+ script:
718
+ - npx agent-security-scanner-mcp scan src/ --format sarif --output gl-sast-report.json
719
+ artifacts:
720
+ reports:
721
+ sast: gl-sast-report.json
722
+ ```
723
+
724
+ ### Programmatic Usage
725
+
726
+ ```javascript
727
+ // Use output_format: 'sarif' parameter
728
+ const result = await client.callTool({
729
+ name: 'scan_security',
730
+ arguments: {
731
+ file_path: '/path/to/file.js',
732
+ output_format: 'sarif' // Returns SARIF 2.1.0 format
733
+ }
734
+ });
735
+ ```
736
+
737
+ ---
738
+
634
739
  ## Security Rules (359 total)
635
740
 
636
741
  ### By Language
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
 
@@ -804,14 +949,126 @@ export function createSandboxServer() {
804
949
  return server;
805
950
  }
806
951
 
952
+ // SARIF (Static Analysis Results Interchange Format) conversion
953
+ // Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
954
+ function convertToSarif(filePath, language, issues) {
955
+ const severityToLevel = {
956
+ 'ERROR': 'error',
957
+ 'WARNING': 'warning',
958
+ 'INFO': 'note',
959
+ 'HINT': 'note'
960
+ };
961
+
962
+ // Build rules from unique rule IDs
963
+ const rulesMap = new Map();
964
+ issues.forEach(issue => {
965
+ if (!rulesMap.has(issue.ruleId)) {
966
+ rulesMap.set(issue.ruleId, {
967
+ id: issue.ruleId,
968
+ name: issue.ruleId.split('.').pop().replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
969
+ shortDescription: {
970
+ text: issue.message.replace(/^\[.*?\]\s*/, '') // Remove [RuleName] prefix
971
+ },
972
+ defaultConfiguration: {
973
+ level: severityToLevel[issue.severity] || 'warning'
974
+ },
975
+ properties: {
976
+ tags: ['security'],
977
+ ...(issue.metadata?.cwe && { 'security-severity': '7.0' }),
978
+ },
979
+ helpUri: issue.metadata?.references?.[0] || `https://cwe.mitre.org/data/definitions/${issue.metadata?.cwe?.replace('CWE-', '')}.html`
980
+ });
981
+ }
982
+ });
983
+
984
+ // Build results
985
+ const results = issues.map(issue => ({
986
+ ruleId: issue.ruleId,
987
+ level: severityToLevel[issue.severity] || 'warning',
988
+ message: {
989
+ text: issue.message
990
+ },
991
+ locations: [{
992
+ physicalLocation: {
993
+ artifactLocation: {
994
+ uri: filePath,
995
+ uriBaseId: '%SRCROOT%'
996
+ },
997
+ region: {
998
+ startLine: (issue.line || 0) + 1, // SARIF uses 1-indexed lines
999
+ startColumn: (issue.column || 0) + 1,
1000
+ endLine: (issue.endLine || issue.line || 0) + 1,
1001
+ endColumn: (issue.endColumn || issue.column || 0) + 1,
1002
+ snippet: issue.line_content ? { text: issue.line_content } : undefined
1003
+ }
1004
+ }
1005
+ }],
1006
+ ...(issue.suggested_fix?.fixed && {
1007
+ fixes: [{
1008
+ description: {
1009
+ text: issue.suggested_fix.description
1010
+ },
1011
+ artifactChanges: [{
1012
+ artifactLocation: {
1013
+ uri: filePath
1014
+ },
1015
+ replacements: [{
1016
+ deletedRegion: {
1017
+ startLine: (issue.line || 0) + 1,
1018
+ startColumn: 1,
1019
+ endLine: (issue.line || 0) + 1,
1020
+ endColumn: (issue.suggested_fix.original?.length || 0) + 1
1021
+ },
1022
+ insertedContent: {
1023
+ text: issue.suggested_fix.fixed
1024
+ }
1025
+ }]
1026
+ }]
1027
+ }]
1028
+ }),
1029
+ properties: {
1030
+ ...(issue.metadata?.cwe && { cwe: issue.metadata.cwe }),
1031
+ ...(issue.metadata?.owasp && { owasp: issue.metadata.owasp })
1032
+ }
1033
+ }));
1034
+
1035
+ return {
1036
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
1037
+ version: '2.1.0',
1038
+ runs: [{
1039
+ tool: {
1040
+ driver: {
1041
+ name: 'agent-security-scanner-mcp',
1042
+ version: '2.0.7',
1043
+ informationUri: 'https://github.com/sinewaveai/agent-security-scanner-mcp',
1044
+ rules: Array.from(rulesMap.values())
1045
+ }
1046
+ },
1047
+ results,
1048
+ invocations: [{
1049
+ executionSuccessful: true,
1050
+ endTimeUtc: new Date().toISOString()
1051
+ }],
1052
+ artifacts: [{
1053
+ location: {
1054
+ uri: filePath,
1055
+ uriBaseId: '%SRCROOT%'
1056
+ },
1057
+ sourceLanguage: language
1058
+ }]
1059
+ }]
1060
+ };
1061
+ }
1062
+
807
1063
  // Register scan_security tool
808
1064
  server.tool(
809
1065
  "scan_security",
810
1066
  "Scan a file for security vulnerabilities and return issues with suggested fixes",
811
1067
  {
812
- file_path: z.string().describe("Path to the file to scan")
1068
+ file_path: z.string().describe("Path to the file to scan"),
1069
+ output_format: z.enum(['json', 'sarif']).optional().describe("Output format: 'json' (default) or 'sarif' for GitHub/GitLab integration")
813
1070
  },
814
- async ({ file_path }) => {
1071
+ async ({ file_path, output_format = 'json' }) => {
815
1072
  if (!existsSync(file_path)) {
816
1073
  return {
817
1074
  content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
@@ -842,6 +1099,18 @@ server.tool(
842
1099
  };
843
1100
  });
844
1101
 
1102
+ // Return SARIF format if requested (for GitHub/GitLab integration)
1103
+ if (output_format === 'sarif') {
1104
+ const sarif = convertToSarif(file_path, language, enhancedIssues);
1105
+ return {
1106
+ content: [{
1107
+ type: "text",
1108
+ text: JSON.stringify(sarif, null, 2)
1109
+ }]
1110
+ };
1111
+ }
1112
+
1113
+ // Default JSON format
845
1114
  return {
846
1115
  content: [{
847
1116
  type: "text",
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.7",
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",
@@ -52,7 +52,10 @@
52
52
  "zed",
53
53
  "prompt-firewall",
54
54
  "auto-fix",
55
- "hallucination"
55
+ "hallucination",
56
+ "sarif",
57
+ "github-code-scanning",
58
+ "gitlab-sast"
56
59
  ],
57
60
  "author": "Sinewave AI <divya@sinewave.ai>",
58
61
  "license": "MIT",