agent-security-scanner-mcp 3.1.0 → 3.3.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.
@@ -0,0 +1,117 @@
1
+ // src/tools/scan-security.js
2
+ import { z } from "zod";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { detectLanguage, runAnalyzer, generateFix, toSarif } from '../utils.js';
5
+
6
+ export const scanSecuritySchema = {
7
+ file_path: z.string().describe("Path to the file to scan"),
8
+ output_format: z.enum(['json', 'sarif']).optional().describe("Output format: 'json' (default) or 'sarif' for GitHub/GitLab integration"),
9
+ verbosity: z.enum(['minimal', 'compact', 'full']).optional().describe("Response detail level: 'minimal' (counts only), 'compact' (default, actionable info), 'full' (complete metadata)")
10
+ };
11
+
12
+ // Verbosity formatters
13
+ function formatMinimal(file_path, language, issues) {
14
+ const bySeverity = { error: 0, warning: 0, info: 0 };
15
+ issues.forEach(i => bySeverity[i.severity] = (bySeverity[i.severity] || 0) + 1);
16
+ return {
17
+ file: file_path,
18
+ language,
19
+ total: issues.length,
20
+ critical: bySeverity.error,
21
+ warning: bySeverity.warning,
22
+ info: bySeverity.info,
23
+ message: issues.length > 0
24
+ ? `Found ${issues.length} issue(s). Use verbosity='compact' for details.`
25
+ : "No security issues found."
26
+ };
27
+ }
28
+
29
+ function formatCompact(file_path, language, issues) {
30
+ return {
31
+ file: file_path,
32
+ language,
33
+ issues_count: issues.length,
34
+ issues: issues.map(i => ({
35
+ line: i.line + 1,
36
+ ruleId: i.ruleId,
37
+ severity: i.severity,
38
+ message: i.message,
39
+ fix: i.suggested_fix?.fixed ? i.suggested_fix.fixed.trim() : null
40
+ }))
41
+ };
42
+ }
43
+
44
+ function formatFull(file_path, language, issues) {
45
+ return {
46
+ file: file_path,
47
+ language,
48
+ issues_count: issues.length,
49
+ issues: issues
50
+ };
51
+ }
52
+
53
+ export async function scanSecurity({ file_path, output_format, verbosity }) {
54
+ if (!existsSync(file_path)) {
55
+ return {
56
+ content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
57
+ };
58
+ }
59
+
60
+ const issues = runAnalyzer(file_path);
61
+
62
+ if (issues.error) {
63
+ return {
64
+ content: [{ type: "text", text: JSON.stringify(issues) }]
65
+ };
66
+ }
67
+
68
+ // Read file content for fix suggestions
69
+ const content = readFileSync(file_path, 'utf-8');
70
+ const lines = content.split('\n');
71
+ const language = detectLanguage(file_path);
72
+
73
+ // Enhance issues with fix suggestions
74
+ const enhancedIssues = issues.map(issue => {
75
+ const line = lines[issue.line] || '';
76
+ const fix = generateFix(issue, line, language);
77
+ return {
78
+ ...issue,
79
+ line_content: line.trim(),
80
+ suggested_fix: fix
81
+ };
82
+ });
83
+
84
+ // Determine verbosity (default: compact)
85
+ const level = verbosity || 'compact';
86
+
87
+ // Return SARIF format if requested (always full detail)
88
+ if (output_format === 'sarif') {
89
+ return {
90
+ content: [{
91
+ type: "text",
92
+ text: JSON.stringify(toSarif(file_path, language, enhancedIssues), null, 2)
93
+ }]
94
+ };
95
+ }
96
+
97
+ // Format based on verbosity
98
+ let result;
99
+ switch (level) {
100
+ case 'minimal':
101
+ result = formatMinimal(file_path, language, enhancedIssues);
102
+ break;
103
+ case 'full':
104
+ result = formatFull(file_path, language, enhancedIssues);
105
+ break;
106
+ case 'compact':
107
+ default:
108
+ result = formatCompact(file_path, language, enhancedIssues);
109
+ }
110
+
111
+ return {
112
+ content: [{
113
+ type: "text",
114
+ text: JSON.stringify(result, null, 2)
115
+ }]
116
+ };
117
+ }
package/src/utils.js ADDED
@@ -0,0 +1,153 @@
1
+ import { execFileSync } from "child_process";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { dirname, join, extname, basename } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { FIX_TEMPLATES } from './fix-patterns.js';
6
+
7
+ // Handle both ESM and CJS bundling (Smithery bundles to CJS)
8
+ let __dirname;
9
+ try {
10
+ __dirname = dirname(fileURLToPath(import.meta.url));
11
+ } catch {
12
+ __dirname = process.cwd();
13
+ }
14
+
15
+ // Detect language from file extension
16
+ export function detectLanguage(filePath) {
17
+ // Check basename first for extensionless files like Dockerfile
18
+ const base = filePath.split('/').pop().split('\\').pop().toLowerCase();
19
+ if (base === 'dockerfile' || base.startsWith('dockerfile.')) return 'dockerfile';
20
+
21
+ const ext = filePath.split('.').pop().toLowerCase();
22
+ const langMap = {
23
+ 'py': 'python', 'js': 'javascript', 'ts': 'typescript',
24
+ 'tsx': 'typescript', 'jsx': 'javascript', 'java': 'java',
25
+ 'go': 'go', 'rb': 'ruby', 'php': 'php',
26
+ 'cs': 'csharp', 'rs': 'rust', 'c': 'c', 'cpp': 'cpp',
27
+ 'cc': 'cpp', 'cxx': 'cpp', 'h': 'c', 'hpp': 'cpp',
28
+ 'tf': 'terraform', 'hcl': 'terraform',
29
+ 'yaml': 'generic', 'yml': 'generic',
30
+ 'sql': 'sql',
31
+ // Prompt/text file extensions for prompt injection scanning
32
+ 'txt': 'generic', 'md': 'generic', 'prompt': 'generic',
33
+ 'jinja': 'generic', 'jinja2': 'generic', 'j2': 'generic'
34
+ };
35
+ return langMap[ext] || 'generic';
36
+ }
37
+
38
+ // Run the Python analyzer
39
+ export function runAnalyzer(filePath) {
40
+ try {
41
+ const analyzerPath = join(__dirname, '..', 'analyzer.py');
42
+ const result = execFileSync('python3', [analyzerPath, filePath], {
43
+ encoding: 'utf-8',
44
+ timeout: 30000
45
+ });
46
+ return JSON.parse(result);
47
+ } catch (error) {
48
+ return { error: error.message };
49
+ }
50
+ }
51
+
52
+ // Generate fix suggestion for an issue
53
+ export function generateFix(issue, line, language) {
54
+ const ruleId = issue.ruleId.toLowerCase();
55
+
56
+ for (const [pattern, template] of Object.entries(FIX_TEMPLATES)) {
57
+ if (ruleId.includes(pattern)) {
58
+ return {
59
+ description: template.description,
60
+ original: line,
61
+ fixed: template.fix(line, language)
62
+ };
63
+ }
64
+ }
65
+
66
+ return {
67
+ description: "Review and fix manually based on the security rule",
68
+ original: line,
69
+ fixed: null
70
+ };
71
+ }
72
+
73
+ // Convert issues to SARIF 2.1.0 format
74
+ export function toSarif(file_path, language, issues) {
75
+ const severityToLevel = {
76
+ 'error': 'error',
77
+ 'ERROR': 'error',
78
+ 'warning': 'warning',
79
+ 'WARNING': 'warning',
80
+ 'info': 'note',
81
+ 'INFO': 'note'
82
+ };
83
+
84
+ // Build unique rules from issues
85
+ const rulesMap = new Map();
86
+ for (const issue of issues) {
87
+ if (!rulesMap.has(issue.ruleId)) {
88
+ rulesMap.set(issue.ruleId, {
89
+ id: issue.ruleId,
90
+ shortDescription: { text: issue.message },
91
+ defaultConfiguration: {
92
+ level: severityToLevel[issue.severity] || 'warning'
93
+ },
94
+ properties: issue.metadata || {}
95
+ });
96
+ }
97
+ }
98
+
99
+ // Build results
100
+ const results = issues.map(issue => {
101
+ const result = {
102
+ ruleId: issue.ruleId,
103
+ level: severityToLevel[issue.severity] || 'warning',
104
+ message: { text: issue.message },
105
+ locations: [{
106
+ physicalLocation: {
107
+ artifactLocation: { uri: file_path },
108
+ region: {
109
+ startLine: (issue.line || 0) + 1,
110
+ startColumn: (issue.column || 0) + 1
111
+ }
112
+ }
113
+ }]
114
+ };
115
+
116
+ // Add fix if available
117
+ if (issue.suggested_fix && issue.suggested_fix.fixed) {
118
+ result.fixes = [{
119
+ description: { text: issue.suggested_fix.description || 'Apply security fix' },
120
+ artifactChanges: [{
121
+ artifactLocation: { uri: file_path },
122
+ replacements: [{
123
+ deletedRegion: {
124
+ startLine: (issue.line || 0) + 1,
125
+ startColumn: 1,
126
+ endLine: (issue.line || 0) + 1,
127
+ endColumn: (issue.line_content?.length || 0) + 1
128
+ },
129
+ insertedContent: { text: issue.suggested_fix.fixed }
130
+ }]
131
+ }]
132
+ }];
133
+ }
134
+
135
+ return result;
136
+ });
137
+
138
+ return {
139
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
140
+ version: '2.1.0',
141
+ runs: [{
142
+ tool: {
143
+ driver: {
144
+ name: 'agent-security-scanner-mcp',
145
+ version: '3.1.0',
146
+ informationUri: 'https://github.com/sinewaveai/agent-security-scanner-mcp',
147
+ rules: Array.from(rulesMap.values())
148
+ }
149
+ },
150
+ results: results
151
+ }]
152
+ };
153
+ }