atlas-iac/testing/tests/test_hermes_handoff_redaction.py

193 lines
6.3 KiB
Python

"""Contracts for the handoff harness credential screen and output bounding."""
from __future__ import annotations
import json
import pytest
from testing.tests.test_hermes_handoff_support import load_handoff_module
redaction = load_handoff_module("hermes_handoff_redaction")
@pytest.mark.parametrize(
"text",
[
"token: ghp_ABCDEFGHIJKLMNOPQRSTUV1234",
"Authorization: Bearer abcdefghijklmnop",
"cookie: session=abcdefghijklmnop",
"password=hunter2000secret",
"-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END OPENSSH PRIVATE KEY-----",
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI",
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0.SflKxwRJ",
"https://user:supersecretvalue@scm.example.dev/x",
"vault token hvs.CAESIJabcdefghijklmnop",
"aws AKIAIOSFODNN7EXAMPLE",
],
)
def test_credential_shapes_are_screened(text: str) -> None:
assert redaction.contains_credential_shape(text)
assert redaction.REDACTED in redaction.redact(text)
@pytest.mark.parametrize(
"text",
[
"sha256:37ebf720c783ae908a602916ffccf88d43d205a157957f5dc4b487867aee45e7",
"ab346f55509d584e457fe26cf90be3078f7a375c",
'"automountServiceAccountToken": false',
'"key": "kubernetes.io/arch"',
"namespace: hermes",
"replicas: 3",
"checked_at: 2026-08-17T09:17:07.224400+00:00",
],
)
def test_ordinary_evidence_survives_screening(text: str) -> None:
assert redaction.redact(text) == text
assert not redaction.contains_credential_shape(text)
def test_screened_json_is_still_parsable_json() -> None:
"""A screen that corrupts structure would break every downstream evaluator."""
payload = {
"spec": {"automountServiceAccountToken": True, "key": "kubernetes.io/arch"},
"data": {"password": "a-long-enough-secret-value"},
}
screened = redaction.redact(json.dumps(payload, indent=2))
reparsed = json.loads(screened)
assert reparsed["spec"]["automountServiceAccountToken"] is True
assert reparsed["spec"]["key"] == "kubernetes.io/arch"
assert reparsed["data"]["password"] == redaction.REDACTED
def test_high_entropy_runs_are_screened_but_hex_digests_are_not() -> None:
assert (
redaction.redact("Zm9vYmFyYmF6cXV4-Quux_1234567890ABCDEFghij")
!= redaction.REDACTED
)
long_mixed = "aB3" + "x9Y2z_" * 10
assert redaction.redact(long_mixed) == redaction.REDACTED
assert redaction.redact("f" * 64) == "f" * 64
@pytest.mark.parametrize(
("key", "sensitive"),
[
("ANTHROPIC_API_KEY", True),
("gitea_token", True),
("clientSecret", True),
("Set-Cookie", True),
("automountServiceAccountToken", True),
("key", False),
("namespace", False),
("", False),
],
)
def test_key_sensitivity_classification(key: str, sensitive: bool) -> None:
assert redaction.is_sensitive_key(key) is sensitive
@pytest.mark.parametrize(
("value", "possible"),
[
("true", False),
("False", False),
("3", False),
("12.5", False),
("short", False),
("", False),
("a-long-enough-secret", True),
('"another-long-secret"', True),
("1234567890123", False),
("-12.5e9", False),
],
)
def test_value_shape_gates_screening(value: str, possible: bool) -> None:
assert redaction.could_hold_secret(value) is possible
def test_bound_truncates_to_a_byte_budget_and_reports_it() -> None:
text, truncated = redaction.bound("x" * 100, max_bytes=10)
assert truncated
assert len(text.encode()) <= 10
assert text == redaction.TRUNCATION_NOTE[:10]
text, truncated = redaction.bound("short", max_bytes=64)
assert (text, truncated) == ("short", False)
def test_bound_with_no_budget_keeps_nothing() -> None:
assert redaction.bound("content", max_bytes=0) == ("", True)
assert redaction.bound("", max_bytes=0) == ("", False)
def test_bound_never_splits_a_multibyte_character() -> None:
text, truncated = redaction.bound("é" * 10, max_bytes=5)
assert truncated
assert len(text.encode()) <= 5
def test_safe_text_screens_before_it_bounds() -> None:
"""A boundary must never preserve a useful credential prefix."""
payload = "password=" + "a" * 200
text, truncated = redaction.safe_text(payload, max_bytes=20)
assert not truncated
assert redaction.REDACTED in text
def test_no_byte_budget_exposes_a_credential_prefix() -> None:
payload = "password=A9secret-prefix-that-must-never-survive"
for budget in range(1, 64):
screened, _ = redaction.safe_text(payload, max_bytes=budget)
assert "A9" not in screened
assert len(screened.encode()) <= budget
def test_scrub_walks_nested_structures_and_keeps_useful_types() -> None:
scrubbed = redaction.scrub(
{
"token": "a-long-enough-secret",
"automountServiceAccountToken": False,
"items": [{"password": "another-long-secret"}, 3, None],
"note": ("tuple", "values"),
}
)
assert scrubbed["token"] == redaction.REDACTED
assert scrubbed["automountServiceAccountToken"] is False
assert scrubbed["items"][0]["password"] == redaction.REDACTED
assert scrubbed["items"][1] == 3
assert scrubbed["items"][2] is None
assert scrubbed["note"] == ["tuple", "values"]
def test_scrub_screens_short_numeric_nested_values_and_mapping_keys() -> None:
scrubbed = redaction.scrub(
{
"token": 1234,
"credentials": {"x": "y"},
"ghp_ABCDEF1234567890": "mapping-key",
"private_key": "x",
}
)
assert scrubbed["token"] == redaction.REDACTED
assert scrubbed["credentials"] == redaction.REDACTED
assert scrubbed["private_key"] == redaction.REDACTED
assert not any("ghp_" in key for key in scrubbed)
def test_unterminated_private_key_blocks_are_screened() -> None:
assert "PRIVATE KEY" not in redaction.redact("-----BEGIN PRIVATE KEY-----\nabc")
def test_screening_is_idempotent() -> None:
once = redaction.redact("api_key=AKIAIOSFODNN7EXAMPLE")
assert redaction.redact(once) == once
assert redaction.redact("") == ""
def test_normalise_key_strips_separators() -> None:
assert redaction.normalise_key("Client-Secret_v2") == "clientsecretv2"