diff --git a/CHANGELOG.md b/CHANGELOG.md index b0282e2..7ebc8f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.4.1 + +### Fixed + +- Card-number redaction and the card-number check skip digit runs inside hex, base64 and ids, and `x402.payment_payload` evidence is no longer redacted. Parts of x402 signatures could be replaced with `[redacted:pan]`, which then failed verification and raised a false `evidence_invalid` alert. + ## 0.4.0 ### Added diff --git a/halvorai_ingest/client.py b/halvorai_ingest/client.py index 181fb7b..a7e50a5 100644 --- a/halvorai_ingest/client.py +++ b/halvorai_ingest/client.py @@ -67,6 +67,10 @@ def events_url(endpoint: str) -> str: return base if base.endswith("/v1/events") else base + "/v1/events" +# JSON evidence that carries a signature over its own fields, so redaction would break it. +SIGNED_JSON_EVIDENCE = frozenset({"x402.payment_payload"}) + + @dataclass class ClientStats: emitted: int = 0 @@ -414,6 +418,9 @@ def _redact_event(self, event: dict[str, Any]) -> None: evidence = event.get("evidence") if isinstance(evidence, list): for item in evidence: + # A signed x402 payload is evidence only while byte-identical. + if isinstance(item, dict) and item.get("kind") in SIGNED_JSON_EVIDENCE: + continue if isinstance(item, dict) and item.get("format") == "json" and "value" in item: item["value"] = redact_deep(item["value"]) item["sha256"] = evidence_sha256(item["value"]) diff --git a/halvorai_ingest/redact.py b/halvorai_ingest/redact.py index 8b274a7..96debd9 100644 --- a/halvorai_ingest/redact.py +++ b/halvorai_ingest/redact.py @@ -16,7 +16,8 @@ r"\b(cvc|cvv2?|security code)(\"\s*:\s*\"|\s*[:=]\s*|\s+)(\d{3,4})(\")?", re.IGNORECASE, ) -PAN_RUN = re.compile(r"\d(?:[ -]?\d)*") +# Digits glued to letters or more digits (hex signatures, base64, ids) are not a card number. +PAN_RUN = re.compile(r"(? bool: """True when text holds a Luhn-valid run of 13 to 19 digits (spaces or dashes allowed).""" - for run in re.findall(r"\d(?:[ -]?\d)*", text): + # Same boundaries as the redaction: digits glued to letters (hex, base64, ids) are not a card. + for run in re.findall(r"(?=3.10" diff --git a/tests/test_redact.py b/tests/test_redact.py index f298ec2..f85c56a 100644 --- a/tests/test_redact.py +++ b/tests/test_redact.py @@ -15,3 +15,16 @@ def test_redacts_secret_key() -> None: def test_redacts_cvc() -> None: assert "[redacted:cvc]" in redact_text("cvc: 123") assert "[redacted:cvc]" in redact_text('"cvc":"999"') + + +def test_leaves_digit_runs_inside_hex_base64_and_ids_alone() -> None: + for text in ( + "0x9632e1311996ebcb38d7a4242424242424242b5beb14f513", + "c2lnbmF0dXJl4242424242424242AAA=", + "ord_4242424242424242x", + ): + assert redact_text(text) == text + + +def test_still_finds_a_card_number_in_a_url_path() -> None: + assert redact_text("https://api.example/r/4242424242424242") == "https://api.example/r/[redacted:pan]" diff --git a/uv.lock b/uv.lock index 37257ad..7927e88 100644 --- a/uv.lock +++ b/uv.lock @@ -323,7 +323,7 @@ wheels = [ [[package]] name = "halvorai-ingest" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "cryptography" },