Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .bandit-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"_comment": "Allowlist of accepted bandit HIGH-severity findings, keyed by (path, test_id) — line numbers are intentionally NOT part of the key. Empty means ANY HIGH finding fails the gate. All 18 pre-existing HIGH findings were fixed or converted to justified inline `# nosec` (see .security-suppressions.json); keep this empty unless a genuinely unavoidable HIGH is reviewed and accepted.",
"findings": []
}
15 changes: 15 additions & 0 deletions .security-suppressions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"_comment": "Justified security suppressions. Every inline `# nosec S<n>` / `# nosec B<n>` marker in src/gaia MUST have a matching entry here with a real justification. Do not blanket-suppress to quiet the scanner.",
"suppressions": [
{
"path": "src/gaia/agents/tools/shell_tools.py",
"rule": "B602",
"justification": "Sandboxed shell tool. Every command (and each pipeline segment) is validated against a whitelist via _validate_command before execution; shell=True is enabled ONLY on Windows so cmd.exe can resolve built-ins (dir/cd/type) and pipes that Git-for-Windows tools rely on. Converting to args-list would break piped/whitelisted commands the tool exists to run."
},
{
"path": "src/gaia/mcp/client/transports/stdio.py",
"rule": "B602",
"justification": "Legacy from_command() API accepts a full shell command STRING (documented contract, e.g. commands using pipes/env-expansion) and needs shell parsing. Command is caller-supplied SDK config, not external/untrusted input; modern from_config() passes an args list and runs shell=False."
}
]
}
2 changes: 1 addition & 1 deletion src/gaia/agents/tools/shell_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ def run_shell_command(
timeout=timeout,
check=False,
env=os.environ.copy(),
shell=use_shell,
shell=use_shell, # nosec B602 - Windows-only; command whitelist-validated above, shell needed for cmd.exe built-ins/pipes
)
duration = time.monotonic() - start_time

Expand Down
38 changes: 24 additions & 14 deletions src/gaia/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3653,9 +3653,11 @@ def main():
system = platform.system()
try:
if system == "Windows":
subprocess.run(
["start", "", mailto_url], shell=True, check=True
)
# os.startfile uses ShellExecute (no shell parsing),
# safe for the user-built mailto URL (which contains
# '&'-separated query params cmd would mis-parse).
# Windows-only attr; guarded by the platform check.
os.startfile(mailto_url) # pylint: disable=no-member
elif system == "Darwin": # macOS
subprocess.run(["open", mailto_url], check=True)
else: # Linux/Unix
Expand Down Expand Up @@ -4776,11 +4778,14 @@ def main():

