Files
openswarm/backend/common/secret_scan.py
T
2026-08-24 11:38:26 -07:00

51 lines
2.1 KiB
Python

"""Shared secret-shape scanner: spot credential-shaped literals in text/files.
Lives in backend.common so the .swarm importer, the skills registry, and the
settings redactor all pull it DOWN from one place instead of one feature app
reaching sideways into another. It catches a secret by its SHAPE (sk-ant-...,
ghp_..., AIza...), which is the fail-safe behind name-based redaction: a key
that's misnamed (so a name rule misses it) still gets caught by its shape."""
from __future__ import annotations
import re
REDACTED = "[redacted]"
# Literal-secret shapes someone might paste into a file, skill body, or setting.
SECRET_SHAPE_PATTERNS = (
re.compile(r"sk-ant-[A-Za-z0-9_\-]{16,}"),
# A real sk- key carries a long DASHLESS run; dashes were counted as key material, so the SPDX
# licence id "Asterisk-linking-protocols-exception" read as an OpenAI key and hard-blocked a
# skill export with advice ("remove the secret") that no user could act on. Requiring the run
# keeps every real shape (sk-proj-<48>, sk-ant-api03-<95>) and drops dictionary-words-with-dashes.
re.compile(r"sk-[A-Za-z0-9_\-]*[A-Za-z0-9_]{20,}"),
re.compile(r"AIza[A-Za-z0-9_\-]{20,}"), # Google API key shape
re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}"), # GitHub tokens
re.compile(r"Bearer\s+[A-Za-z0-9._\-]{16,}"),
)
def looks_secret(text: str) -> bool:
"""True if `text` contains a credential-shaped literal."""
return any(p.search(text) for p in SECRET_SHAPE_PATTERNS)
def redact_secret_shapes(text: str) -> str:
"""Replace every secret-shaped literal in `text` with the redacted marker."""
for p in SECRET_SHAPE_PATTERNS:
text = p.sub(REDACTED, text)
return text
def find_secrets_in_files(files: dict[str, bytes]) -> list[str]:
"""Paths of any file whose text body holds a secret-shaped literal. Binary
files (a null byte in the first 4KB) are skipped, they aren't pasted text."""
hits: list[str] = []
for path, data in files.items():
if b"\x00" in data[:4096]:
continue
if looks_secret(data.decode("utf-8", errors="ignore")):
hits.append(path)
return hits