"""Quarantine content-scanning contracts for brokered Git pushes.""" from __future__ import annotations import hashlib import io import zlib import pytest from testing.tests.test_hermes_scm_broker_support import ( _blob_pack, _load, _object_entry, _pack_of, _receive_command, ) ZERO = b"0" * 40 COMMIT = b"1" * 40 TOKEN = "runtime-sentinel" FORMS = (b"runtime-sentinel",) def _request(pack: bytes, ref: bytes = b"refs/heads/hermes/scan") -> bytes: return _receive_command(ZERO, COMMIT, ref, pack) def test_clean_push_with_real_content_passes(): scan = _load("receive_pack_scan") pack = _blob_pack( b"# services/example/deployment.yaml\napiVersion: apps/v1\n", b"tree 5b3a\nparent none\n", ) scan.validate_receive_pack(_request(pack), TOKEN, FORMS) @pytest.mark.parametrize( "payload", [ b"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaA==\n", b"-----BEGIN RSA PRIVATE KEY-----\nMIIE\n", b"key = ghp_" + b"a1" * 12, b"aws_access_key_id = AKIAABCDEFGHIJKLMNOP", b"jwt eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJlLXNlZ21lbnQ", b"hook https://hooks.slack.com/services/T000/B000/tokentokentokentoken", b"AGE-SECRET-KEY-1QQPCXR7QQPCXR7QQPCXR7QQPCXR7", b"PuTTY-User-Key-File-3: ssh-ed25519", b"Authorization: Bearer abc123-tokenvalue", b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF2c hermes@atlas", b"gitea gta_" + b"z9" * 12, ], ) def test_credential_shaped_object_content_is_rejected(payload): scan = _load("receive_pack_scan") with pytest.raises(scan.PolicyError, match="credential-shaped"): scan.validate_receive_pack(_request(_blob_pack(payload)), TOKEN, FORMS) def test_runtime_token_hidden_by_compression_is_still_caught(): scan = _load("receive_pack_scan") pack = _blob_pack(b"config value: runtime-sentinel\n") # The compressed wire bytes do not contain the token; only the # decompressed payload does. assert b"runtime-sentinel" not in pack with pytest.raises(scan.PolicyError, match="runtime credential material"): scan.validate_receive_pack(_request(pack), TOKEN, FORMS) def _size_varint(value: int) -> bytes: out = bytearray() while True: byte = value & 0x7F value >>= 7 if value: out.append(byte | 0x80) else: out.append(byte) return bytes(out) def _typed_header(type_code: int, size: int) -> bytes: byte = (type_code << 4) | (size & 0x0F) size >>= 4 header = bytearray() while size: header.append(byte | 0x80) byte = size & 0x7F size >>= 7 header.append(byte) return bytes(header) def test_secret_assembled_only_by_delta_resolution_is_caught(): scan = _load("receive_pack_scan") base = b"docs: AKIAABCD" completion = b"EFGHIJKLMNOP" delta = ( _size_varint(len(base)) + _size_varint(len(base) + len(completion)) + bytes([0x80 | 0x10, len(base)]) + bytes([len(completion)]) + completion ) base_sha = hashlib.sha1(b"blob %d\x00" % len(base) + base).digest() delta_entry = _typed_header(7, len(delta)) + base_sha + zlib.compress(delta) pack = _pack_of([_object_entry(3, base), delta_entry]) # Neither the base nor the delta insert alone matches; the resolved # payload is the only place the credential exists. with pytest.raises(scan.PolicyError, match="runtime credential|credential-shaped"): scan.validate_receive_pack(_request(pack), TOKEN, FORMS) def test_thin_pack_deltas_are_rejected_outright(): scan = _load("receive_pack_scan") delta = _size_varint(4) + _size_varint(1) + bytes([1]) + b"x" entry = _typed_header(7, len(delta)) + b"\xbb" * 20 + zlib.compress(delta) with pytest.raises(scan.PolicyError, match="push full packs"): scan.validate_receive_pack(_request(_pack_of([entry])), TOKEN, FORMS) def test_trailing_bytes_after_the_pack_are_rejected(): scan = _load("receive_pack_scan") body = _request(_pack_of([])) + b"extra" with pytest.raises(scan.PolicyError, match="bytes after its pack"): scan.validate_receive_pack(body, TOKEN, FORMS) @pytest.mark.parametrize( ("line", "match"), [ (b"shallow " + COMMIT, "ref command is invalid"), (COMMIT + b" " + COMMIT + b" refs/heads/hermes/x", "feature-branch creation"), (ZERO + b" " + ZERO + b" refs/heads/hermes/x", "feature-branch creation"), (ZERO + b" " + COMMIT + b" refs/heads/main", "namespaced feature branches"), (ZERO + b" " + COMMIT + b" refs/tags/v1", "namespaced feature branches"), (ZERO + b" " + COMMIT + b" refs/heads/hermes/\xff", "canonical ASCII"), (b"zz" * 20 + b" " + COMMIT + b" refs/heads/hermes/x", "ref command is invalid"), ], ) def test_push_commands_outside_the_narrow_grant_are_rejected(line, match): scan = _load("receive_pack_scan") framed = f"{len(line) + 5:04x}".encode() + line + b"\n" + b"0000" with pytest.raises(scan.PolicyError, match=match): scan.validate_receive_pack(framed + _pack_of([]), TOKEN, FORMS) def test_command_stream_bounds_are_enforced(): scan = _load("receive_pack_scan") with pytest.raises(scan.PolicyError, match="no ref command"): scan.validate_receive_pack(b"0000" + _pack_of([]), TOKEN, FORMS) with pytest.raises(scan.PolicyError, match="omitted its command terminator"): scan.validate_receive_pack(b"", TOKEN, FORMS) line = ZERO + b" " + COMMIT + b" refs/heads/hermes/x\n" framed = f"{len(line) + 4:04x}".encode() + line many = framed * (scan.MAX_PUSH_COMMANDS + 1) + b"0000" + _pack_of([]) with pytest.raises(scan.PolicyError, match="too many ref commands"): scan.validate_receive_pack(many, TOKEN, FORMS) with pytest.raises(scan.PolicyError, match="framing is invalid"): scan.validate_receive_pack(b"zzzz", TOKEN, FORMS) with pytest.raises(scan.PolicyError, match="framing is invalid"): scan.validate_receive_pack(b"0003", TOKEN, FORMS) with pytest.raises(scan.PolicyError, match="framing is invalid"): scan.validate_receive_pack(b"0009abc", TOKEN, FORMS) def test_credential_forms_in_command_lines_are_rejected(): scan = _load("receive_pack_scan") line = ZERO + b" " + COMMIT + b" refs/heads/hermes/x runtime-sentinel\n" framed = f"{len(line) + 4:04x}".encode() + line + b"0000" with pytest.raises(scan.PolicyError, match="runtime credential material"): scan.validate_receive_pack(framed + _pack_of([]), TOKEN, FORMS) def test_scan_crosses_chunk_boundaries_with_carry(monkeypatch): scan = _load("receive_pack_scan") monkeypatch.setattr(scan, "SCAN_CHUNK", 8) monkeypatch.setattr(scan, "SCAN_CARRY", 48) payload = b"x" * 20 + b" AKIAABCDEFGHIJKLMNOP " + b"y" * 20 with pytest.raises(scan.PolicyError, match="credential-shaped"): scan._scan_payload(io.BytesIO(payload), FORMS) scan._scan_payload(io.BytesIO(b"clean " * 40), FORMS) scan._scan_payload(io.BytesIO(b""), FORMS) def test_scan_loose_object_reports_type_and_scans_content(): scan = _load("receive_pack_scan") clean = zlib.compress(b"blob 5\x00clean") assert scan.scan_loose_object(clean, FORMS) == 3 secret = b"-----BEGIN EC PRIVATE KEY-----" dirty = zlib.compress(b"blob %d\x00" % len(secret) + secret) with pytest.raises(scan.PolicyError, match="credential-shaped"): scan.scan_loose_object(dirty, FORMS) leaked = b"the runtime-sentinel value" with pytest.raises(scan.PolicyError, match="runtime credential material"): scan.scan_loose_object( zlib.compress(b"blob %d\x00" % len(leaked) + leaked), FORMS ) def test_content_patterns_extend_the_shared_standalone_corpus(): scan = _load("receive_pack_scan") policy = _load("gitea_api_policy") assert len(scan.CONTENT_SECRET_PATTERNS) == len(policy.STANDALONE_SECRET_PATTERNS) + 3 assert all( isinstance(pattern.pattern, bytes) for pattern in scan.CONTENT_SECRET_PATTERNS ) def test_validation_rewinds_the_stream_for_upstream_forwarding(): scan = _load("receive_pack_scan") body = _request(_blob_pack(b"safe content")) stream = io.BytesIO(body) scan.validate_receive_pack(stream, TOKEN, FORMS) assert stream.tell() == 0 with pytest.raises(scan.PolicyError): scan.validate_receive_pack(io.BytesIO(b"zzzz"), TOKEN, FORMS)