atlas-iac/testing/tests/test_hermes_scm_broker_internal_coverage.py
jenkins 340ec58b68 hermes: quarantine-scan pushed git objects
Inflate every receive-pack object under strict pack, size, and checksum
bounds, resolve deltas against in-pack bases only, and scan the real
decompressed payloads for runtime-token forms, private keys, SSH key
material, and known provider token formats. Thin packs are rejected so
no pushed content escapes the scan, and upstream Git exchanges now run
under one absolute stream deadline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 15:15:47 -03:00

273 lines
9.1 KiB
Python

"""Behavioral branch coverage for SCM broker protocol helpers."""
from __future__ import annotations
import io
from email.message import Message
import pytest
from testing.tests.test_hermes_scm_broker_support import (
Response,
_load,
_receive_command,
)
def test_broker_redirect_status_and_bounded_read_helpers(monkeypatch):
broker = _load("scm_broker")
with pytest.raises(broker.PolicyError, match="redirects"):
broker.RejectRedirect().redirect_request(
object(), None, 302, "redirect", {}, "https://elsewhere.invalid"
)
class CodeOnly:
def getcode(self):
return 204
assert broker._status(type("Status", (), {"status": 200})()) == 200
assert broker._status(CodeOnly()) == 204
assert broker._status(object()) is None
with pytest.raises(broker.PolicyError, match="safe size"):
broker._read_bounded(io.BytesIO(b"x"), 4, -1)
with pytest.raises(broker.PolicyError, match="safe size"):
broker._read_bounded(io.BytesIO(b"xxxxx"), 4)
with pytest.raises(broker.PolicyError, match="safe size"):
broker._read_bounded(io.BytesIO(b"x"), 4, 2)
with pytest.raises(broker.PolicyError, match="deadline"):
broker._read_bounded(io.BytesIO(b"x"), 4, 1, deadline=0)
timeouts = []
assert (
broker._read_bounded(
io.BytesIO(b"xy"),
4,
2,
deadline=broker.time.monotonic() + 5,
set_timeout=timeouts.append,
)
== b"xy"
)
assert timeouts
def test_spool_bounded_rejects_bounds_early_eof_and_closes_on_read_error():
broker = _load("scm_broker")
for length in (-1, 5):
with pytest.raises(broker.PolicyError, match="safe size"):
broker._spool_bounded(
io.BytesIO(b"x"),
4,
length,
token="sentinel",
context="request",
deadline=999999999,
)
with pytest.raises(broker.PolicyError, match="ended early"):
broker._spool_bounded(
io.BytesIO(b"x"),
4,
2,
token="sentinel",
context="request",
deadline=999999999,
)
class Broken:
def read(self, _size):
raise OSError("broken stream")
with pytest.raises(OSError, match="broken"):
broker._spool_bounded(
Broken(), 4, 1, token="sentinel", context="request", deadline=999999999
)
class _HandlerInput:
def __init__(self, body: bytes, *, content_type="application/json", length=None):
self.headers = Message()
self.headers["Content-Type"] = content_type
if length is None:
length = len(body)
self.headers["Content-Length"] = str(length)
self.rfile = io.BytesIO(body)
self.connection = type(
"Connection", (), {"settimeout": lambda self, value: None}
)()
def test_control_json_loader_requires_fixed_json_object():
broker = _load("scm_broker")
handler = _HandlerInput(b"{}")
handler.headers["Transfer-Encoding"] = "chunked"
with pytest.raises(broker.PolicyError, match="chunked"):
broker._load_json(handler)
with pytest.raises(broker.PolicyError, match="must be JSON"):
broker._load_json(_HandlerInput(b"{}", content_type="text/plain"))
with pytest.raises(broker.PolicyError, match="must be an object"):
broker._load_json(_HandlerInput(b"[]"))
assert broker._load_json(_HandlerInput(b'{"path":"safe"}')) == {"path": "safe"}
@pytest.mark.parametrize(
("target", "expected"),
[
(
"/git/atlas/cassandra.git/info/refs?service=git-upload-pack",
("cassandra", "info/refs", "git-upload-pack"),
),
(
"/git/atlas/cassandra.git/info/refs?service=git-receive-pack",
("cassandra", "info/refs", "git-receive-pack"),
),
(
"/git/atlas/cassandra.git/git-upload-pack",
("cassandra", "git-upload-pack", "git-upload-pack"),
),
(
"/git/atlas/cassandra.git/git-receive-pack",
("cassandra", "git-receive-pack", "git-receive-pack"),
),
],
)
def test_git_target_accepts_each_exact_smart_http_route(target, expected):
broker = _load("scm_broker")
assert broker._git_target(target) == expected
def test_content_length_and_credential_helpers_cover_safe_and_rejected_values():
broker = _load("scm_broker")
headers = Message()
headers["Content-Length"] = "0"
assert broker._content_length(headers, 1) == 0
forms = broker._credential_forms("sentinel")
assert len(forms) == 3 and forms[0] == b"sentinel"
zero = b"0" * 40
commit = b"1" * 40
line = zero + b" " + commit + b" refs/heads/hermes/x " + forms[2] + b"\n"
framed = f"{len(line) + 4:04x}".encode() + line + b"0000"
with pytest.raises(broker.PolicyError, match="credential material"):
broker._validate_receive_pack(framed, "sentinel")
def test_receive_pack_validation_rewinds_stream_and_accepts_bytes():
broker = _load("scm_broker")
body = _receive_command(b"0" * 40, b"1" * 40, b"refs/heads/hermes/rewind")
stream = io.BytesIO(body)
stream.seek(9)
broker._validate_receive_pack(stream, "sentinel")
assert stream.tell() == 0
broker._validate_receive_pack(body, "sentinel")
@pytest.mark.parametrize(
("body", "match"),
[
(b"bad!", "framing"),
(b"0000", "no ref command"),
(b"0003", "framing"),
(b"0008abc", "framing"),
(b"0008a b\n0000", "ref command"),
],
)
def test_receive_pack_rejects_bad_framing_and_commands(body, match):
broker = _load("scm_broker")
with pytest.raises(broker.PolicyError, match=match):
broker._validate_receive_pack(body, "sentinel")
def _packet(old: bytes, new: bytes, ref: bytes, *, terminator=True) -> bytes:
command = old + b" " + new + b" " + ref + b"\n"
framed = f"{len(command) + 4:04x}".encode() + command
return framed + (b"0000" if terminator else b"")
def test_receive_pack_rejects_nonascii_many_commands_and_missing_terminator(
monkeypatch,
):
broker = _load("scm_broker")
zero = b"0" * 40
commit = b"1" * 40
with pytest.raises(broker.PolicyError, match="canonical ASCII"):
broker._validate_receive_pack(
_packet(zero, commit, b"refs/heads/hermes/\xff"), "sentinel"
)
command = _packet(zero, commit, b"refs/heads/hermes/fix", terminator=False)
with pytest.raises(broker.PolicyError, match="terminator"):
broker._validate_receive_pack(command, "sentinel")
limit = _load("receive_pack_scan").MAX_PUSH_COMMANDS
many = command * (limit + 1) + b"0000"
with pytest.raises(broker.PolicyError, match="too many"):
broker._validate_receive_pack(many, "sentinel")
def test_upstream_request_infers_byte_length_and_requires_stream_length():
broker = _load("scm_broker")
seen = []
def opener(request, timeout):
seen.append((request, timeout))
return Response(b"result", content_type="application/x-git-upload-pack-result")
assert (
broker._upstream_git_request(
"/atlas/cassandra.git/git-upload-pack",
method="POST",
body=b"request",
content_type=None,
expected_type="application/x-git-upload-pack-result",
token="sentinel",
opener=opener,
)
== b"result"
)
assert seen[0][0].get_header("Content-length") == "7"
with pytest.raises(broker.PolicyError, match="length is missing"):
broker._upstream_git_request(
"/atlas/cassandra.git/git-upload-pack",
method="POST",
body=io.BytesIO(b"request"),
content_type=None,
expected_type="application/x-git-upload-pack-result",
token="sentinel",
opener=opener,
)
def test_upstream_request_rejects_wrong_type_and_supports_getcode_status():
broker = _load("scm_broker")
class CodeOnly(Response):
def __init__(self, body, content_type):
super().__init__(body, content_type=content_type)
del self.status
def getcode(self):
return 200
assert (
broker._upstream_git_request(
"/atlas/cassandra.git/info/refs?service=git-upload-pack",
method="GET",
body=None,
content_type=None,
expected_type="application/x-git-upload-pack-advertisement",
token="sentinel",
opener=lambda *_a, **_k: CodeOnly(
b"advertisement", "application/x-git-upload-pack-advertisement"
),
)
== b"advertisement"
)
with pytest.raises(broker.PolicyError, match="response type"):
broker._upstream_git_request(
"/atlas/cassandra.git/info/refs?service=git-upload-pack",
method="GET",
body=None,
content_type=None,
expected_type="application/x-git-upload-pack-advertisement",
token="sentinel",
opener=lambda *_a, **_k: Response(b"bad", content_type="text/plain"),
)