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.
- package/README.md +128 -2
- package/index.js +119 -2427
- package/package.json +11 -4
- package/rules/openclaw.security.yaml +283 -0
- package/skills/openclaw/SKILL.md +102 -0
- package/skills/security-scan-batch.md +107 -0
- package/skills/security-scanner.md +76 -0
- package/src/analyzer.py +119 -0
- package/src/cli/demo.js +238 -0
- package/src/cli/doctor.js +273 -0
- package/src/cli/init.js +381 -0
- package/src/fix-patterns.js +698 -0
- package/src/tools/check-package.js +169 -0
- package/src/tools/fix-security.js +115 -0
- package/src/tools/scan-packages.js +154 -0
- package/src/tools/scan-prompt.js +640 -0
- package/src/tools/scan-security.js +117 -0
- package/src/utils.js +153 -0
|
@@ -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
|
+
}
|