diff --git a/apps/api/src/utils/__init__.py b/apps/api/src/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/utils/secret_redactor.py b/apps/api/src/utils/secret_redactor.py new file mode 100644 index 00000000..2d2dc7c7 --- /dev/null +++ b/apps/api/src/utils/secret_redactor.py @@ -0,0 +1,31 @@ +# awoooi utils: generic secret redactor | 2026-04-20 @ Asia/Taipei +"""把字串/dict/list 內 secret pattern 遮罩為 。 +符合 feedback_secrets_leak_incidents_2026-04-18.md — 任何進 PG/TG/log 前都該過這層。""" +from __future__ import annotations +import re +from typing import Any + +# 順序:specific 在前,generic 在後(sk-or-v1 / sk-ant 排 sk- 之前) +_PATTERNS: list[tuple[re.Pattern[str], str]] = [ + (re.compile(r"sk-or-v1-[A-Za-z0-9]{36,}"), "openrouter"), + (re.compile(r"sk-ant-api\d{2}-[A-Za-z0-9_\-]{12,}"), "anthropic"), + (re.compile(r"sk-[A-Za-z0-9]{40,}"), "openai"), + (re.compile(r"ghp_[A-Za-z0-9]{36}"), "github"), + (re.compile(r"AIza[0-9A-Za-z_\-]{35}"), "google"), + (re.compile(r"\b\d{8,10}:[A-Za-z0-9_\-]{35}\b"), "telegram"), + (re.compile(r"AKIA[0-9A-Z]{16}"), "aws"), +] + + +def redact(obj: Any) -> Any: + """遮罩字串/dict/list 內 secret,其他型別原值回傳。""" + if isinstance(obj, str): + s = obj + for pat, kind in _PATTERNS: + s = pat.sub(f"", s) + return s + if isinstance(obj, dict): + return {k: redact(v) for k, v in obj.items()} + if isinstance(obj, list): + return [redact(x) for x in obj] + return obj diff --git a/apps/api/tests/test_secret_redactor.py b/apps/api/tests/test_secret_redactor.py new file mode 100644 index 00000000..ded8d00a --- /dev/null +++ b/apps/api/tests/test_secret_redactor.py @@ -0,0 +1,37 @@ +# 2026-04-20 @ Asia/Taipei +from apps.api.src.utils.secret_redactor import redact + + +def test_openrouter_key_redacted(): + assert "" in redact("sk-or-v1-abcdef0123456789ABCDEFghijklmnopqrstuv") + + +def test_anthropic_key_redacted(): + assert "" in redact("sk-ant-api03-abcDEF_123-xyz") + + +def test_github_token_redacted(): + assert "" in redact("ghp_abcdef0123456789ABCDEFghijklmnopqrst") + + +def test_google_key_redacted(): + assert "" in redact("AIzaSyABCDEFGHIJKLMNOPQRSTUVWXYZ1234567") + + +def test_telegram_bot_token_redacted(): + assert "" in redact("8474499448:AAFqu_i4-PN4zGFOK5ea8o0Ud56qqEtCMeI") + + +def test_aws_key_redacted(): + assert "" in redact("key=AKIAIOSFODNN7EXAMPLE") + + +def test_clean_passthrough(): + assert redact("normal text here") == "normal text here" + + +def test_nested_dict(): + d = {"a": "ghp_abcdef0123456789ABCDEFghijklmnopqrst", "b": {"c": "AIzaSyABCDEFGHIJKLMNOPQRSTUVWXYZ1234567"}} + out = redact(d) + assert "ghp_abc" not in str(out) + assert "AIzaSy" not in str(out)