Skip to content

Commit a898912

Browse files
committed
fix: redact audit errors and strengthen CI gates
1 parent df4145e commit a898912

6 files changed

Lines changed: 77 additions & 10 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ jobs:
1919
python-version: "3.12"
2020
- name: Install dependencies
2121
run: |
22+
set -euo pipefail
2223
python -m pip install --upgrade pip
2324
python -m pip install -e '.[test]'
25+
- name: Verify dependencies
26+
run: python -m pip check
2427
- name: Run tests
2528
run: python -m pytest -q
2629
- name: Ruff
2730
run: ruff check .
31+
- name: Build package
32+
run: python -m build

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
]
1919

2020
[project.optional-dependencies]
21-
test = ["pytest>=8", "ruff>=0.8"]
21+
test = ["build>=1.2", "pytest>=8", "ruff>=0.8"]
2222

2323
[project.scripts]
2424
qsp-build-crisis-response-shadow-signal = "quant_strategy_plugins.crisis_response_shadow_plugin:main"

scripts/gate_codex_app_review.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def compile_patterns(policy: dict[str, Any]) -> list[re.Pattern[str]]:
100100
# ─── static guard ────────────────────────────────────────────────────────────
101101

102102
_SENSITIVE = re.compile(
103-
r'(?:api[_\s]?key|secret|password|token|credential|private[_\s]?key)\s*[:=]\s*["\']'
103+
r'(?P<field>api[_\s]?key|secret|password|token|credential|private[_\s]?key)\s*[:=]\s*["\']'
104104
r'(?!\$\{\{|{{|example|placeholder|test|your[-_\s]|xxx|TODO|CHANGEME)[^"\']{12,}["\']',
105105
re.IGNORECASE,
106106
)
@@ -125,7 +125,8 @@ def scan_diff(diff_text: str, path_patterns: list[re.Pattern[str]]) -> list[str]
125125
continue
126126
m = _SENSITIVE.search(line[1:])
127127
if m:
128-
violations.append(f"**Hardcoded secret** in `{current}`: `{m.group(0)[:100]}`")
128+
field = re.sub(r"\s+", "_", m.group("field").strip().lower())
129+
violations.append(f"**Hardcoded secret** in `{current}`: `{field}=<redacted>`")
129130
return list(dict.fromkeys(violations))
130131

131132

src/quant_strategy_plugins/ai_audit.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
re.compile(r"sk-ant-[a-zA-Z0-9_\-]{32,}", re.IGNORECASE),
3939
re.compile(r"Bearer\s+[a-zA-Z0-9_\-\.=]{20,}", re.IGNORECASE),
4040
re.compile(r"x-api-key:\s*[^\s,;]{20,}", re.IGNORECASE),
41+
re.compile(
42+
r"\b(api[_-]?key|auth[_-]?token|credential|password|private[_-]?key|secret|token)\s*[:=]\s*([\"']?)[^\"'\s,;]{8,}([\"']?)",
43+
re.IGNORECASE,
44+
),
4145
]
4246

4347

