agent-security-scanner-mcp 4.0.0 → 4.1.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 +47 -58
- package/code-review-agent/README.md +25 -4
- package/code-review-agent/TODO.md +1 -1
- package/code-review-agent/bin/cr-agent.ts +7 -1
- package/code-review-agent/dist/bin/cr-agent.js +7 -1
- package/code-review-agent/dist/bin/cr-agent.js.map +1 -1
- package/code-review-agent/dist/src/analyzer/engine.d.ts +5 -0
- package/code-review-agent/dist/src/analyzer/engine.d.ts.map +1 -1
- package/code-review-agent/dist/src/analyzer/engine.js +30 -3
- package/code-review-agent/dist/src/analyzer/engine.js.map +1 -1
- package/code-review-agent/dist/src/analyzer/postprocess.d.ts +15 -0
- package/code-review-agent/dist/src/analyzer/postprocess.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/postprocess.js +275 -0
- package/code-review-agent/dist/src/analyzer/postprocess.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/semantic.d.ts +5 -1
- package/code-review-agent/dist/src/analyzer/semantic.d.ts.map +1 -1
- package/code-review-agent/dist/src/analyzer/semantic.js +80 -20
- package/code-review-agent/dist/src/analyzer/semantic.js.map +1 -1
- package/code-review-agent/dist/src/context/assembler.d.ts +8 -2
- package/code-review-agent/dist/src/context/assembler.d.ts.map +1 -1
- package/code-review-agent/dist/src/context/assembler.js +33 -1
- package/code-review-agent/dist/src/context/assembler.js.map +1 -1
- package/code-review-agent/dist/src/context/file.d.ts.map +1 -1
- package/code-review-agent/dist/src/context/file.js +11 -23
- package/code-review-agent/dist/src/context/file.js.map +1 -1
- package/code-review-agent/dist/src/context/security-summary.d.ts +19 -0
- package/code-review-agent/dist/src/context/security-summary.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/security-summary.js +199 -0
- package/code-review-agent/dist/src/context/security-summary.js.map +1 -0
- package/code-review-agent/dist/src/graph/dependency.d.ts.map +1 -1
- package/code-review-agent/dist/src/graph/dependency.js +8 -1
- package/code-review-agent/dist/src/graph/dependency.js.map +1 -1
- package/code-review-agent/dist/src/graph/resolver.d.ts.map +1 -1
- package/code-review-agent/dist/src/graph/resolver.js +14 -5
- package/code-review-agent/dist/src/graph/resolver.js.map +1 -1
- package/code-review-agent/dist/src/index.d.ts +4 -1
- package/code-review-agent/dist/src/index.d.ts.map +1 -1
- package/code-review-agent/dist/src/index.js +2 -0
- package/code-review-agent/dist/src/index.js.map +1 -1
- package/code-review-agent/dist/src/types/config.d.ts +3 -0
- package/code-review-agent/dist/src/types/config.d.ts.map +1 -1
- package/code-review-agent/dist/src/types/config.js +9 -0
- package/code-review-agent/dist/src/types/config.js.map +1 -1
- package/code-review-agent/src/analyzer/engine.ts +36 -2
- package/code-review-agent/src/analyzer/postprocess.ts +311 -0
- package/code-review-agent/src/analyzer/semantic.ts +87 -18
- package/code-review-agent/src/context/assembler.ts +44 -2
- package/code-review-agent/src/context/file.ts +13 -18
- package/code-review-agent/src/context/security-summary.ts +225 -0
- package/code-review-agent/src/graph/dependency.ts +8 -1
- package/code-review-agent/src/graph/resolver.ts +14 -5
- package/code-review-agent/src/index.ts +4 -0
- package/code-review-agent/src/types/config.ts +16 -0
- package/code-review-agent/tests/analyzer/engine.test.ts +5 -0
- package/code-review-agent/tests/analyzer/postprocess.test.ts +450 -0
- package/code-review-agent/tests/analyzer/prompt-routing.test.ts +137 -0
- package/code-review-agent/tests/config-mode.test.ts +71 -0
- package/code-review-agent/tests/context/file.test.ts +16 -1
- package/code-review-agent/tests/context/security-summary.test.ts +181 -0
- package/code-review-agent/tests/fixtures/guarded-agent/router.py +6 -0
- package/code-review-agent/tests/fixtures/guarded-agent/tools/executor.py +10 -0
- package/code-review-agent/tests/fixtures/guarded-agent/tools/guard.py +4 -0
- package/code-review-agent/tests/fixtures/guarded-agent/vuln-tool.py +6 -0
- package/code-review-agent/tests/graph/dependency.test.ts +76 -0
- package/index.js +18 -18
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +43 -4
- package/server.json +1 -1
- package/src/cli/init-hooks.js +3 -3
- package/src/cli/init.js +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
buildRelatedFileSummaries,
|
|
5
|
+
formatRelatedFileSummaries,
|
|
6
|
+
} from '../../src/context/security-summary.js';
|
|
7
|
+
import type { FileContext } from '../../src/types/analysis.js';
|
|
8
|
+
|
|
9
|
+
const FIXTURES_DIR = path.resolve(__dirname, '..', 'fixtures', 'guarded-agent');
|
|
10
|
+
|
|
11
|
+
function makeFileContext(overrides: Partial<FileContext> = {}): FileContext {
|
|
12
|
+
return {
|
|
13
|
+
filePath: 'router.py',
|
|
14
|
+
content: 'from tools.executor import execute_tool\n',
|
|
15
|
+
language: 'python',
|
|
16
|
+
lineCount: 5,
|
|
17
|
+
imports: ['./tools/executor'],
|
|
18
|
+
importedBy: [],
|
|
19
|
+
siblingFiles: ['vuln-tool.py', 'tools'],
|
|
20
|
+
isTestFile: false,
|
|
21
|
+
isConfigFile: false,
|
|
22
|
+
isGenerated: false,
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('buildRelatedFileSummaries', () => {
|
|
28
|
+
it('extracts security-relevant lines from imported files', () => {
|
|
29
|
+
const file = makeFileContext({
|
|
30
|
+
filePath: 'router.py',
|
|
31
|
+
imports: ['./tools/executor'],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
35
|
+
|
|
36
|
+
expect(summaries.length).toBeGreaterThan(0);
|
|
37
|
+
const executorSummary = summaries.find((s) => s.filePath.includes('executor'));
|
|
38
|
+
expect(executorSummary).toBeDefined();
|
|
39
|
+
expect(executorSummary!.relationship).toBe('imports');
|
|
40
|
+
// Should capture subprocess.run line
|
|
41
|
+
expect(executorSummary!.relevantLines.some((l) => l.includes('subprocess'))).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('includes security-relevant sibling files', () => {
|
|
45
|
+
const file = makeFileContext({
|
|
46
|
+
filePath: 'router.py',
|
|
47
|
+
imports: [],
|
|
48
|
+
siblingFiles: ['vuln-tool.py', 'guard.py', 'readme.txt'],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// guard.py is a security-relevant sibling
|
|
52
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
53
|
+
|
|
54
|
+
// May or may not find guard.py depending on whether it exists at the right path
|
|
55
|
+
// The point is the function doesn't crash and returns valid summaries
|
|
56
|
+
expect(Array.isArray(summaries)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('returns empty array when no related files have security-relevant content', () => {
|
|
60
|
+
const file = makeFileContext({
|
|
61
|
+
filePath: 'empty.py',
|
|
62
|
+
imports: [],
|
|
63
|
+
importedBy: [],
|
|
64
|
+
siblingFiles: [],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
68
|
+
expect(summaries).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('limits to MAX_RELATED_FILES (4)', () => {
|
|
72
|
+
const file = makeFileContext({
|
|
73
|
+
imports: [
|
|
74
|
+
'./tools/executor',
|
|
75
|
+
'./tools/guard',
|
|
76
|
+
'./vuln-tool',
|
|
77
|
+
'./router',
|
|
78
|
+
'./extra1',
|
|
79
|
+
'./extra2',
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
84
|
+
expect(summaries.length).toBeLessThanOrEqual(4);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('resolves Python bare module imports (tools.executor style)', () => {
|
|
88
|
+
const file = makeFileContext({
|
|
89
|
+
filePath: 'router.py',
|
|
90
|
+
imports: ['tools.executor'], // as buildFileContext would actually produce
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
94
|
+
|
|
95
|
+
const executorSummary = summaries.find((s) => s.filePath.includes('executor'));
|
|
96
|
+
expect(executorSummary).toBeDefined();
|
|
97
|
+
expect(executorSummary!.relationship).toBe('imports');
|
|
98
|
+
expect(executorSummary!.relevantLines.some((l) => l.includes('subprocess'))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('resolves Python single-token imports (import guard style)', () => {
|
|
102
|
+
const file = makeFileContext({
|
|
103
|
+
filePath: 'router.py',
|
|
104
|
+
imports: ['tools.guard'], // from tools.guard import is_allowed_command
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
108
|
+
|
|
109
|
+
const guardSummary = summaries.find((s) => s.filePath.includes('guard'));
|
|
110
|
+
expect(guardSummary).toBeDefined();
|
|
111
|
+
expect(guardSummary!.relevantLines.some((l) => /allow/i.test(l))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('resolves submodule import from file.ts output (from tools import executor)', () => {
|
|
115
|
+
// file.ts now emits both 'tools' and 'tools.executor' for `from tools import executor`
|
|
116
|
+
// The resolver should find tools/executor.py via the dotted form
|
|
117
|
+
const file = makeFileContext({
|
|
118
|
+
filePath: 'router.py',
|
|
119
|
+
imports: ['tools', 'tools.executor'], // as file.ts now produces
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
123
|
+
|
|
124
|
+
const executorSummary = summaries.find((s) => s.filePath.includes('executor'));
|
|
125
|
+
expect(executorSummary).toBeDefined();
|
|
126
|
+
expect(executorSummary!.relevantLines.some((l) => l.includes('subprocess'))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('does not inject unrelated package children for bare import', () => {
|
|
130
|
+
// `from tools import safe_helper` should NOT pull in unrelated executor.py
|
|
131
|
+
// file.ts emits ['tools', 'tools.safe_helper'] — neither resolves to executor.py
|
|
132
|
+
const file = makeFileContext({
|
|
133
|
+
filePath: 'router.py',
|
|
134
|
+
imports: ['tools', 'tools.safe_helper'], // safe_helper doesn't exist in fixture
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
138
|
+
|
|
139
|
+
// 'tools' alone resolves to nothing (no tools.py, no tools/__init__.py)
|
|
140
|
+
// 'tools.safe_helper' resolves to nothing (no tools/safe_helper.py)
|
|
141
|
+
// So no executor.py should appear from the import path
|
|
142
|
+
const executorFromImport = summaries.find((s) =>
|
|
143
|
+
s.filePath.includes('executor') && s.relationship === 'imports');
|
|
144
|
+
expect(executorFromImport).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('does not summarize the same file twice across relationships', () => {
|
|
148
|
+
// If a file is both imported and a sibling, it should appear only once
|
|
149
|
+
const file = makeFileContext({
|
|
150
|
+
filePath: 'router.py',
|
|
151
|
+
imports: ['./tools/executor'],
|
|
152
|
+
importedBy: [],
|
|
153
|
+
siblingFiles: ['tools'], // tools dir is a sibling
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const summaries = buildRelatedFileSummaries(file, FIXTURES_DIR);
|
|
157
|
+
const paths = summaries.map((s) => s.filePath);
|
|
158
|
+
const unique = new Set(paths);
|
|
159
|
+
expect(paths.length).toBe(unique.size);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('formatRelatedFileSummaries', () => {
|
|
164
|
+
it('returns empty string for no summaries', () => {
|
|
165
|
+
expect(formatRelatedFileSummaries([])).toBe('');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('formats summaries with file path, relationship, and lines', () => {
|
|
169
|
+
const summaries = [
|
|
170
|
+
{
|
|
171
|
+
filePath: 'tools/executor.py',
|
|
172
|
+
relationship: 'imports' as const,
|
|
173
|
+
relevantLines: ['L9: result = subprocess.run([command, *args], capture_output=True, text=True, shell=False)'],
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const formatted = formatRelatedFileSummaries(summaries);
|
|
178
|
+
expect(formatted).toContain('tools/executor.py (imports)');
|
|
179
|
+
expect(formatted).toContain('subprocess.run');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from .guard import is_allowed_command
|
|
3
|
+
|
|
4
|
+
ALLOWED_COMMANDS = {"ls", "cat", "echo", "date", "whoami"}
|
|
5
|
+
|
|
6
|
+
def execute_tool(command: str, args: list[str]) -> str:
|
|
7
|
+
if command not in ALLOWED_COMMANDS:
|
|
8
|
+
raise ValueError(f"Command not allowed: {command}")
|
|
9
|
+
result = subprocess.run([command, *args], capture_output=True, text=True, shell=False)
|
|
10
|
+
return result.stdout
|
|
@@ -119,6 +119,82 @@ describe('DependencyGraphBuilder', () => {
|
|
|
119
119
|
}
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
it('resolves Python bare module imports into the graph', () => {
|
|
123
|
+
const fixtureDir = path.resolve(__dirname, '../fixtures/guarded-agent');
|
|
124
|
+
const builder = new DependencyGraphBuilder(fixtureDir);
|
|
125
|
+
const graph = builder.build(['router.py']);
|
|
126
|
+
|
|
127
|
+
// router.py has `from tools.executor import execute_tool`
|
|
128
|
+
// which should resolve to tools/executor.py
|
|
129
|
+
const routerNode = graph.nodes.get('router.py');
|
|
130
|
+
expect(routerNode).toBeTruthy();
|
|
131
|
+
|
|
132
|
+
// Check that tools/executor.py is in the graph with a reverse edge
|
|
133
|
+
const executorKey = Array.from(graph.nodes.keys()).find((k) => k.includes('executor'));
|
|
134
|
+
expect(executorKey).toBeDefined();
|
|
135
|
+
const executorNode = graph.nodes.get(executorKey!);
|
|
136
|
+
expect(executorNode?.importedBy).toContain('router.py');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('resolves Python bare imports from project root for nested files', () => {
|
|
140
|
+
// Simulate app/router.py importing tools.executor where tools/ is at project root
|
|
141
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-nested-py-'));
|
|
142
|
+
try {
|
|
143
|
+
fs.mkdirSync(path.join(tmpDir, 'app'));
|
|
144
|
+
fs.mkdirSync(path.join(tmpDir, 'tools'));
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
path.join(tmpDir, 'app', 'router.py'),
|
|
147
|
+
'from tools.executor import run\n',
|
|
148
|
+
);
|
|
149
|
+
fs.writeFileSync(
|
|
150
|
+
path.join(tmpDir, 'tools', 'executor.py'),
|
|
151
|
+
'def run(): pass\n',
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
155
|
+
const graph = builder.build(['app/router.py']);
|
|
156
|
+
|
|
157
|
+
const normalizedKey = Array.from(graph.nodes.keys()).find((k) =>
|
|
158
|
+
k.replace(/\\/g, '/').includes('tools/executor'));
|
|
159
|
+
expect(normalizedKey).toBeDefined();
|
|
160
|
+
const executorNode = graph.nodes.get(normalizedKey!);
|
|
161
|
+
// Path separators vary by OS — normalize for comparison
|
|
162
|
+
const importedByNorm = executorNode?.importedBy.map((p) => p.replace(/\\/g, '/'));
|
|
163
|
+
expect(importedByNorm).toContain('app/router.py');
|
|
164
|
+
} finally {
|
|
165
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('resolves "from package import submodule" form into the graph', () => {
|
|
170
|
+
// `from tools import executor` — the graph resolver must emit tools.executor
|
|
171
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-from-import-'));
|
|
172
|
+
try {
|
|
173
|
+
fs.mkdirSync(path.join(tmpDir, 'app'));
|
|
174
|
+
fs.mkdirSync(path.join(tmpDir, 'tools'));
|
|
175
|
+
fs.writeFileSync(
|
|
176
|
+
path.join(tmpDir, 'app', 'router.py'),
|
|
177
|
+
'from tools import executor\n',
|
|
178
|
+
);
|
|
179
|
+
fs.writeFileSync(
|
|
180
|
+
path.join(tmpDir, 'tools', 'executor.py'),
|
|
181
|
+
'def run(): pass\n',
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const builder = new DependencyGraphBuilder(tmpDir);
|
|
185
|
+
const graph = builder.build(['app/router.py']);
|
|
186
|
+
|
|
187
|
+
const normalizedKey = Array.from(graph.nodes.keys()).find((k) =>
|
|
188
|
+
k.replace(/\\/g, '/').includes('tools/executor'));
|
|
189
|
+
expect(normalizedKey).toBeDefined();
|
|
190
|
+
const executorNode = graph.nodes.get(normalizedKey!);
|
|
191
|
+
const importedByNorm = executorNode?.importedBy.map((p) => p.replace(/\\/g, '/'));
|
|
192
|
+
expect(importedByNorm).toContain('app/router.py');
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
122
198
|
it('keeps supported non-JS entry files in the graph', () => {
|
|
123
199
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cr-java-'));
|
|
124
200
|
try {
|
package/index.js
CHANGED
|
@@ -47,7 +47,7 @@ try {
|
|
|
47
47
|
// Create MCP Server
|
|
48
48
|
const server = new McpServer(
|
|
49
49
|
{
|
|
50
|
-
name: "
|
|
50
|
+
name: "agent-security-scanner-mcp",
|
|
51
51
|
version: _pkgVersion,
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -306,7 +306,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
306
306
|
// CLI mode: scan-prompt <text> [--verbosity minimal|compact|full]
|
|
307
307
|
const text = cliArgs[1];
|
|
308
308
|
if (!text) {
|
|
309
|
-
console.error('Usage:
|
|
309
|
+
console.error('Usage: agent-security-scanner-mcp scan-prompt <text> [--verbosity minimal|compact|full]');
|
|
310
310
|
process.exit(1);
|
|
311
311
|
}
|
|
312
312
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -326,7 +326,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
326
326
|
// CLI mode: scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]
|
|
327
327
|
const filePath = cliArgs[1];
|
|
328
328
|
if (!filePath) {
|
|
329
|
-
console.error('Usage:
|
|
329
|
+
console.error('Usage: agent-security-scanner-mcp scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]');
|
|
330
330
|
process.exit(1);
|
|
331
331
|
}
|
|
332
332
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -348,7 +348,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
348
348
|
const packageName = cliArgs[1];
|
|
349
349
|
const ecosystem = cliArgs[2];
|
|
350
350
|
if (!packageName || !ecosystem) {
|
|
351
|
-
console.error('Usage:
|
|
351
|
+
console.error('Usage: agent-security-scanner-mcp check-package <name> <ecosystem>');
|
|
352
352
|
console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
|
|
353
353
|
process.exit(1);
|
|
354
354
|
}
|
|
@@ -367,7 +367,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
367
367
|
const filePath = cliArgs[1];
|
|
368
368
|
const ecosystem = cliArgs[2];
|
|
369
369
|
if (!filePath || !ecosystem) {
|
|
370
|
-
console.error('Usage:
|
|
370
|
+
console.error('Usage: agent-security-scanner-mcp scan-packages <file> <ecosystem> [--verbosity minimal|compact|full]');
|
|
371
371
|
console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
|
|
372
372
|
process.exit(1);
|
|
373
373
|
}
|
|
@@ -387,7 +387,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
387
387
|
// CLI mode: scan-project <dir> [--recursive] [--diff-only] [--cross-file] [--include '*.py'] [--exclude '*.test.js'] [--verbosity minimal|compact|full]
|
|
388
388
|
const dirPath = cliArgs[1];
|
|
389
389
|
if (!dirPath || dirPath.startsWith('--')) {
|
|
390
|
-
console.error('Usage:
|
|
390
|
+
console.error('Usage: agent-security-scanner-mcp scan-project <directory> [--recursive] [--diff-only] [--cross-file] [--include <pattern>] [--exclude <pattern>] [--verbosity minimal|compact|full]');
|
|
391
391
|
process.exit(1);
|
|
392
392
|
}
|
|
393
393
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -455,7 +455,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
455
455
|
// CLI mode: scan-mcp <path> [--verbosity minimal|compact|full]
|
|
456
456
|
const serverPath = cliArgs[1];
|
|
457
457
|
if (!serverPath) {
|
|
458
|
-
console.error('Usage:
|
|
458
|
+
console.error('Usage: agent-security-scanner-mcp scan-mcp <server-path> [--verbosity minimal|compact|full]');
|
|
459
459
|
process.exit(1);
|
|
460
460
|
}
|
|
461
461
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -474,7 +474,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
474
474
|
const actionType = cliArgs[1];
|
|
475
475
|
const actionValue = cliArgs[2];
|
|
476
476
|
if (!actionType || !actionValue) {
|
|
477
|
-
console.error('Usage:
|
|
477
|
+
console.error('Usage: agent-security-scanner-mcp scan-action <type> <value> [--verbosity minimal|compact|full]');
|
|
478
478
|
console.error('Types: bash, file_write, file_read, http_request, file_delete, cron, process_spawn, git, docker');
|
|
479
479
|
process.exit(1);
|
|
480
480
|
}
|
|
@@ -492,7 +492,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
492
492
|
} else if (cliArgs[0] === 'scan-skill') {
|
|
493
493
|
const skillPath = cliArgs[1];
|
|
494
494
|
if (!skillPath) {
|
|
495
|
-
console.error('Usage:
|
|
495
|
+
console.error('Usage: agent-security-scanner-mcp scan-skill <skill-path> [--verbosity minimal|compact|full] [--baseline]');
|
|
496
496
|
process.exit(1);
|
|
497
497
|
}
|
|
498
498
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -529,7 +529,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
529
529
|
await import('./src/cli/scan-clawhub-safe.js');
|
|
530
530
|
// Exit is handled by scan-clawhub-safe.js
|
|
531
531
|
} else if (cliArgs[0] === '--help' || cliArgs[0] === '-h' || cliArgs[0] === 'help') {
|
|
532
|
-
console.log('\n
|
|
532
|
+
console.log('\n agent-security-scanner-mcp\n');
|
|
533
533
|
console.log(' Commands:');
|
|
534
534
|
console.log(' init [client] Set up MCP config for a client');
|
|
535
535
|
console.log(' init-hooks Install Claude Code hooks for auto-scanning');
|
|
@@ -557,14 +557,14 @@ const cliArgs = process.argv.slice(2);
|
|
|
557
557
|
console.log(' --include <pattern> Include only matching files (scan-project)');
|
|
558
558
|
console.log(' --exclude <pattern> Exclude matching files (scan-project)\n');
|
|
559
559
|
console.log(' Examples:');
|
|
560
|
-
console.log(' npx
|
|
561
|
-
console.log(' npx
|
|
562
|
-
console.log(' npx
|
|
563
|
-
console.log(' npx
|
|
564
|
-
console.log(' npx
|
|
565
|
-
console.log(' npx
|
|
566
|
-
console.log(' npx
|
|
567
|
-
console.log(' npx
|
|
560
|
+
console.log(' npx agent-security-scanner-mcp init');
|
|
561
|
+
console.log(' npx agent-security-scanner-mcp scan-prompt "ignore previous instructions"');
|
|
562
|
+
console.log(' npx agent-security-scanner-mcp scan-security ./app.py --verbosity minimal');
|
|
563
|
+
console.log(' npx agent-security-scanner-mcp check-package flask pypi');
|
|
564
|
+
console.log(' npx agent-security-scanner-mcp scan-project ./src --verbosity minimal');
|
|
565
|
+
console.log(' npx agent-security-scanner-mcp scan-diff HEAD~1');
|
|
566
|
+
console.log(' npx agent-security-scanner-mcp report ./src --json');
|
|
567
|
+
console.log(' npx agent-security-scanner-mcp benchmark --save --compare-latest\n');
|
|
568
568
|
process.exit(0);
|
|
569
569
|
} else {
|
|
570
570
|
// Normal MCP server mode
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "agent-security-scanner-mcp",
|
|
3
3
|
"version": "4.0.0",
|
|
4
4
|
"description": "Security scanner for OpenClaw: prompt injection firewall, package hallucination detection, code vulnerability scanning, auto-fix",
|
|
5
5
|
"author": "Sinewave AI",
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
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), 1700+ vulnerability rules with AST & taint analysis, LLM-powered semantic code review, auto-fix. For Claude Code, Cursor, Windsurf, Cline, OpenClaw.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"agent-security-scanner-mcp": "index.js"
|
|
9
|
+
"agent-security-scanner-mcp": "index.js",
|
|
10
|
+
"cr-agent": "code-review-agent/dist/bin/cr-agent.js"
|
|
10
11
|
},
|
|
11
12
|
"scripts": {
|
|
12
13
|
"start": "node index.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* postinstall.js -
|
|
4
|
-
*
|
|
3
|
+
* postinstall.js - Setup script for agent-security-scanner-mcp
|
|
4
|
+
* 1. Install Python dependencies for tree-sitter AST engine (optional)
|
|
5
|
+
* 2. Install and build code-review-agent dependencies (optional)
|
|
5
6
|
*/
|
|
6
|
-
import { execFileSync } from "child_process";
|
|
7
|
+
import { execFileSync, execSync } from "child_process";
|
|
7
8
|
import { join, dirname } from "path";
|
|
8
9
|
import { fileURLToPath } from "url";
|
|
10
|
+
import { existsSync } from "fs";
|
|
9
11
|
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const
|
|
13
|
+
const rootDir = join(__dirname, "..");
|
|
14
|
+
const requirementsPath = join(rootDir, "requirements.txt");
|
|
15
|
+
const codeReviewAgentDir = join(rootDir, "code-review-agent");
|
|
12
16
|
|
|
13
17
|
// Check if Python 3 is available
|
|
14
18
|
function findPython() {
|
|
@@ -33,6 +37,7 @@ function isTreeSitterInstalled(pythonCmd) {
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
// Setup Python dependencies
|
|
36
41
|
const pythonCmd = findPython();
|
|
37
42
|
|
|
38
43
|
if (!pythonCmd) {
|
|
@@ -58,3 +63,37 @@ if (!pythonCmd) {
|
|
|
58
63
|
);
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
|
|
67
|
+
// Setup code-review-agent (LLM-powered semantic analysis)
|
|
68
|
+
if (existsSync(codeReviewAgentDir)) {
|
|
69
|
+
const distExists = existsSync(join(codeReviewAgentDir, "dist", "bin", "cr-agent.js"));
|
|
70
|
+
|
|
71
|
+
if (distExists) {
|
|
72
|
+
console.log("[postinstall] code-review-agent already built — cr-agent CLI available.");
|
|
73
|
+
} else {
|
|
74
|
+
console.log("[postinstall] Setting up code-review-agent (LLM-powered code review)...");
|
|
75
|
+
try {
|
|
76
|
+
// Install dependencies
|
|
77
|
+
execSync("npm install --omit=dev", {
|
|
78
|
+
cwd: codeReviewAgentDir,
|
|
79
|
+
timeout: 180000,
|
|
80
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Build TypeScript
|
|
84
|
+
execSync("npm run build", {
|
|
85
|
+
cwd: codeReviewAgentDir,
|
|
86
|
+
timeout: 60000,
|
|
87
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log("[postinstall] code-review-agent installed — run: npx cr-agent --help");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.log(
|
|
93
|
+
"[postinstall] Could not set up code-review-agent (optional LLM-powered review).\n" +
|
|
94
|
+
" The main scanner still works. To set up manually:\n" +
|
|
95
|
+
" cd node_modules/agent-security-scanner-mcp/code-review-agent && npm install && npm run build"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/server.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "io.github.sinewaveai/
|
|
3
|
+
"name": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
4
4
|
"description": "MCP security scanner with prompt injection firewall, package hallucination detection, LLM-powered code review, and auto-fix.",
|
|
5
5
|
"version": "4.0.0",
|
|
6
6
|
"transport": "stdio",
|
package/src/cli/init-hooks.js
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
|
|
8
|
-
const SCANNER_HOOK_MARKER = '
|
|
8
|
+
const SCANNER_HOOK_MARKER = 'agent-security-scanner-mcp';
|
|
9
9
|
|
|
10
10
|
function buildHooksConfig(withPromptGuard) {
|
|
11
11
|
const hooks = {
|
|
12
12
|
'post-tool-use': [
|
|
13
13
|
{
|
|
14
14
|
matcher: 'Write|Edit|MultiEdit',
|
|
15
|
-
command: `npx
|
|
15
|
+
command: `npx agent-security-scanner-mcp scan-security "$TOOL_INPUT_FILE_PATH" --verbosity minimal`,
|
|
16
16
|
},
|
|
17
17
|
],
|
|
18
18
|
};
|
|
@@ -21,7 +21,7 @@ function buildHooksConfig(withPromptGuard) {
|
|
|
21
21
|
hooks['pre-tool-use'] = [
|
|
22
22
|
{
|
|
23
23
|
matcher: 'Bash',
|
|
24
|
-
command: `npx
|
|
24
|
+
command: `npx agent-security-scanner-mcp scan-prompt "$TOOL_INPUT_COMMAND" --verbosity minimal`,
|
|
25
25
|
},
|
|
26
26
|
];
|
|
27
27
|
}
|