Read the script
#!/usr/bin/env python3
"""
Hook PreToolUse para Bash: guardia de comandos peligrosos.
Intercepta las ejecuciones de Bash y analiza el comando en busca de patrones
potencialmente destructivos (borrado catastrofico, force push, destruccion de
datos, fork bombs, etc.). Si detecta un patron peligroso, bloquea la operacion
(exit 2) con un aviso explicativo.
Politica de seguridad: fail-closed. Si no se puede parsear la entrada del hook
o se produce cualquier error inesperado, se bloquea la operacion (exit 2).
Un hook de seguridad que permite la operacion cuando falla equivale a
desactivar la proteccion justo cuando mas se necesita. Esta politica es
coherente con secret-guard.sh, que tambien es fail-closed.
Patrones vigilados:
- rm -rf / (o ~, o $HOME): borrado catastrofico del sistema o del home.
- git push --force a main/master: perdida de historial en ramas protegidas.
- DROP DATABASE / DROP TABLE: destruccion de datos en base de datos.
- docker system prune -af: eliminacion de volumenes y datos de contenedores.
- chmod -R 777: permisos inseguros en todo un arbol de directorios.
- fork bombs: denegacion de servicio local.
- mkfs / dd sobre dispositivos: destruccion de disco.
- Escritura directa a dispositivos de bloque.
"""
import json
import os
import re
import shlex
import sys
# --- Patrones peligrosos ---------------------------------------------------
# Cada tupla contiene (patron_compilado, descripcion_del_riesgo).
# Los patrones se evaluan en orden; la primera coincidencia bloquea.
_DANGEROUS_PATTERNS = [
# Borrado catastrofico: rm -rf aplicado a raiz, home o rutas de sistema.
# Cubre: flags juntas (-rf, -fr), separadas (-r -f), con sudo, y flags largas.
(
re.compile(
r"(?:sudo\s+)?rm\s+"
r"(?:-[a-zA-Z]*\s+)*"
r"(?:--\w[\w-]*\s+)*"
r"(?=-[a-zA-Z]*r)(?=.*-[a-zA-Z]*f)"
r".*\s+(/\s|/\*|/$|~\s|~$|~\/|\$HOME|\$\{HOME\}|/usr|/etc|/var|/boot|/System)"
),
"Borrado catastrofico: rm -rf sobre directorio raiz o de sistema",
),
# Force push a ramas protegidas
(
re.compile(
r"git\s+push\s+.*"
r"(--force\b|-f\b)"
r".*\b(main|master)\b"
),
"Force push a rama protegida (main/master): riesgo de perdida de historial",
),
# Variante: force push sin rama explicita (se asume rama actual)
(
re.compile(
r"git\s+push\s+--force-with-lease\s*$"
r"|git\s+push\s+-f\s*$"
r"|git\s+push\s+--force\s*$"
),
"Force push sin rama explicita: verifica que no estas en main/master",
),
# Destruccion de base de datos
(
re.compile(r"DROP\s+(DATABASE|TABLE|SCHEMA)\s", re.IGNORECASE),
"Destruccion de datos: DROP DATABASE/TABLE/SCHEMA",
),
# Docker prune agresivo (cubre -af, -fa, -a -f, -f -a y combinaciones con otros flags)
(
re.compile(
r"docker\s+system\s+prune\s+.*(-af|-fa)\b"
r"|docker\s+system\s+prune\s+.*-a\b.*-f\b"
r"|docker\s+system\s+prune\s+.*-f\b.*-a\b"
),
"Docker system prune con -af: elimina todos los datos de contenedores",
),
# Permisos inseguros
(
re.compile(r"chmod\s+(-R\s+)?777\s+/"),
"Permisos inseguros: chmod 777 recursivo sobre directorio raiz",
),
# Fork bomb (variantes comunes en bash)
(
re.compile(r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:"),
"Fork bomb: denegacion de servicio local",
),
# Formateo de disco
(
re.compile(r"mkfs\.\w+\s+/dev/"),
"Formateo de disco: mkfs sobre dispositivo de bloque",
),
# dd sobre dispositivo de bloque
(
re.compile(r"dd\s+.*of=/dev/(sd|hd|nvme|vd|xvd)"),
"Escritura directa a dispositivo de bloque con dd",
),
# Escritura a dispositivo via redireccion
(
re.compile(r">\s*/dev/(sd|hd|nvme|vd|xvd)"),
"Redireccion de salida a dispositivo de bloque",
),
# git reset --hard a remote (destructivo en combinacion con push)
(
re.compile(r"git\s+reset\s+--hard\s+origin/(main|master)"),
"git reset --hard a origin/main: descarta todos los cambios locales",
),
]
_SAFE_ALFRED_HELPER_SUBCOMMANDS = frozenset({
"allow-stop-once",
"blocked",
"consume-prefetch",
"discuss",
"in-progress",
"map-codebase",
"memory-ui",
"next",
"pause",
"progress",
"quick",
"resume",
"search",
"standup",
"validate",
"verify",
})
_SHELL_CONTROL_TOKENS = frozenset({
";",
"&&",
"||",
"|",
">",
">>",
"<",
"<<",
"2>",
"&>",
"&>>",
})
_SHELL_CONTROL_SUBSTRINGS = (
"$(",
"`",
)
_SAFE_CAPTURE_SUFFIXES = (
"2>&1",
"2>/dev/null",
"2>/dev/null 2>&1",
)
_DROP_SQL_REGEX = re.compile(r"\bDROP\s+(DATABASE|TABLE|SCHEMA)\b", re.IGNORECASE)
_DEVICE_REDIRECT_REGEX = re.compile(r">\s*/dev/(sd|hd|nvme|vd|xvd)\w*", re.IGNORECASE)
_SHELL_WRAPPERS = frozenset({"sh", "bash", "zsh"})
_SQL_CLIENTS = frozenset({"psql", "mysql", "mariadb", "sqlite3", "duckdb"})
def _command_basename(token: str) -> str:
return os.path.basename(token or "")
def _strip_leading_wrappers(tokens):
"""Elimina wrappers neutros al principio: sudo, env y variables inline."""
idx = 0
while idx < len(tokens):
token = tokens[idx]
base = _command_basename(token)
if base == "sudo":
idx += 1
while idx < len(tokens) and tokens[idx].startswith("-"):
idx += 1
continue
if base == "env":
idx += 1
while idx < len(tokens) and "=" in tokens[idx] and not tokens[idx].startswith("-"):
idx += 1
continue
if "=" in token and not token.startswith("-"):
name, _value = token.split("=", 1)
if name.replace("_", "").isalnum():