This commit is contained in:
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# check-credential-leaks — falla si journald reciente contiene patrones de credencial.
|
||||
# Regla permanente: los secretos jamás en logs. Ejecutar manualmente o desde cron
|
||||
# (sin timer propio: no fue autorizado).
|
||||
set -euo pipefail
|
||||
SINCE="${1:-2 hours ago}"
|
||||
HITS=$(journalctl --since "$SINCE" --no-pager 2>/dev/null | grep -cE "(postgres|postgresql)://[A-Za-z0-9_]+:[^@[:space:]]{8,}@|nsec1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,}" || true)
|
||||
if [ "$HITS" -gt 0 ]; then
|
||||
echo "ALERTA: $HITS líneas de journald (desde $SINCE) contienen patrones de credencial" >&2
|
||||
journalctl --since "$SINCE" --no-pager 2>/dev/null | grep -nE "(postgres|postgresql)://[A-Za-z0-9_]+:[^@[:space:]]{8,}@|nsec1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{50,}" | sed -E "s#(://[A-Za-z0-9_]+:)[^@[:space:]]{8,}@#\1***@#" | head -5
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: sin patrones de credencial en journald desde $SINCE"
|
||||
@@ -0,0 +1,68 @@
|
||||
"""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}"
|
||||
Reference in New Issue
Block a user