Files
windy-git/api/tests/test_secret_guard.py
Kit OC5 1c552e94de secret-guard: Twilio shapes (SID/API key, 32-hex secret assignment) + per-kind warn mode
Windy Text 10-01: a live Twilio auth token sat in bridged-repo tests. Auth tokens are bare
32-hex, so they are matched only when assigned to a name containing token/secret/key/password;
hash is of the value alone. SECRET_GUARD_WARN_KINDS lets a new shape warn before it blocks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 00:59:05 -04:00

149 lines
6.5 KiB
Python

"""Secret guard: shapes, hash-only findings, allow by hash."""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts"))
_spec = importlib.util.spec_from_file_location("secret_guard", ROOT / "scripts" / "secret_guard.py")
sg = importlib.util.module_from_spec(_spec)
sys.modules["secret_guard"] = sg
_spec.loader.exec_module(sg)
ss = sg.ss
# Synthetic shapes only: none of these is a real credential.
TG = "1234567890:" + "A" * 35
CASES = [
("telegram bot token", f"TELEGRAM_BOT_TOKEN={TG}"),
("github token", "token = 'ghp_" + "a1" * 18 + "'"),
("aws access key", "aws_access_key_id = AKIA" + "ABCDEFGHIJKLMNOP"),
("slack token", "xoxb-" + "1234567890-abcdefghij"),
("anthropic key", "ANTHROPIC_API_KEY=sk-ant-" + "x" * 30),
("openai key", "OPENAI_API_KEY=sk-proj-" + "y" * 40),
("stripe live key", "STRIPE=sk_live_" + "z" * 24),
("google api key", "key=AIza" + "B" * 35),
("private key block", "-----BEGIN OPENSSH PRIVATE KEY-----"),
]
@pytest.mark.parametrize("kind, text", CASES)
def test_each_shape_is_found_and_only_its_hash_is_kept(kind, text):
hits = sg.scan_line("app.py", text)
assert [k for k, _ in hits] == [kind]
match = hits[0][1]
assert match.startswith(f"{kind} #") and len(match.rsplit("#", 1)[1]) == 8
# house rule 10: the value itself must never appear in a finding
secret = text.split("=", 1)[-1].strip(" '")
assert secret not in match
@pytest.mark.parametrize("text", [
"sha512-" + "Q" * 86 + "==", # lockfile integrity
"version: 12345678:abc", # short, not a token
"sk-ant-short", # too short
"re_test_register_sends_verification", # windy-pro's fake Resend key
"ANTHROPIC_API_KEY=", # a name, not a value
])
def test_non_secrets_are_not_flagged(text):
assert sg.scan_line("x", text) == []
def test_anthropic_key_is_not_double_counted_as_openai():
assert [k for k, _ in sg.scan_line("x", "sk-ant-" + "q" * 40)] == ["anthropic key"]
def test_allow_is_by_hash_only():
F = sg.cg.Finding
fake = F("tests/t.py", 3, "telegram bot token", f"telegram bot token #{ss.h8(TG)}")
real = F("tests/t.py", 9, "telegram bot token", "telegram bot token #deadbeef")
allow = {"windy-chat": {"hashes": {ss.h8(TG)}, "paths": []}}
assert sg._drop_allowed("windy-chat", [fake, real], allow) == [real]
assert sg._drop_allowed("windy-mail", [fake], allow) == [fake]
def test_private_key_blocks_are_allowed_by_path_never_by_hash():
F = sg.cg.Finding
hdr = "private key block #" + ss.h8("-----BEGIN PRIVATE KEY-----")
test_key = F("tests/keys/test.pem", 1, "private key block", hdr)
prod_key = F("deploy/prod.pem", 1, "private key block", hdr)
allow = {"r": {"hashes": {hdr.rsplit("#", 1)[1]}, "paths": [("tests/keys/*", {"private key block"})]}}
assert sg._drop_allowed("r", [test_key, prod_key], allow) == [prod_key]
def test_path_allow_cannot_cover_real_token_kinds(tmp_path):
bad = tmp_path / "a.yml"
bad.write_text("allow:\n - repo: r\n paths: [tests/*]\n kinds: [telegram bot token]\n reason: no\n")
with pytest.raises(ValueError):
sg.load_allow(bad)
def test_shipped_allow_file_never_excuses_the_real_leaked_tokens():
a = sg.load_allow()
every = set().union(*(v["hashes"] for v in a.values())) if a else set()
assert not {"1354fc9b", "d49dc2ba"} & every # real (now revoked) credentials: remove, never allow
def test_allow_file_loads_and_needs_reasons(tmp_path):
assert isinstance(sg.load_allow(), dict)
bad = tmp_path / "a.yml"
bad.write_text("allow:\n - repo: r\n hashes: [abcd1234]\n")
with pytest.raises(ValueError):
sg.load_allow(bad)
def test_block_and_warn(monkeypatch):
f = sg.cg.Finding("a.py", 1, "github token", "github token #abcd1234")
monkeypatch.setattr(sg, "MODE", "block")
assert sg.status_for([f], False)[0] == "failure"
assert sg.status_for([], False, grant=[f])[0] == "success"
monkeypatch.setattr(sg, "MODE", "warn")
state, desc, _ = sg.status_for([f], True)
assert state == "success" and desc.startswith("⚠ WARN (not blocking): 1 secret-shaped string in tree")
def test_public_scan_excuses_by_hash_and_by_path():
spec = importlib.util.spec_from_file_location("public_secret_scan", ROOT / "scripts" / "public_secret_scan.py")
ps = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ps)
allow = {"windy-agent": {"hashes": {"aaaa1111"}, "paths": [("tests/keys/*", {"private key block"})]}}
assert ps.excused("windy-agent", "openai key", "aaaa1111", ["tests/x.py"], allow)
assert not ps.excused("windy-agent", "telegram bot token", "1354fc9b", ["tests/test_log_redaction.py"], allow)
assert ps.excused("windy-agent", "private key block", "ffff0000", ["tests/keys/a.pem"], allow)
# a PEM header anywhere outside the allowed paths still counts
assert not ps.excused("windy-agent", "private key block", "ffff0000", ["tests/keys/a.pem", "deploy/k.pem"], allow)
assert not ps.excused("other", "openai key", "aaaa1111", ["x"], allow)
HEX32 = "0123456789abcdef" * 2 # synthetic
def test_twilio_shapes_hash_only_and_no_md5_noise():
kinds = lambda t: [k for k, _ in ss.find(t)] # noqa: E731
assert kinds(f'TWILIO_AUTH_TOKEN = "{HEX32}"') == ["32-hex secret assignment"]
assert kinds(f"auth_token: {HEX32}") == ["32-hex secret assignment"]
assert kinds("AC" + HEX32) == ["twilio sid/api key"]
assert kinds("SK" + HEX32) == ["twilio sid/api key"]
# plain md5 / uuid-without-dashes / a 64-hex sha256 are NOT secrets by shape
assert kinds(f"md5 = {HEX32}") == []
assert kinds(f"checksum_key = {HEX32}{HEX32}") == []
assert kinds(f"name = 'x{HEX32}'") == []
# the hash is of the value alone, so renaming the variable keeps the same allow hash
a = ss.find(f"A_TOKEN={HEX32}")[0][1]
b = ss.find(f"OTHER_SECRET: '{HEX32}'")[0][1]
assert a == b == ss.h8(HEX32)
assert HEX32 not in repr(ss.find(f"A_TOKEN={HEX32}"))
def test_warn_kinds_do_not_block(monkeypatch):
f = sg.cg.Finding("a.py", 1, "32-hex secret assignment", "32-hex secret assignment #abcd1234")
monkeypatch.setattr(sg, "MODE", "block")
monkeypatch.setattr(sg, "WARN_KINDS", {"32-hex secret assignment"})
assert sg.status_for([f], True)[0] == "success"
monkeypatch.setattr(sg, "WARN_KINDS", set())
assert sg.status_for([f], True)[0] == "failure"