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.
Files changed (67) hide show
  1. package/code-review-agent/README.md +25 -4
  2. package/code-review-agent/bin/cr-agent.ts +7 -1
  3. package/code-review-agent/dist/bin/cr-agent.js +6 -0
  4. package/code-review-agent/dist/bin/cr-agent.js.map +1 -1
  5. package/code-review-agent/dist/src/analyzer/engine.d.ts +5 -0
  6. package/code-review-agent/dist/src/analyzer/engine.d.ts.map +1 -1
  7. package/code-review-agent/dist/src/analyzer/engine.js +30 -3
  8. package/code-review-agent/dist/src/analyzer/engine.js.map +1 -1
  9. package/code-review-agent/dist/src/analyzer/postprocess.d.ts +15 -0
  10. package/code-review-agent/dist/src/analyzer/postprocess.d.ts.map +1 -0
  11. package/code-review-agent/dist/src/analyzer/postprocess.js +275 -0
  12. package/code-review-agent/dist/src/analyzer/postprocess.js.map +1 -0
  13. package/code-review-agent/dist/src/analyzer/semantic.d.ts +5 -1
  14. package/code-review-agent/dist/src/analyzer/semantic.d.ts.map +1 -1
  15. package/code-review-agent/dist/src/analyzer/semantic.js +80 -20
  16. package/code-review-agent/dist/src/analyzer/semantic.js.map +1 -1
  17. package/code-review-agent/dist/src/context/assembler.d.ts +8 -2
  18. package/code-review-agent/dist/src/context/assembler.d.ts.map +1 -1
  19. package/code-review-agent/dist/src/context/assembler.js +33 -1
  20. package/code-review-agent/dist/src/context/assembler.js.map +1 -1
  21. package/code-review-agent/dist/src/context/file.d.ts.map +1 -1
  22. package/code-review-agent/dist/src/context/file.js +11 -23
  23. package/code-review-agent/dist/src/context/file.js.map +1 -1
  24. package/code-review-agent/dist/src/context/security-summary.d.ts +19 -0
  25. package/code-review-agent/dist/src/context/security-summary.d.ts.map +1 -0
  26. package/code-review-agent/dist/src/context/security-summary.js +199 -0
  27. package/code-review-agent/dist/src/context/security-summary.js.map +1 -0
  28. package/code-review-agent/dist/src/graph/dependency.d.ts.map +1 -1
  29. package/code-review-agent/dist/src/graph/dependency.js +8 -1
  30. package/code-review-agent/dist/src/graph/dependency.js.map +1 -1
  31. package/code-review-agent/dist/src/graph/resolver.d.ts.map +1 -1
  32. package/code-review-agent/dist/src/graph/resolver.js +14 -5
  33. package/code-review-agent/dist/src/graph/resolver.js.map +1 -1
  34. package/code-review-agent/dist/src/index.d.ts +4 -1
  35. package/code-review-agent/dist/src/index.d.ts.map +1 -1
  36. package/code-review-agent/dist/src/index.js +2 -0
  37. package/code-review-agent/dist/src/index.js.map +1 -1
  38. package/code-review-agent/dist/src/llm/claude-cli.d.ts.map +1 -1
  39. package/code-review-agent/dist/src/llm/claude-cli.js +2 -1
  40. package/code-review-agent/dist/src/llm/claude-cli.js.map +1 -1
  41. package/code-review-agent/dist/src/types/config.d.ts +3 -0
  42. package/code-review-agent/dist/src/types/config.d.ts.map +1 -1
  43. package/code-review-agent/dist/src/types/config.js +9 -0
  44. package/code-review-agent/dist/src/types/config.js.map +1 -1
  45. package/code-review-agent/src/analyzer/engine.ts +36 -2
  46. package/code-review-agent/src/analyzer/postprocess.ts +311 -0
  47. package/code-review-agent/src/analyzer/semantic.ts +87 -18
  48. package/code-review-agent/src/context/assembler.ts +44 -2
  49. package/code-review-agent/src/context/file.ts +13 -18
  50. package/code-review-agent/src/context/security-summary.ts +225 -0
  51. package/code-review-agent/src/graph/dependency.ts +8 -1
  52. package/code-review-agent/src/graph/resolver.ts +14 -5
  53. package/code-review-agent/src/index.ts +4 -0
  54. package/code-review-agent/src/llm/claude-cli.ts +2 -1
  55. package/code-review-agent/src/types/config.ts +16 -0
  56. package/code-review-agent/tests/analyzer/engine.test.ts +5 -0
  57. package/code-review-agent/tests/analyzer/postprocess.test.ts +450 -0
  58. package/code-review-agent/tests/analyzer/prompt-routing.test.ts +137 -0
  59. package/code-review-agent/tests/config-mode.test.ts +71 -0
  60. package/code-review-agent/tests/context/file.test.ts +16 -1
  61. package/code-review-agent/tests/context/security-summary.test.ts +181 -0
  62. package/code-review-agent/tests/fixtures/guarded-agent/router.py +6 -0
  63. package/code-review-agent/tests/fixtures/guarded-agent/tools/executor.py +10 -0
  64. package/code-review-agent/tests/fixtures/guarded-agent/tools/guard.py +4 -0
  65. package/code-review-agent/tests/fixtures/guarded-agent/vuln-tool.py +6 -0
  66. package/code-review-agent/tests/graph/dependency.test.ts +76 -0
  67. 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,6 @@
1
+ from tools.executor import execute_tool
2
+
3
+ def handle_request(action: str, args: list[str]) -> str:
4
+ if action == "run_command":
5
+ return execute_tool(args[0], args[1:])
6
+ return "unknown action"
@@ -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
@@ -0,0 +1,4 @@
1
+ ALLOWED_COMMANDS = frozenset({"ls", "cat", "echo", "date", "whoami"})
2
+
3
+ def is_allowed_command(command: str) -> bool:
4
+ return command in ALLOWED_COMMANDS
@@ -0,0 +1,6 @@
1
+ import requests
2
+
3
+ def fetch_url(url: str) -> str:
4
+ """Fetches a URL without any validation - SSRF vulnerability."""
5
+ response = requests.get(url)
6
+ return response.text
@@ -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.1",
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",