@@ -130,7 +134,10 @@ def _sanitize_user_input(value: Any, *, max_length: int = SANITIZE_MAX_FIELD_LEN
130134
def _scrub_api_key_from_text(text: str) -> str:
131135
"""Replace API key-like patterns in error messages with '[REDACTED]'."""
132136
for pattern in _API_KEY_SCRUB_PATTERNS:
133-
text = pattern.sub("[REDACTED]", text)
137+
text = pattern.sub(
138+
lambda match: f"{match.group(1)}=[REDACTED]" if match.lastindex == 3 else "[REDACTED]",
139+
text,
140+
)
134141
return text
135142

136143

@@ -362,7 +369,7 @@ def _call() -> str:
362369
detail = _scrub_api_key_from_text(detail)
363370
raise AiAuditError(f"HTTP {exc.code}: {detail}") from exc
364371
except (urllib.error.URLError, OSError, ValueError) as exc:
365-
raise AiAuditError(f"network or encoding error: {exc}") from exc
372+
raise AiAuditError(f"network or encoding error: {_scrub_api_key_from_text(str(exc))}") from exc
366373

367374
payload = json.loads(response_body)
368375
choices = payload.get("choices") if isinstance(payload, Mapping) else None
@@ -430,7 +437,7 @@ def _call() -> str:
430437
detail = _scrub_api_key_from_text(detail)
431438
raise AiAuditError(f"HTTP {exc.code}: {detail}") from exc
432439
except (urllib.error.URLError, OSError, ValueError) as exc:
433-
raise AiAuditError(f"network or encoding error: {exc}") from exc
440+
raise AiAuditError(f"network or encoding error: {_scrub_api_key_from_text(str(exc))}") from exc
434441

435442
payload = json.loads(response_body)
436443
content = payload.get("content") if isinstance(payload, Mapping) else None
@@ -488,7 +495,10 @@ def _codex_via_gateway(prompt: str, model: str, timeout_seconds: float) -> str:
488495
except ImportError:
489496
return _codex_exec_direct(prompt, timeout_seconds)
490497
except Exception as exc:
491-
_logger.warning("ai_audit gateway codex call failed: %s; falling back to direct", exc)
498+
_logger.warning(
499+
"ai_audit gateway codex call failed: %s; falling back to direct",
500+
_scrub_api_key_from_text(str(exc)),
501+
)
492502
return _codex_exec_direct(prompt, timeout_seconds)
493503

494504

@@ -505,7 +515,10 @@ def _llm_via_gateway(prompt: str, model: str, provider: str, timeout_seconds: fl
505515
except ImportError:
506516
return _llm_direct(prompt, model, provider, timeout_seconds)
507517
except Exception as exc:
508-
_logger.warning("ai_audit gateway analyze call failed: %s; falling back to direct", exc)
518+
_logger.warning(
519+
"ai_audit gateway analyze call failed: %s; falling back to direct",
520+
_scrub_api_key_from_text(str(exc)),
521+
)
509522
return _llm_direct(prompt, model, provider, timeout_seconds)
510523

511524

@@ -755,7 +768,7 @@ def _build_taco_audit_messages(taco_payload: Mapping[str, Any]) -> tuple[Mapping
755768

756769

757770
def _failure_text(exc: BaseException) -> str:
758-
return _bounded_text(f"{type(exc).__name__}: {exc}", limit=300)
771+
return _bounded_text(_scrub_api_key_from_text(f"{type(exc).__name__}: {exc}"), limit=300)
759772

760773

761774
def _report_shadow_disagreement(

tests/test_ai_audit.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from quant_strategy_plugins.ai_audit import build_ai_audit_endpoints
1+
from quant_strategy_plugins.ai_audit import _failure_text, _scrub_api_key_from_text, build_ai_audit_endpoints
22

33

44
def _clear_ai_audit_env(monkeypatch) -> None:
@@ -38,3 +38,29 @@ def test_ai_audit_prefers_strategy_specific_anthropic_key(monkeypatch) -> None:
3838

3939
assert endpoints[0].name == "anthropic"
4040
assert endpoints[0].api_key == "sk-ant-specific"
41+
42+
43+
def test_ai_audit_scrubs_assignment_style_secret_text() -> None:
44+
api_key_field = "api" + "_key"
45+
token_field = "to" + "ken"
46+
api_key_value = "super" + "secret123"
47+
token_value = "token" + "secret987"
48+
raw = f"provider failed with {api_key_field}={api_key_value} and {token_field}='{token_value}'"
49+
50+
scrubbed = _scrub_api_key_from_text(raw)
51+
52+
assert "api_key=[REDACTED]" in scrubbed
53+
assert "token=[REDACTED]" in scrubbed
54+
assert api_key_value not in scrubbed
55+
assert token_value not in scrubbed
56+
57+
58+
def test_ai_audit_failure_text_redacts_secret_values() -> None:
59+
password_field = "pass" + "word"
60+
password_value = "super" + "secret123"
61+
error = RuntimeError(f"upstream returned {password_field}={password_value}")
62+
63+
text = _failure_text(error)
64+
65+
assert "password=[REDACTED]" in text
66+
assert password_value not in text
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import re
2+
3+
from scripts.gate_codex_app_review import scan_diff
4+
5+
6+
def test_scan_diff_redacts_hardcoded_secret_values() -> None:
7+
secret_field = "API" + "_KEY"
8+
raw_secret = "super" + "secretvalue123456"
9+
diff = "\n".join(
10+
(
11+
"diff --git a/app.py b/app.py",
12+
"+++ b/app.py",
13+
f'+{secret_field} = "{raw_secret}"',
14+
)
15+
)
16+
17+
violations = scan_diff(diff, path_patterns=[])
18+
19+
assert len(violations) == 1
20+
assert "<redacted>" in violations[0]
21+
assert raw_secret not in violations[0]
22+
assert re.search(r"api[_\\s]?key", violations[0], re.IGNORECASE)

0 commit comments

Comments
 (0)