agnes-the-ai-analyst/tests/test_telegram_bot_runner.py
ZdenekSrotyr c97fd504c5 release: 0.45.0 — easy-wins bundle (#84 #164 #177 #178 #203 #204)
Operator-and-analyst quality bundle: a security fix for the optional
Telegram bot, two CLI gaps closed, and three rounds of UX polish on
`agnes diagnose` and `agnes pull` so non-TTY consumers (CI runners,
Claude Code SessionStart hooks, sub-agent watchdogs) get readable,
actionable signal.

- Pairing-code RNG: random.choices -> secrets.choice (CSPRNG).
- Telegram script runner: refuse out-of-shape usernames before sudo -u.

CLAUDE.md.bak.<ISO-timestamp> before regenerating.

- agnes admin unregister-table <id> -> DELETE /api/admin/registry/{id}
- agnes admin update-table <id> --field=value ...  -> PUT /api/admin/registry/{id}

response but never promotes the headline. BQ billing-equals-data check
downgraded warning -> info.

default (5 s / 1 MiB vs 30 s / 10%) so sub-agent watchdogs don't kill
the pull as a hung process. New env knobs:
AGNES_PULL_PROGRESS_INTERVAL_{SECONDS,BYTES}.

--include-schema (or ?include=schema) to opt back in.

Tests: 120 passed across the touched modules, including new tests for
each fix. Pre-existing failures on main (DB migration v1->v9, binary
rename) are unrelated and not introduced here.
2026-05-07 11:43:16 +02:00

60 lines
2.2 KiB
Python

"""Tests for `services.telegram_bot.runner` username validation.
Issue #84: the runner shells out via `sudo -u <username>`. Without an
input gate, a username controlled by an attacker (via tampering with
the linked-users JSON, or via an upstream caller that doesn't validate)
could carry sudo flags or shell metacharacters. Every value flowing
into `subprocess.run([..., "-u", username, ...])` must match a
POSIX-conservative shape; bad shapes are refused before the subprocess
fires.
"""
from unittest.mock import patch
from services.telegram_bot.runner import _USERNAME_RE, run_user_script
def test_username_regex_accepts_normal_usernames():
for u in ("alice", "bob42", "data_ops", "svc-agnes", "_system"):
assert _USERNAME_RE.match(u), u
def test_username_regex_rejects_obvious_attacks():
bad = [
"-u", # sudo flag
"--shell=/bin/bash", # GNU long flag
"alice; rm -rf /", # shell metachar
"alice && id",
"alice|cat /etc/shadow",
"alice$IFS",
"1starts_with_digit",
"alice/with/slash",
"alice with space",
"", # empty
"a" * 33, # too long
]
for u in bad:
assert not _USERNAME_RE.match(u), u
def test_run_user_script_refuses_bad_username_without_subprocess():
"""If validation refuses the username, subprocess.run must not fire.
Pre-fix, a tampered telegram_users.json with `username = "-u root"`
would have sudo'd as root via flag injection. The fix has the runner
short-circuit to None before any subprocess call.
"""
with patch("services.telegram_bot.runner.subprocess.run") as run_mock:
result = run_user_script("-u", "ok_script.py")
assert result is None
run_mock.assert_not_called()
def test_run_user_script_refuses_bad_script_name_without_subprocess():
"""Existing guard at L24 rejects non-.py scripts; verify it still does
after the new username gate so a valid username + bad script combo
doesn't slip through and run."""
with patch("services.telegram_bot.runner.subprocess.run") as run_mock:
result = run_user_script("alice", "not_python.sh")
assert result is None
run_mock.assert_not_called()