mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
Release-readiness hardening. - redaction.py (zero-dep): redact_secrets masks provider API keys, AWS/GitHub/Slack/ Google tokens, bearer tokens, URL-embedded credentials, PEM private keys, and sensitive key=value assignments. Wired into the trace writer and the markdown report (on by default), so scan artifacts never leak a credential quoted from source. - provider/openrouter.py: call_with_retry — exponential backoff on transient failures (HTTP 429/5xx, connection errors, timeouts); non-transient errors fail fast. Both the chat and embedding clients use it (max_attempts/retry_base_delay configurable). - config [hardening]: redact_secrets (default true) + max_findings (bounded-CI output cap, 0 = unlimited; truncation is severity-ordered and disclosed as a `budget` degradation). Agentic spend stays bounded by [harnessx] max_cost_usd/token_threshold. - docs/threat-model.md (trust boundaries, controls, sandbox limits) + docs/examples.md (six end-to-end walkthroughs); README "Security & hardening" section links both. - CI lint/typecheck/test/detection-gate/dagger gates already existed. tests/test_redaction.py, test_provider_retry.py, test_hardening_pipeline.py + config tests. 206 passed, 4 skipped; ruff + mypy clean; detection gate PASS. Co-Authored-By: Claude Opus 4.8 <[email protected]>
46 lines
2.0 KiB
Python
46 lines
2.0 KiB
Python
"""Secret redaction for traces and reports (Phase 15 hardening).
|
|
|
|
Scan artifacts (trace events, reports) can quote source code that contains live
|
|
credentials. This module masks well-known secret shapes before they are persisted, so
|
|
a finding's evidence never leaks a key into an artifact an analyst later shares.
|
|
Conservative by design: it targets recognizable secret formats, not arbitrary text.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
REDACTED = "***REDACTED***"
|
|
|
|
# (compiled pattern, replacement). Replacements keep any non-secret prefix group so the
|
|
# redaction is legible (e.g. `api_key = ***REDACTED***`, `Authorization: Bearer ***REDACTED***`).
|
|
_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
# PEM private key blocks.
|
|
(re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.DOTALL), REDACTED),
|
|
# Authorization: Bearer <token>
|
|
(re.compile(r"(Bearer\s+)[A-Za-z0-9._\-]{12,}"), r"\1" + REDACTED),
|
|
# Provider API keys: OpenAI/Anthropic sk-..., AWS AKIA..., GitHub gh*_..., Slack xox*, Google AIza...
|
|
(re.compile(r"\bsk-[A-Za-z0-9._\-]{16,}"), REDACTED),
|
|
(re.compile(r"\bAKIA[0-9A-Z]{16}\b"), REDACTED),
|
|
(re.compile(r"\bgh[pousr]_[A-Za-z0-9]{20,}"), REDACTED),
|
|
(re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}"), REDACTED),
|
|
(re.compile(r"\bAIza[A-Za-z0-9_\-]{20,}"), REDACTED),
|
|
# Credentials embedded in a URL: scheme://user:secret@host
|
|
(re.compile(r"([a-zA-Z][a-zA-Z0-9+.\-]*://[^\s:/@]+:)[^\s:/@]{3,}(@)"), r"\1" + REDACTED + r"\2"),
|
|
# Generic `key = "value"` assignments for sensitive names.
|
|
(
|
|
re.compile(r"(?i)\b(api[_-]?key|secret|token|password|passwd|pwd|access[_-]?key)\b(\s*[=:]\s*)(['\"]?)[A-Za-z0-9/+_\-]{8,}\3"),
|
|
r"\1\2\3" + REDACTED + r"\3",
|
|
),
|
|
)
|
|
|
|
|
|
def redact_secrets(text: str) -> str:
|
|
"""Return ``text`` with recognizable secrets masked. Idempotent and pattern-scoped."""
|
|
for pattern, replacement in _PATTERNS:
|
|
text = pattern.sub(replacement, text)
|
|
return text
|
|
|
|
|
|
__all__ = ["REDACTED", "redact_secrets"]
|