@raghulm/aegis-mcp 1.0.5 → 1.0.9
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 +340 -323
- package/audit/audit_logger.py +62 -62
- package/package.json +53 -54
- package/policies/roles.yaml +34 -34
- package/policies/scope_rules.yaml +16 -16
- package/requirements.txt +8 -8
- package/run_stdio.py +22 -22
- package/server/auth.py +69 -69
- package/server/config.py +82 -82
- package/server/health.py +19 -19
- package/server/logging.py +33 -33
- package/server/main.py +224 -212
- package/server/stdio.py +7 -7
- package/tools/aws/ec2.py +26 -26
- package/tools/aws/s3.py +54 -54
- package/tools/cicd/pipeline.py +33 -33
- package/tools/git/repo.py +22 -22
- package/tools/kubernetes/audit.py +108 -108
- package/tools/kubernetes/pods.py +27 -27
- package/tools/network/headers.py +99 -99
- package/tools/network/port_scanner.py +66 -66
- package/tools/network/ssl_checker.py +65 -65
- package/tools/security/deps.py +103 -103
- package/tools/security/secrets.py +91 -91
- package/tools/security/semgrep.py +261 -261
- package/tools/security/terraform.py +382 -0
- package/tools/security/trivy.py +19 -19
|
@@ -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
|
+
}
|