@raghulm/aegis-mcp 1.0.5 → 1.0.7

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.
@@ -1,261 +1,261 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- import shutil
6
- import subprocess
7
- import sys
8
- from typing import Any
9
-
10
-
11
- def _find_semgrep_cmd() -> list[str]:
12
- """Find the best available semgrep command."""
13
- # Check the project's local virtual environment first (critical for Claude Desktop)
14
- project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
- venv_scripts = os.path.join(project_root, ".venv", "Scripts")
16
- venv_bin = os.path.join(project_root, ".venv", "bin")
17
-
18
- if sys.platform == "win32":
19
- venv_pysemgrep = os.path.join(venv_scripts, "pysemgrep.exe")
20
- if os.path.exists(venv_pysemgrep):
21
- return [venv_pysemgrep]
22
- venv_semgrep = os.path.join(venv_scripts, "semgrep.exe")
23
- if os.path.exists(venv_semgrep):
24
- return [venv_semgrep]
25
- else:
26
- venv_semgrep = os.path.join(venv_bin, "semgrep")
27
- if os.path.exists(venv_semgrep):
28
- return [venv_semgrep]
29
-
30
- # Check relative to current Python executable
31
- bin_dir = os.path.dirname(sys.executable)
32
- if sys.platform == "win32":
33
- pysemgrep = os.path.join(bin_dir, "pysemgrep.exe")
34
- if os.path.exists(pysemgrep):
35
- return [pysemgrep]
36
- semgrep = os.path.join(bin_dir, "semgrep.exe")
37
- if os.path.exists(semgrep):
38
- return [semgrep]
39
- else:
40
- semgrep = os.path.join(bin_dir, "semgrep")
41
- if os.path.exists(semgrep):
42
- return [semgrep]
43
-
44
- # Then check PATH
45
- if sys.platform == "win32":
46
- pysemgrep_which = shutil.which("pysemgrep")
47
- if pysemgrep_which:
48
- return [pysemgrep_which]
49
- semgrep_which = shutil.which("semgrep")
50
- if semgrep_which:
51
- return [semgrep_which]
52
-
53
- return [sys.executable, "-m", "semgrep"]
54
-
55
-
56
- def run_semgrep_scan(path: str, config: str = "auto") -> dict[str, Any]:
57
- """Run a Semgrep SAST scan on a file or directory.
58
-
59
- Args:
60
- path: File or directory path to scan.
61
- config: Semgrep ruleset config. Defaults to "auto" which uses
62
- recommended rules. Can also be a path to a custom
63
- rules file, or a registry identifier like "p/python",
64
- "p/javascript", "p/owasp-top-ten", "p/security-audit".
65
-
66
- Returns:
67
- Scan results as a JSON-compatible dict with findings summary.
68
- """
69
- cmd = _find_semgrep_cmd() + [
70
- "scan",
71
- "--json",
72
- "--config", config,
73
- "--jobs=1", # Fix Windows RPC IPC bug in semgrep-core
74
- "--quiet", # Prevent interactive progress bars from deadlocking stdout pipe
75
- "--no-rewrite-rule-ids",
76
- "--no-git-ignore", # Scan all files regardless of git tracking status
77
- path,
78
- ]
79
-
80
- # Set UTF-8 mode to avoid Windows charmap encoding errors
81
- env = os.environ.copy()
82
- env["PYTHONUTF8"] = "1"
83
- env["PYTHONIOENCODING"] = "utf-8"
84
- # --config auto requires metrics to be enabled; Claude Desktop may
85
- # inherit SEMGREP_SEND_METRICS=off which causes a silent exit-code-2 crash.
86
- env["SEMGREP_SEND_METRICS"] = "on"
87
- env["SEMGREP_FORCE_COLOR"] = "0" # no ANSI escapes in captured output
88
-
89
- # Strip dangerous global Python environment variables. If Claude Desktop
90
- # executes us via global python, passing these down breaks pysemgrep.exe's venv.
91
- env.pop("PYTHONPATH", None)
92
- env.pop("PYTHONHOME", None)
93
- # Also drop VIRTUAL_ENV if it points elsewhere
94
- if "VIRTUAL_ENV" in env:
95
- env.pop("VIRTUAL_ENV")
96
-
97
- # Ensure the venv Scripts dir and the semgrep bin dir (containing
98
- # semgrep-core.exe) are on PATH. Claude Desktop often launches with a
99
- # minimal PATH that doesn't include either location.
100
- project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
101
- extra_paths = []
102
- venv_scripts = os.path.join(project_root, ".venv", "Scripts")
103
- if os.path.isdir(venv_scripts):
104
- extra_paths.append(venv_scripts)
105
- # semgrep-core.exe lives inside the semgrep package
106
- semgrep_bin = os.path.join(
107
- project_root, ".venv", "Lib", "site-packages", "semgrep", "bin"
108
- )
109
- if os.path.isdir(semgrep_bin):
110
- extra_paths.append(semgrep_bin)
111
- if extra_paths:
112
- env["PATH"] = os.pathsep.join(extra_paths) + os.pathsep + env.get("PATH", "")
113
-
114
- import tempfile
115
-
116
- cmd_str = subprocess.list2cmdline(cmd)
117
-
118
- if sys.platform == "win32":
119
- # On Windows, pysemgrep spawns semgrep-core.exe via internal RPC
120
- # pipes. When the parent process has no console (Claude Desktop /
121
- # MCP stdio mode), pipe inheritance is broken and semgrep-core
122
- # receives empty RPC input → "Expected a number, got ''".
123
- #
124
- # Fix: write a temp .bat file that runs semgrep and redirects
125
- # stdout/stderr to temp files. The batch file runs in its own
126
- # cmd.exe session with proper pipe/console semantics.
127
- stdout_file = tempfile.NamedTemporaryFile(
128
- mode="w", suffix="_stdout.json", delete=False, dir=tempfile.gettempdir()
129
- )
130
- stderr_file = tempfile.NamedTemporaryFile(
131
- mode="w", suffix="_stderr.txt", delete=False, dir=tempfile.gettempdir()
132
- )
133
- bat_file = tempfile.NamedTemporaryFile(
134
- mode="w", suffix="_semgrep.bat", delete=False, dir=tempfile.gettempdir()
135
- )
136
- stdout_path = stdout_file.name; stdout_file.close()
137
- stderr_path = stderr_file.name; stderr_file.close()
138
- bat_path = bat_file.name
139
-
140
- # Write batch script that runs semgrep and captures output to files
141
- bat_file.write(f'@echo off\r\n{cmd_str} >"{stdout_path}" 2>"{stderr_path}"\r\n')
142
- bat_file.close()
143
-
144
- try:
145
- proc = subprocess.run(
146
- bat_path,
147
- timeout=300,
148
- env=env,
149
- stdin=subprocess.DEVNULL,
150
- stdout=subprocess.DEVNULL,
151
- stderr=subprocess.DEVNULL,
152
- shell=True,
153
- )
154
- returncode = proc.returncode
155
- try:
156
- stdout_text = open(stdout_path, "r", encoding="utf-8", errors="replace").read()
157
- except Exception:
158
- stdout_text = ""
159
- try:
160
- stderr_text = open(stderr_path, "r", encoding="utf-8", errors="replace").read()
161
- except Exception:
162
- stderr_text = ""
163
-
164
- class _R:
165
- pass
166
- result = _R()
167
- result.stdout = stdout_text
168
- result.stderr = stderr_text
169
- result.returncode = returncode
170
- except FileNotFoundError:
171
- return {
172
- "path": path, "config": config,
173
- "error": "semgrep is not installed. Install it with: pip install semgrep",
174
- "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
175
- }
176
- except subprocess.TimeoutExpired:
177
- return {
178
- "path": path, "config": config,
179
- "error": f"Semgrep scan timed out after 300 seconds for path '{path}'",
180
- "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
181
- }
182
- finally:
183
- for f in (bat_path, stdout_path, stderr_path):
184
- try:
185
- os.unlink(f)
186
- except Exception:
187
- pass
188
- else:
189
- # Unix: normal subprocess with pipes works fine
190
- try:
191
- result = subprocess.run(
192
- cmd,
193
- capture_output=True,
194
- text=True,
195
- encoding="utf-8",
196
- errors="replace",
197
- timeout=300,
198
- env=env,
199
- stdin=subprocess.DEVNULL,
200
- )
201
- except FileNotFoundError:
202
- return {
203
- "path": path, "config": config,
204
- "error": "semgrep is not installed. Install it with: pip install semgrep",
205
- "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
206
- }
207
- except subprocess.TimeoutExpired:
208
- return {
209
- "path": path, "config": config,
210
- "error": f"Semgrep scan timed out after 300 seconds for path '{path}'",
211
- "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
212
- }
213
-
214
- try:
215
- stdout_text = result.stdout or ""
216
- output = json.loads(stdout_text)
217
- except json.JSONDecodeError:
218
- stderr_text = (result.stderr or "")[:2000]
219
- stdout_clip = (result.stdout or "")[:2000]
220
- return {
221
- "path": path,
222
- "config": config,
223
- "error": f"Semgrep failed to return valid JSON (exit code {result.returncode})",
224
- "stderr": stderr_text,
225
- "stdout_clip": stdout_clip,
226
- "command": cmd_str,
227
- "total_findings": 0,
228
- "severity_summary": {},
229
- "findings": [],
230
- "errors_count": 1,
231
- }
232
-
233
- # Build a concise summary
234
- results = output.get("results", [])
235
- errors = output.get("errors", [])
236
-
237
- findings: list[dict[str, Any]] = []
238
- for r in results:
239
- findings.append({
240
- "rule_id": r.get("check_id", ""),
241
- "severity": r.get("extra", {}).get("severity", "unknown"),
242
- "message": r.get("extra", {}).get("message", ""),
243
- "file": r.get("path", ""),
244
- "start_line": r.get("start", {}).get("line"),
245
- "end_line": r.get("end", {}).get("line"),
246
- "matched_code": r.get("extra", {}).get("lines", ""),
247
- })
248
-
249
- severity_counts: dict[str, int] = {}
250
- for f in findings:
251
- sev = f["severity"].upper()
252
- severity_counts[sev] = severity_counts.get(sev, 0) + 1
253
-
254
- return {
255
- "path": path,
256
- "config": config,
257
- "total_findings": len(findings),
258
- "severity_summary": severity_counts,
259
- "findings": findings,
260
- "errors_count": len(errors),
261
- }
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from typing import Any
9
+
10
+
11
+ def _find_semgrep_cmd() -> list[str]:
12
+ """Find the best available semgrep command."""
13
+ # Check the project's local virtual environment first (critical for Claude Desktop)
14
+ project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
+ venv_scripts = os.path.join(project_root, ".venv", "Scripts")
16
+ venv_bin = os.path.join(project_root, ".venv", "bin")
17
+
18
+ if sys.platform == "win32":
19
+ venv_pysemgrep = os.path.join(venv_scripts, "pysemgrep.exe")
20
+ if os.path.exists(venv_pysemgrep):
21
+ return [venv_pysemgrep]
22
+ venv_semgrep = os.path.join(venv_scripts, "semgrep.exe")
23
+ if os.path.exists(venv_semgrep):
24
+ return [venv_semgrep]
25
+ else:
26
+ venv_semgrep = os.path.join(venv_bin, "semgrep")
27
+ if os.path.exists(venv_semgrep):
28
+ return [venv_semgrep]
29
+
30
+ # Check relative to current Python executable
31
+ bin_dir = os.path.dirname(sys.executable)
32
+ if sys.platform == "win32":
33
+ pysemgrep = os.path.join(bin_dir, "pysemgrep.exe")
34
+ if os.path.exists(pysemgrep):
35
+ return [pysemgrep]
36
+ semgrep = os.path.join(bin_dir, "semgrep.exe")
37
+ if os.path.exists(semgrep):
38
+ return [semgrep]
39
+ else:
40
+ semgrep = os.path.join(bin_dir, "semgrep")
41
+ if os.path.exists(semgrep):
42
+ return [semgrep]
43
+
44
+ # Then check PATH
45
+ if sys.platform == "win32":
46
+ pysemgrep_which = shutil.which("pysemgrep")
47
+ if pysemgrep_which:
48
+ return [pysemgrep_which]
49
+ semgrep_which = shutil.which("semgrep")
50
+ if semgrep_which:
51
+ return [semgrep_which]
52
+
53
+ return [sys.executable, "-m", "semgrep"]
54
+
55
+
56
+ def run_semgrep_scan(path: str, config: str = "auto") -> dict[str, Any]:
57
+ """Run a Semgrep SAST scan on a file or directory.
58
+
59
+ Args:
60
+ path: File or directory path to scan.
61
+ config: Semgrep ruleset config. Defaults to "auto" which uses
62
+ recommended rules. Can also be a path to a custom
63
+ rules file, or a registry identifier like "p/python",
64
+ "p/javascript", "p/owasp-top-ten", "p/security-audit".
65
+
66
+ Returns:
67
+ Scan results as a JSON-compatible dict with findings summary.
68
+ """
69
+ cmd = _find_semgrep_cmd() + [
70
+ "scan",
71
+ "--json",
72
+ "--config", config,
73
+ "--jobs=1", # Fix Windows RPC IPC bug in semgrep-core
74
+ "--quiet", # Prevent interactive progress bars from deadlocking stdout pipe
75
+ "--no-rewrite-rule-ids",
76
+ "--no-git-ignore", # Scan all files regardless of git tracking status
77
+ path,
78
+ ]
79
+
80
+ # Set UTF-8 mode to avoid Windows charmap encoding errors
81
+ env = os.environ.copy()
82
+ env["PYTHONUTF8"] = "1"
83
+ env["PYTHONIOENCODING"] = "utf-8"
84
+ # --config auto requires metrics to be enabled; Claude Desktop may
85
+ # inherit SEMGREP_SEND_METRICS=off which causes a silent exit-code-2 crash.
86
+ env["SEMGREP_SEND_METRICS"] = "on"
87
+ env["SEMGREP_FORCE_COLOR"] = "0" # no ANSI escapes in captured output
88
+
89
+ # Strip dangerous global Python environment variables. If Claude Desktop
90
+ # executes us via global python, passing these down breaks pysemgrep.exe's venv.
91
+ env.pop("PYTHONPATH", None)
92
+ env.pop("PYTHONHOME", None)
93
+ # Also drop VIRTUAL_ENV if it points elsewhere
94
+ if "VIRTUAL_ENV" in env:
95
+ env.pop("VIRTUAL_ENV")
96
+
97
+ # Ensure the venv Scripts dir and the semgrep bin dir (containing
98
+ # semgrep-core.exe) are on PATH. Claude Desktop often launches with a
99
+ # minimal PATH that doesn't include either location.
100
+ project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
101
+ extra_paths = []
102
+ venv_scripts = os.path.join(project_root, ".venv", "Scripts")
103
+ if os.path.isdir(venv_scripts):
104
+ extra_paths.append(venv_scripts)
105
+ # semgrep-core.exe lives inside the semgrep package
106
+ semgrep_bin = os.path.join(
107
+ project_root, ".venv", "Lib", "site-packages", "semgrep", "bin"
108
+ )
109
+ if os.path.isdir(semgrep_bin):
110
+ extra_paths.append(semgrep_bin)
111
+ if extra_paths:
112
+ env["PATH"] = os.pathsep.join(extra_paths) + os.pathsep + env.get("PATH", "")
113
+
114
+ import tempfile
115
+
116
+ cmd_str = subprocess.list2cmdline(cmd)
117
+
118
+ if sys.platform == "win32":
119
+ # On Windows, pysemgrep spawns semgrep-core.exe via internal RPC
120
+ # pipes. When the parent process has no console (Claude Desktop /
121
+ # MCP stdio mode), pipe inheritance is broken and semgrep-core
122
+ # receives empty RPC input → "Expected a number, got ''".
123
+ #
124
+ # Fix: write a temp .bat file that runs semgrep and redirects
125
+ # stdout/stderr to temp files. The batch file runs in its own
126
+ # cmd.exe session with proper pipe/console semantics.
127
+ stdout_file = tempfile.NamedTemporaryFile(
128
+ mode="w", suffix="_stdout.json", delete=False, dir=tempfile.gettempdir()
129
+ )
130
+ stderr_file = tempfile.NamedTemporaryFile(
131
+ mode="w", suffix="_stderr.txt", delete=False, dir=tempfile.gettempdir()
132
+ )
133
+ bat_file = tempfile.NamedTemporaryFile(
134
+ mode="w", suffix="_semgrep.bat", delete=False, dir=tempfile.gettempdir()
135
+ )
136
+ stdout_path = stdout_file.name; stdout_file.close()
137
+ stderr_path = stderr_file.name; stderr_file.close()
138
+ bat_path = bat_file.name
139
+
140
+ # Write batch script that runs semgrep and captures output to files
141
+ bat_file.write(f'@echo off\r\n{cmd_str} >"{stdout_path}" 2>"{stderr_path}"\r\n')
142
+ bat_file.close()
143
+
144
+ try:
145
+ proc = subprocess.run(
146
+ bat_path,
147
+ timeout=300,
148
+ env=env,
149
+ stdin=subprocess.DEVNULL,
150
+ stdout=subprocess.DEVNULL,
151
+ stderr=subprocess.DEVNULL,
152
+ shell=True,
153
+ )
154
+ returncode = proc.returncode
155
+ try:
156
+ stdout_text = open(stdout_path, "r", encoding="utf-8", errors="replace").read()
157
+ except Exception:
158
+ stdout_text = ""
159
+ try:
160
+ stderr_text = open(stderr_path, "r", encoding="utf-8", errors="replace").read()
161
+ except Exception:
162
+ stderr_text = ""
163
+
164
+ class _R:
165
+ pass
166
+ result = _R()
167
+ result.stdout = stdout_text
168
+ result.stderr = stderr_text
169
+ result.returncode = returncode
170
+ except FileNotFoundError:
171
+ return {
172
+ "path": path, "config": config,
173
+ "error": "semgrep is not installed. Install it with: pip install semgrep",
174
+ "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
175
+ }
176
+ except subprocess.TimeoutExpired:
177
+ return {
178
+ "path": path, "config": config,
179
+ "error": f"Semgrep scan timed out after 300 seconds for path '{path}'",
180
+ "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
181
+ }
182
+ finally:
183
+ for f in (bat_path, stdout_path, stderr_path):
184
+ try:
185
+ os.unlink(f)
186
+ except Exception:
187
+ pass
188
+ else:
189
+ # Unix: normal subprocess with pipes works fine
190
+ try:
191
+ result = subprocess.run(
192
+ cmd,
193
+ capture_output=True,
194
+ text=True,
195
+ encoding="utf-8",
196
+ errors="replace",
197
+ timeout=300,
198
+ env=env,
199
+ stdin=subprocess.DEVNULL,
200
+ )
201
+ except FileNotFoundError:
202
+ return {
203
+ "path": path, "config": config,
204
+ "error": "semgrep is not installed. Install it with: pip install semgrep",
205
+ "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
206
+ }
207
+ except subprocess.TimeoutExpired:
208
+ return {
209
+ "path": path, "config": config,
210
+ "error": f"Semgrep scan timed out after 300 seconds for path '{path}'",
211
+ "total_findings": 0, "severity_summary": {}, "findings": [], "errors_count": 1,
212
+ }
213
+
214
+ try:
215
+ stdout_text = result.stdout or ""
216
+ output = json.loads(stdout_text)
217
+ except json.JSONDecodeError:
218
+ stderr_text = (result.stderr or "")[:2000]
219
+ stdout_clip = (result.stdout or "")[:2000]
220
+ return {
221
+ "path": path,
222
+ "config": config,
223
+ "error": f"Semgrep failed to return valid JSON (exit code {result.returncode})",
224
+ "stderr": stderr_text,
225
+ "stdout_clip": stdout_clip,
226
+ "command": cmd_str,
227
+ "total_findings": 0,
228
+ "severity_summary": {},
229
+ "findings": [],
230
+ "errors_count": 1,
231
+ }
232
+
233
+ # Build a concise summary
234
+ results = output.get("results", [])
235
+ errors = output.get("errors", [])
236
+
237
+ findings: list[dict[str, Any]] = []
238
+ for r in results:
239
+ findings.append({
240
+ "rule_id": r.get("check_id", ""),
241
+ "severity": r.get("extra", {}).get("severity", "unknown"),
242
+ "message": r.get("extra", {}).get("message", ""),
243
+ "file": r.get("path", ""),
244
+ "start_line": r.get("start", {}).get("line"),
245
+ "end_line": r.get("end", {}).get("line"),
246
+ "matched_code": r.get("extra", {}).get("lines", ""),
247
+ })
248
+
249
+ severity_counts: dict[str, int] = {}
250
+ for f in findings:
251
+ sev = f["severity"].upper()
252
+ severity_counts[sev] = severity_counts.get(sev, 0) + 1
253
+
254
+ return {
255
+ "path": path,
256
+ "config": config,
257
+ "total_findings": len(findings),
258
+ "severity_summary": severity_counts,
259
+ "findings": findings,
260
+ "errors_count": len(errors),
261
+ }
@@ -1,19 +1,19 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import subprocess
5
-
6
-
7
- def run_trivy_scan(image: str) -> dict:
8
- try:
9
- output = subprocess.check_output(
10
- ["trivy", "image", "--format", "json", image],
11
- stderr=subprocess.STDOUT,
12
- text=True,
13
- )
14
- except FileNotFoundError as exc:
15
- raise RuntimeError("trivy is not installed or not on PATH") from exc
16
- except subprocess.CalledProcessError as exc:
17
- raise RuntimeError(f"Trivy scan failed for image '{image}': {exc.output}") from exc
18
-
19
- return json.loads(output)
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+
6
+
7
+ def run_trivy_scan(image: str) -> dict:
8
+ try:
9
+ output = subprocess.check_output(
10
+ ["trivy", "image", "--format", "json", image],
11
+ stderr=subprocess.STDOUT,
12
+ text=True,
13
+ )
14
+ except FileNotFoundError as exc:
15
+ raise RuntimeError("trivy is not installed or not on PATH") from exc
16
+ except subprocess.CalledProcessError as exc:
17
+ raise RuntimeError(f"Trivy scan failed for image '{image}': {exc.output}") from exc
18
+
19
+ return json.loads(output)