def kill_process_by_port(port):
"""Find and kill a process running on a specific port."""
try:
port = int(port)
except (ValueError, TypeError):
return {"success": False, "message": f"Invalid port number: {port!r}"}
try:
if sys.platform.startswith("win"):
# Windows implementation
cmd = f"netstat -ano | findstr :{port}"
output = subprocess.check_output(cmd, shell=True).decode()
# Windows implementation (filter netstat output in Python, no shell pipe)
output = subprocess.check_output(["netstat", "-ano"]).decode()
if output:
# Split output into lines and process each line
for line in output.strip().split("\n"):
Expand All @@ -4792,7 +4797,9 @@ def kill_process_by_port(port):
pid = int(parts[-1])
if pid > 0: # Ensure we don't try to kill PID 0
subprocess.run(
f"taskkill /PID {pid} /F", shell=True, check=True
["taskkill", "/PID", str(pid), "/F"],
shell=False,
check=True,
)
return {
"success": True,
Expand All @@ -4808,16 +4815,19 @@ def kill_process_by_port(port):
# Linux/Unix implementation
try:
# Use lsof to find process using the port
cmd = f"lsof -ti:{port}"
output = subprocess.check_output(cmd, shell=True).decode().strip()
output = (
subprocess.check_output(["lsof", f"-ti:{port}"]).decode().strip()
)
if output:
pids = output.split("\n")
killed_pids = []
for pid_str in pids:
try:
pid = int(pid_str.strip())
if pid > 0:
subprocess.run(f"kill -9 {pid}", shell=True, check=True)
subprocess.run(
["kill", "-9", str(pid)], shell=False, check=True
)
killed_pids.append(str(pid))
except (ValueError, subprocess.CalledProcessError):
continue
Expand All @@ -4834,8 +4844,8 @@ def kill_process_by_port(port):
# If lsof is not available, try netstat + ps approach
try:
# Use netstat to find the port, then extract PID
cmd = f"netstat -tulpn | grep :{port}"
output = subprocess.check_output(cmd, shell=True).decode()
# (filter output in Python, no shell pipe)
output = subprocess.check_output(["netstat", "-tulpn"]).decode()
if output:
for line in output.strip().split("\n"):
if f":{port}" in line:
Expand All @@ -4847,8 +4857,8 @@ def kill_process_by_port(port):
pid = int(part.split("/")[0])
if pid > 0:
subprocess.run(
f"kill -9 {pid}",
shell=True,
["kill", "-9", str(pid)],
shell=False,
check=True,
)
return {
Expand Down
12 changes: 11 additions & 1 deletion src/gaia/llm/lemonade_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,7 +1109,17 @@ def terminate_server(self):
# For subprocess.Popen
if sys.platform.startswith("win") and self.server_process.pid:
# On Windows, use taskkill to ensure process tree is terminated
os.system(f"taskkill /F /PID {self.server_process.pid} /T")
subprocess.run(
[
"taskkill",
"/F",
"/PID",
str(self.server_process.pid),
"/T",
],
shell=False,
check=False,
)
elif self.server_process.pid:
# On Linux/Unix, kill the process group to terminate child processes
try:
Expand Down
6 changes: 5 additions & 1 deletion src/gaia/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ def configure_console_encoding():

# Also try to set the console code page to UTF-8
try:
# chcp is a cmd.exe builtin; invoke via cmd /c so we avoid shell=True
subprocess.run(
["chcp", "65001"], capture_output=True, shell=True, check=False
["cmd", "/c", "chcp", "65001"],
capture_output=True,
shell=False,
check=False,
)
except (subprocess.SubprocessError, OSError, FileNotFoundError):
pass # Ignore if chcp command fails
Expand Down
6 changes: 5 additions & 1 deletion src/gaia/mcp/client/transports/stdio.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,13 @@ def connect(self) -> bool:
merged_env = os.environ.copy()
merged_env.update(self.env)

# Legacy from_command() path passes a full shell command string
# (documented contract) and needs shell parsing; modern from_config()
# passes an args list and runs shell=False. Command is caller-supplied
# SDK config, not external/untrusted input.
self._process = subprocess.Popen(
cmd,
shell=use_shell,
shell=use_shell, # nosec B602 - legacy shell-string API, trusted SDK config
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
Expand Down
2 changes: 1 addition & 1 deletion src/gaia/mcp/context7_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def _doc_cache_file(self, library: str, query: str) -> Path:
Path to cache file
"""
key = f"{library}:{query}"
hash_key = hashlib.md5(key.encode()).hexdigest()[:12]
hash_key = hashlib.md5(key.encode(), usedforsecurity=False).hexdigest()[:12]
safe_lib = library.replace("/", "_").replace(".", "_")
return self.docs_dir / f"{safe_lib}_{hash_key}.json"

Expand Down
16 changes: 10 additions & 6 deletions src/gaia/ui/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,24 @@ def ensure_webui_built(log_fn=print, warn_fn=None, _webui_dir=None):
warn_fn("Warning: npm not found. Cannot auto-rebuild Agent UI frontend.")
return False

# On Windows, npm is a .cmd batch file requiring shell execution
_shell = sys.platform == "win32"
# On Windows npm is a .cmd batch file, which CreateProcess can't launch
# directly; invoke via `cmd /c` (args are static) so we avoid shell=True.
def _npm(*args):
if sys.platform == "win32":
return ["cmd", "/c", "npm", *args]
return ["npm", *args]

# Step 1 — npm install (only if node_modules/ missing)
if not (webui_dir / "node_modules").is_dir():
log_fn("Installing Agent UI frontend dependencies...")
try:
subprocess.run(
["npm", "install"],
_npm("install"),
cwd=str(webui_dir),
check=True,
capture_output=True,
text=True,
shell=_shell,
shell=False,
)
except subprocess.CalledProcessError as e:
warn_fn(f"Warning: npm install failed: {e.stderr}")
Expand All @@ -106,10 +110,10 @@ def ensure_webui_built(log_fn=print, warn_fn=None, _webui_dir=None):
log_fn("Building Agent UI frontend...")
try:
subprocess.run(
["npm", "run", "build"],
_npm("run", "build"),
cwd=str(webui_dir),
check=True,
shell=_shell,
shell=False,
)
log_fn("Agent UI frontend built successfully.")
return True
Expand Down
16 changes: 9 additions & 7 deletions src/gaia/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ def kill_process_on_port(port):
raise ValueError(f"Invalid port number: {port!r}")
try:
if sys.platform.startswith("win"):
# Windows: use netstat + taskkill
# Windows: use netstat + taskkill (filter output in Python, no shell pipe)
result = subprocess.run(
f"netstat -ano | findstr :{port}",
shell=True,
["netstat", "-ano"],
shell=False,
capture_output=True,
text=True,
check=False,
Expand All @@ -39,7 +39,7 @@ def kill_process_on_port(port):
print(f"Found process with PID {pid} on port {port}")
try:
subprocess.run(
f"taskkill /F /PID {pid}", shell=True, check=False
["taskkill", "/F", "/PID", pid], shell=False, check=False
)
print(f"Killed process with PID {pid}")
except Exception as e:
Expand All @@ -50,8 +50,8 @@ def kill_process_on_port(port):
else:
# Unix/macOS: use lsof + kill
result = subprocess.run(
f"lsof -ti :{port}",
shell=True,
["lsof", "-ti", f":{port}"],
shell=False,
capture_output=True,
text=True,
check=False,
Expand All @@ -64,7 +64,9 @@ def kill_process_on_port(port):
if pid:
print(f"Found process with PID {pid} on port {port}")
try:
subprocess.run(f"kill -9 {pid}", shell=True, check=False)
subprocess.run(
["kill", "-9", pid], shell=False, check=False
)
print(f"Killed process with PID {pid}")
except Exception as e:
print(f"Error killing PID {pid}: {e}")
Expand Down
Loading
Loading