ci / lint-test-leaks (pull_request) Canceled after 0s
Nueva regla del operador (2026-10-31): los ficheros de secretos solo se inspeccionan con herramientas que no muestran valores (jq keys, grep -c, stat). Prohibido cat/grep -r sobre .env y volcar errores con DSN. - tests/test_no_credential_patterns.py: 3 tests — DSN con credencial literal fuera de placeholders, nsec bech32 versionado, tokens hex de 64 chars fuera de tests/. - scripts/check-credential-leaks.sh: hook que falla si journald reciente contiene DSN con contraseña o nsec (redactando el valor en la salida). Sin timer propio (no autorizado); ejecutar manual o desde cron del operador. Contexto: la rotación del rol hardcap y el DSN filtrado a journald por el test del gate (corregido; la credencial filtrada ya está rotada).
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Regla permanente (2026-10-31): ningún fichero versionado contiene patrones
|
|
de credenciales. Placeholders de .env.example están permitidos.
|
|
|
|
Fallará si alguien mete un DSN con contraseña real, un token en claro, o
|
|
cualquier valor que deba vivir solo en .env / tokens.json.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
SCAN_DIRS = ["runtime", "deploy", "docs", "scripts", "prompts"]
|
|
SCAN_GLOBS = ["*.py", "*.mjs", "*.js", "*.ts", "*.md", "*.yml", "*.yaml", "*.sh", "*.sql"]
|
|
|
|
# DSN con credencial literal (user:pass@host) — excluye placeholders conocidos
|
|
DSN = re.compile(r"(postgres|postgresql|mysql|mongodb)(\+[a-z]+)?://[A-Za-z0-9_]+:(?!password\b|change-me\b|<[^>]+>)[^@\s/]{8,}@", re.I)
|
|
# tokens hex largos fuera de tests (los fixtures de tests están allowlistados por diseño)
|
|
HEXTOKEN = re.compile(r"\b[0-9a-f]{64}\b")
|
|
# nsec bech32
|
|
NSEC = re.compile(r"nsec1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,}")
|
|
ALLOW_SUBSTR = ("user:password", "change-me", "<", "EXAMPLE", "example", "itest-pg", "0011", "11*", "22*", "33*", "44*")
|
|
|
|
|
|
def _iter_files():
|
|
for d in SCAN_DIRS:
|
|
base = REPO / d
|
|
if not base.exists():
|
|
continue
|
|
for pat in SCAN_GLOBS:
|
|
for f in base.rglob(pat):
|
|
if ".git" in f.parts or "node_modules" in f.parts:
|
|
continue
|
|
yield f
|
|
|
|
|
|
def test_no_dsn_with_credentials():
|
|
hits = []
|
|
for f in _iter_files():
|
|
for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1):
|
|
if DSN.search(line) and not any(a in line for a in ALLOW_SUBSTR):
|
|
hits.append(f"{f}:{i}")
|
|
assert not hits, f"DSN con credencial literal en: {hits}"
|
|
|
|
|
|
def test_no_nsec_in_versioned_files():
|
|
hits = []
|
|
for f in _iter_files():
|
|
if "/tests/" in str(f):
|
|
continue
|
|
for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1):
|
|
if NSEC.search(line):
|
|
hits.append(f"{f}:{i}")
|
|
assert not hits, f"nsec en fichero versionado: {hits}"
|
|
|
|
|
|
def test_no_raw_hex_tokens_outside_tests():
|
|
"""Tokens hex de 64 chars fuera de tests/ y del conftest (las claves de
|
|
test viven solo en tests/). El outbox no debe contener secretos."""
|
|
hits = []
|
|
for f in _iter_files():
|
|
if "/tests/" in str(f) or f.name == "cayce-identity.md":
|
|
continue
|
|
for i, line in enumerate(f.read_text(errors="ignore").splitlines(), 1):
|
|
if HEXTOKEN.search(line):
|
|
hits.append(f"{f}:{i}")
|
|
assert not hits, f"posible token hex versionado: {hits}"
|