From e76bed8ddcb9f4d743d962ce0b171fac1491b69b Mon Sep 17 00:00:00 2001 From: CoderDeltaLAN Date: Tue, 9 Jun 2026 05:33:37 +0100 Subject: [PATCH] feat: add secret redaction --- src/agent_rules_kit/redaction.py | 58 ++++++++++++++++++++++++++++++ tests/test_redaction.py | 61 ++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/agent_rules_kit/redaction.py create mode 100644 tests/test_redaction.py diff --git a/src/agent_rules_kit/redaction.py b/src/agent_rules_kit/redaction.py new file mode 100644 index 0000000..d993360 --- /dev/null +++ b/src/agent_rules_kit/redaction.py @@ -0,0 +1,58 @@ +"""Redaction helpers for secret-like values.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from re import Pattern + +REDACTION_TEXT = "[REDACTED]" + + +@dataclass(frozen=True, slots=True) +class RedactionPattern: + """A named pattern used to redact secret-like values.""" + + name: str + pattern: Pattern[str] + + +SECRET_LIKE_PATTERNS: tuple[RedactionPattern, ...] = ( + RedactionPattern( + name="openai_api_key", + pattern=re.compile(r"sk-[A-Za-z0-9_-]{12,}"), + ), + RedactionPattern( + name="github_token", + pattern=re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}"), + ), + RedactionPattern( + name="aws_access_key", + pattern=re.compile(r"AKIA[0-9A-Z]{16}"), + ), + RedactionPattern( + name="private_key_block", + pattern=re.compile( + r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", + re.DOTALL, + ), + ), +) + + +def redact_secret_like_values(text: str) -> str: + """Redact supported secret-like values from text.""" + redacted = text + + for item in SECRET_LIKE_PATTERNS: + redacted = item.pattern.sub(REDACTION_TEXT, redacted) + + return redacted + + +__all__ = [ + "REDACTION_TEXT", + "RedactionPattern", + "SECRET_LIKE_PATTERNS", + "redact_secret_like_values", +] diff --git a/tests/test_redaction.py b/tests/test_redaction.py new file mode 100644 index 0000000..878c830 --- /dev/null +++ b/tests/test_redaction.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import unittest + +from agent_rules_kit.redaction import REDACTION_TEXT, redact_secret_like_values + + +class RedactionTests(unittest.TestCase): + def test_safe_text_is_unchanged(self) -> None: + text = "Use local checks before opening a pull request." + + self.assertEqual(redact_secret_like_values(text), text) + + def test_redacts_openai_like_key(self) -> None: + secret = "sk-" + ("A" * 24) + + redacted = redact_secret_like_values(f"token={secret}") + + self.assertEqual(redacted, f"token={REDACTION_TEXT}") + self.assertNotIn(secret, redacted) + + def test_redacts_github_like_token(self) -> None: + secret = "ghp_" + ("B" * 36) + + redacted = redact_secret_like_values(f"github={secret}") + + self.assertEqual(redacted, f"github={REDACTION_TEXT}") + self.assertNotIn(secret, redacted) + + def test_redacts_aws_like_access_key(self) -> None: + secret = "AKIA" + ("C" * 16) + + redacted = redact_secret_like_values(f"aws={secret}") + + self.assertEqual(redacted, f"aws={REDACTION_TEXT}") + self.assertNotIn(secret, redacted) + + def test_redacts_private_key_block(self) -> None: + header = "-----BEGIN " + "PRIVATE KEY" + "-----" + footer = "-----END " + "PRIVATE KEY" + "-----" + secret = f"{header}\nabc123\n{footer}" + + redacted = redact_secret_like_values(f"key:\n{secret}") + + self.assertEqual(redacted, f"key:\n{REDACTION_TEXT}") + self.assertNotIn("abc123", redacted) + + def test_redacts_multiple_secret_like_values(self) -> None: + openai_like = "sk-" + ("D" * 24) + github_like = "gho_" + ("E" * 36) + text = f"first={openai_like}\nsecond={github_like}" + + redacted = redact_secret_like_values(text) + + self.assertEqual(redacted.count(REDACTION_TEXT), 2) + self.assertNotIn(openai_like, redacted) + self.assertNotIn(github_like, redacted) + + +if __name__ == "__main__": + unittest.main()