You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(security): remove pre-existing bandit HIGH findings and enable the HIGH gate (#2350)
Bandit ran warning-only, so 18 pre-existing HIGH-severity findings sat
unblocked in core paths (CLI, subprocess helpers, MCP stdio transport,
Lemonade launch). Before: a new `shell=True` injection site or weak-hash
regression would pass lint and ship. After: all 18 are fixed (16 by
removing the risk, 2 kept as justified suppressions) and any **new**
HIGH finding now fails `util/lint.py --bandit`.
### How each finding was handled
**Fixed by removing the risk (16):** `shell=True` string commands
converted to `shell=False` args-lists.
- `util.py` / `cli.py` port-kill helpers: run `netstat`/`lsof` bare and
filter output in Python instead of piping through the shell;
`taskkill`/`kill` take args-lists. Port inputs are `int()`-validated
first.
- `lemonade_client.py`: `os.system(taskkill …)` → `subprocess.run([...],
shell=False)` (B605).
- `logger.py`: `chcp` (a cmd.exe builtin) → `cmd /c chcp`.
- `ui/build.py`: `npm` calls (`.cmd` shims) → `cmd /c npm …` on Windows,
bare on other platforms.
- `cli.py` mailto opener: `start "" <url>` → `os.startfile()`
(ShellExecute, no shell parsing) — this one mattered because the URL is
user-built.
- `context7_cache.py`: MD5 is a cache key, fixed with
`usedforsecurity=False` (B324).
**Kept as justified suppressions (2)** — inline `# nosec B602` + a
matching entry in `.security-suppressions.json`:
- `agents/tools/shell_tools.py` — sandboxed shell executor; every
command (and pipeline segment) is whitelist-validated before running,
and `shell=True` is Windows-only so cmd.exe can resolve built-ins/pipes
the tool exists to run.
- `mcp/client/transports/stdio.py` — legacy `from_command()` accepts a
full shell command **string** (documented contract), needs shell
parsing; command is trusted SDK config, and modern `from_config()`
already runs `shell=False`.
### The gate
`util/check_security_gates.py` adds `new_bandit_highs(results,
baseline)`, keyed by `(normalized path, test_id)` — never line number,
so cosmetic line shifts don't trip it. Wired into `util/lint.py`'s
`check_bandit()`; `.bandit-baseline.json` ships as an empty allowlist so
**any** HIGH fails.
### Test plan
- [x] `python -m bandit -r src/gaia -ll -f json` → 0 HIGH findings
- [x] `python util/lint.py --bandit` → PASS
- [x] Negative check: adding a `subprocess.run(f"echo {x}", shell=True)`
probe makes the gate exit 1; passes again once removed
- [x] `pytest tests/unit/test_check_security_gates.py` → 18 passed
- [x] `pytest tests/unit/test_webui_build.py tests/unit/mcp/client/
tests/unit/test_shell_guardrails.py` → pass
- [x] Positive-path functional test: started a real listener, confirmed
the converted `netstat` detection + `taskkill` args-list finds and kills
it and frees the port
- [x] `black`/`isort` clean on all changed files
---------
Co-authored-by: Ovtcharov <kovtchar@amd.com>
"_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.",
"_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.",
3
+
"suppressions": [
4
+
{
5
+
"path": "src/gaia/agents/tools/shell_tools.py",
6
+
"rule": "B602",
7
+
"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."
"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."
0 commit comments