mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-19 00:47:22 +02:00
131 lines
4.2 KiB
Python
131 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import inspect
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi import WebSocketDisconnect
|
|
|
|
from routers import agent_shell
|
|
from services import agent_shell_ws_token as ws_tokens
|
|
from services import updater
|
|
|
|
|
|
class _FakeWebSocket:
|
|
def __init__(self, headers: dict[str, str] | None = None, host: str = "127.0.0.1") -> None:
|
|
self.headers = headers or {}
|
|
self.client = SimpleNamespace(host=host)
|
|
self.closed: tuple[int, str] | None = None
|
|
|
|
async def close(self, code: int = 1000, reason: str = "") -> None:
|
|
self.closed = (code, reason)
|
|
|
|
|
|
def setup_function() -> None:
|
|
ws_tokens.reset_agent_shell_ws_tokens_for_tests()
|
|
|
|
|
|
def teardown_function() -> None:
|
|
ws_tokens.reset_agent_shell_ws_tokens_for_tests()
|
|
|
|
|
|
def test_agent_shell_query_no_longer_accepts_admin_key() -> None:
|
|
parameters = inspect.signature(agent_shell.agent_shell_websocket).parameters
|
|
assert "admin_key" not in parameters
|
|
assert "ws_token" in parameters
|
|
|
|
|
|
def test_loopback_source_ip_is_not_agent_shell_authorization() -> None:
|
|
ws = _FakeWebSocket({"host": "127.0.0.1:8000"})
|
|
with pytest.raises(WebSocketDisconnect):
|
|
asyncio.run(agent_shell._authorize_agent_shell_ws(ws, ""))
|
|
assert ws.closed is not None
|
|
assert ws.closed[0] == 4403
|
|
|
|
|
|
def test_browser_one_time_token_allows_same_host_origin() -> None:
|
|
token, _ = ws_tokens.mint_agent_shell_ws_token()
|
|
ws = _FakeWebSocket(
|
|
{
|
|
"host": "127.0.0.1:8000",
|
|
"origin": "http://127.0.0.1:3000",
|
|
}
|
|
)
|
|
asyncio.run(agent_shell._authorize_agent_shell_ws(ws, token))
|
|
|
|
# The capability is single-use.
|
|
second = _FakeWebSocket(
|
|
{
|
|
"host": "127.0.0.1:8000",
|
|
"origin": "http://127.0.0.1:3000",
|
|
}
|
|
)
|
|
with pytest.raises(WebSocketDisconnect):
|
|
asyncio.run(agent_shell._authorize_agent_shell_ws(second, token))
|
|
|
|
|
|
def test_cross_origin_browser_is_rejected_before_token_use() -> None:
|
|
token, _ = ws_tokens.mint_agent_shell_ws_token()
|
|
ws = _FakeWebSocket(
|
|
{
|
|
"host": "127.0.0.1:8000",
|
|
"origin": "https://attacker.example",
|
|
}
|
|
)
|
|
with pytest.raises(WebSocketDisconnect):
|
|
asyncio.run(agent_shell._authorize_agent_shell_ws(ws, token))
|
|
assert ws.closed is not None
|
|
assert ws.closed[0] == 4403
|
|
# Rejection happens before capability consumption.
|
|
assert ws_tokens.consume_agent_shell_ws_token(token) is True
|
|
|
|
|
|
def test_agent_shell_token_store_stays_bounded_without_recursive_locking() -> None:
|
|
minted = [ws_tokens.mint_agent_shell_ws_token()[0] for _ in range(400)]
|
|
assert len(ws_tokens._store) <= ws_tokens._MAX_ACTIVE_TOKENS
|
|
assert ws_tokens.consume_agent_shell_ws_token(minted[-1]) is True
|
|
|
|
|
|
def test_updater_refuses_same_release_checksum_as_sole_trust_root(tmp_path, monkeypatch) -> None:
|
|
archive = tmp_path / "ShadowBroker_v9.9.9.zip"
|
|
archive.write_bytes(b"archive-under-test")
|
|
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
|
|
|
|
monkeypatch.delenv("MESH_UPDATE_SHA256", raising=False)
|
|
monkeypatch.setattr(updater, "_load_baked_in_release_digests", lambda: {})
|
|
monkeypatch.setattr(
|
|
updater,
|
|
"_fetch_sha256sums",
|
|
lambda _url: {archive.name: digest},
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="no independent archive digest"):
|
|
updater._validate_zip_hash(
|
|
str(archive),
|
|
asset_name=archive.name,
|
|
sha256sums_url="https://github.com/BigBodyCobain/Shadowbroker/releases/download/v9.9.9/SHA256SUMS.txt",
|
|
release_tag="v9.9.9",
|
|
)
|
|
|
|
|
|
def test_updater_accepts_preinstalled_baked_digest(tmp_path, monkeypatch) -> None:
|
|
archive = tmp_path / "ShadowBroker_v9.9.9.zip"
|
|
archive.write_bytes(b"trusted-archive")
|
|
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
|
|
|
|
monkeypatch.delenv("MESH_UPDATE_SHA256", raising=False)
|
|
monkeypatch.setattr(
|
|
updater,
|
|
"_load_baked_in_release_digests",
|
|
lambda: {"v9.9.9": {archive.name: digest}},
|
|
)
|
|
|
|
note = updater._validate_zip_hash(
|
|
str(archive),
|
|
asset_name=archive.name,
|
|
release_tag="v9.9.9",
|
|
)
|
|
assert "baked-in digest" in note
|