agent-security-scanner-mcp 4.0.1 → 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/code-review-agent/README.md +25 -4
- package/code-review-agent/bin/cr-agent.ts +7 -1
- package/code-review-agent/dist/bin/cr-agent.js +6 -0
- 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/llm/claude-cli.d.ts.map +1 -1
- package/code-review-agent/dist/src/llm/claude-cli.js +2 -1
- package/code-review-agent/dist/src/llm/claude-cli.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/llm/claude-cli.ts +2 -1
- 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/package.json +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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "4.0
|
|
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",
|