Back to sh0
sh0

34 reglas para detectar errores de despliegue antes de que ocurran

Construimos un motor de analisis estatico en Rust puro con 34 reglas en 8 categorias para detectar problemas de seguridad, errores de configuracion y errores de despliegue antes de que lleguen a produccion.

Thales & Claude | March 30, 2026 13 min sh0
EN/ FR/ ES
ruststatic-analysissecuritycode-healthdeploymentdevops

Un build exitoso no es lo mismo que un despliegue exitoso.

Tu imagen Docker se construye limpiamente. El contenedor arranca. Treinta segundos despues se cae porque Django tiene DEBUG = True activado, no hay script de inicio en package.json, una clave API esta codificada en el codigo fuente, o la aplicacion se vincula a 127.0.0.1 en lugar de 0.0.0.0 y es inaccesible desde fuera del contenedor.

Estos no son errores de build. Son errores de despliegue -- del tipo que pasan el CI, sobreviven la revision de codigo y solo aparecen a las 2 de la manana cuando un cliente reporta que tu aplicacion esta caida. Decidimos que sh0 deberia detectarlos antes de que el contenedor siquiera arranque.

El resultado: un motor de analisis estatico en Rust puro con 34 reglas en 8 categorias, construido en el Dia Cero como Fase 6 del maraton de desarrollo de sh0.

El cambio: de "Compila?" a "Funcionara?"

Cuando terminamos el motor de build (Fase 5), sh0 podia detectar el stack de un proyecto, generar un Dockerfile y construir una imagen de contenedor. Eso era necesario pero insuficiente. Un PaaS que construye tu codigo y luego deja que falle en produccion no es gran cosa como plataforma.

La idea fue simple: la mayoria de los fallos de despliegue no son exoticos. Son los mismos 30 o 40 errores, repetidos en millones de proyectos. Un script de inicio ausente. Una direccion localhost codificada. Un secreto expuesto. Un flag de debug dejado activado. Todos son detectables solo desde el codigo fuente, sin ejecutar la aplicacion, sin un LLM, sin ningun servicio externo.

Queriamos que sh0 dijera: "Tu codigo se construyo exitosamente, pero aqui hay 3 cosas que probablemente fallaran en produccion."

Arquitectura: punteros a funciones y un contexto de lectura unica

El motor tiene tres capas: el ScanContext que lee el proyecto, las structs Rule que lo analizan, y el Engine que orquesta todo.

ScanContext: leer una vez, escanear muchas

La decision de rendimiento mas importante fue leer cada archivo exactamente una vez. Cada regla necesita buscar patrones en el contenido de archivos -- claves API, valores de configuracion, declaraciones de import. Dejar que cada regla lea archivos independientemente significaria leer el mismo package.json 34 veces.

rustpub struct ScanContext {
    pub files: HashMap<PathBuf, String>,
    pub package_json: Option<serde_json::Value>,
    pub gitignore_patterns: Vec<String>,
    pub file_count: usize,
}

impl ScanContext {
    pub fn from_directory(path: &Path) -> Result<Self, BuilderError> {
        let mut files = HashMap::new();

        for entry in WalkDir::new(path)
            .into_iter()
            .filter_entry(|e| !is_skippable(e))
        {
            let entry = entry?;
            if !entry.file_type().is_file() {
                continue;
            }
            // Saltar archivos mayores de 1MB
            if entry.metadata()?.len() > 1_048_576 {
                continue;
            }
            if let Ok(content) = std::fs::read_to_string(entry.path()) {
                let rel = entry.path().strip_prefix(path)?;
                files.insert(rel.to_path_buf(), content);
            }
        }

        let package_json = files.get(Path::new("package.json"))
            .and_then(|s| serde_json::from_str(s).ok());

        let gitignore_patterns = files.get(Path::new(".gitignore"))
            .map(|s| s.lines().map(String::from).collect())
            .unwrap_or_default();

        let file_count = files.len();

        Ok(Self { files, package_json, gitignore_patterns, file_count })
    }
}

El recorrido del directorio omite .git, node_modules, target, vendor, __pycache__, .venv, dist, .next y .nuxt -- directorios que pueden contener decenas de miles de archivos pero nunca contienen codigo fuente escrito por el usuario. El limite de 1MB evita que archivos binarios consuman memoria.

El contexto tambien proporciona metodos auxiliares que las reglas usan constantemente:

rustimpl ScanContext {
    pub fn has_file(&self, name: &str) -> bool {
        self.files.contains_key(Path::new(name))
    }

    pub fn read_file(&self, name: &str) -> Option<&str> {
        self.files.get(Path::new(name)).map(|s| s.as_str())
    }

    pub fn grep(&self, pattern: &str) -> Vec<(&Path, usize, &str)> {
        self.files.iter()
            .flat_map(|(path, content)| {
                content.lines().enumerate()
                    .filter(|(_, line)| line.contains(pattern))
                    .map(move |(n, line)| (path.as_path(), n + 1, line))
            })
            .collect()
    }

    pub fn grep_in(&self, file: &str, pattern: &str) -> Vec<(usize, &str)> {
        self.files.get(Path::new(file))
            .map(|content| {
                content.lines().enumerate()
                    .filter(|(_, line)| line.contains(pattern))
                    .map(|(n, line)| (n + 1, line))
                    .collect()
            })
            .unwrap_or_default()
    }

    pub fn is_test_file(&self, path: &Path) -> bool {
        let s = path.to_string_lossy();
        s.contains("test") || s.contains("spec") || s.contains("__tests__")
    }
}

El metodo grep() es el caballo de batalla. Busca en cada archivo del proyecto un patron y devuelve la ruta del archivo, el numero de linea y el contenido de la linea. Las reglas lo usan para producir hallazgos con ubicaciones precisas: "Clave API encontrada en src/config.js en la linea 42."

Reglas: punteros a funciones, no traits

Cada regla es una struct que contiene un puntero a funcion:

rustpub struct Rule {
    pub id: &'static str,
    pub name: &'static str,
    pub category: Category,
    pub severity: Severity,
    pub stacks: Option<Vec<Stack>>,  // None = aplica a todos los stacks
    pub check: fn(&ScanContext) -> Vec<HealthIssue>,
}

Elegimos punteros a funciones sobre objetos trait por dos razones. Primera, simplicidad: definir una nueva regla significa escribir una funcion, no implementar un trait en una nueva struct. Segunda, rendimiento: los punteros a funciones son un unico valor del tamano de un puntero con dispatch estatico, mientras que los objetos trait requieren indireccion de vtable.

El campo stacks habilita el filtrado por stack. Las reglas de Django solo se ejecutan en proyectos Django. Las reglas de Next.js solo se ejecutan en proyectos Next.js. Las reglas genericas como las verificaciones de seguridad se ejecutan en todo.

Las 34 reglas

Reglas de seguridad (SEC001-SEC007)

Las reglas de seguridad son las mas universalmente aplicables. Escanean cada proyecto independientemente del stack.

SEC001 -- Claves API en el codigo fuente:

rustfn check_api_keys(ctx: &ScanContext) -> Vec<HealthIssue> {
    let patterns = [
        "AKIA",                        // Prefijo de clave de acceso AWS
        "sk_live_",                    // Clave secreta live de Stripe
        "sk_test_",                    // Clave secreta de prueba de Stripe
        "ghp_",                        // Token de acceso personal de GitHub
        "gho_",                        // Token de acceso OAuth de GitHub
        "glpat-",                      // Token de acceso personal de GitLab
        "xoxb-",                       // Token de bot de Slack
        "xoxp-",                       // Token de usuario de Slack
    ];

    let mut issues = Vec::new();
    for pattern in &patterns {
        for (path, line, content) in ctx.grep(pattern) {
            if ctx.is_test_file(path) || ctx.is_comment_line(content) {
                continue;
            }
            issues.push(issue(
                "SEC001",
                Severity::Blocking,
                format!("Posible clave API ({}) encontrada en {}:{}", pattern, path.display(), line),
                "Mueve los secretos a variables de entorno y anade el archivo a .gitignore.",
            ));
        }
    }
    issues
}

La regla busca prefijos de tokens bien conocidos. Las claves de acceso de AWS siempre empiezan con AKIA. Las claves secretas de Stripe empiezan con sk_live_ o sk_test_. Los tokens de GitHub empiezan con ghp_. Estos no son heuristicas -- son prefijos estructurales definidos por cada servicio.

Las verificaciones is_test_file e is_comment_line previenen falsos positivos. Un archivo de prueba que incluye "AKIA_FAKE_KEY" como mock no es un problema de seguridad. Un comentario que explica "usa el formato AKIA" no es una clave filtrada.

SEC002 -- Contrasenas en el codigo fuente: ``rust fn check_passwords(ctx: &ScanContext) -> Vec<HealthIssue> { let patterns = [ "password = \"", "password = '", "PASSWORD = \"", "passwd = \"", "secret = \"", ]; // ... escaneo basado en grep similar } ``

SEC006 -- Modo debug activado detecta DEBUG = True en la configuracion de Django, debug: true en archivos de configuracion y patrones similares entre frameworks.

SEC007 -- .env en el repositorio verifica si .env esta siendo rastreado (presente en el arbol de archivos pero no en .gitignore):

rustfn check_env_file(ctx: &ScanContext) -> Vec<HealthIssue> {
    if ctx.has_file(".env") && !ctx.gitignore_patterns.iter().any(|p| p.trim() == ".env") {
        vec![issue(
            "SEC007",
            Severity::Blocking,
            "Archivo .env encontrado en el proyecto sin exclusion en .gitignore",
            "Anade .env a .gitignore. Nunca hagas commit de archivos de entorno.",
        )]
    } else {
        vec![]
    }
}

Reglas de Node.js (NODE001-NODE005)

NODE001 -- Script de inicio ausente es la razon mas comun por la que los despliegues de Node.js fallan:

rustfn check_start_script(ctx: &ScanContext) -> Vec<HealthIssue> {
    if let Some(pkg) = &ctx.package_json {
        let has_start = pkg.get("scripts")
            .and_then(|s| s.get("start"))
            .is_some();

        if !has_start {
            return vec![issue(
                "NODE001",
                Severity::Blocking,
                "No hay script \"start\" en package.json",
                "Anade un script \"start\" (p. ej., \"node dist/index.js\") a package.json.",
            )];
        }
    }
    vec![]
}

Este es un problema bloqueante porque sin un script de inicio, el contenedor arrancara y se detendra inmediatamente. El usuario vera "despliegue fallido" sin ningun mensaje de error util de su aplicacion.

NODE002 -- Puerto codificado detecta app.listen(3000) o server.listen(8080) sin leer de process.env.PORT. En un entorno PaaS, la plataforma asigna el puerto via variable de entorno.

NODE003 -- Dependencias de desarrollo en produccion verifica entradas de devDependencies como nodemon o ts-node en el script start -- herramientas que nunca deberian ejecutarse en produccion.

Reglas de Python (PY001-PY006)

Las reglas de Python se centran en Django y FastAPI, los dos frameworks Python de produccion mas comunes.

PY001 -- Django DEBUG = True: ``rust fn check_django_debug(ctx: &ScanContext) -> Vec<HealthIssue> { for (path, line, content) in ctx.grep("DEBUG") { if path.to_string_lossy().contains("settings") && content.contains("= True") && !ctx.is_comment_line(content) { return vec![issue( "PY001", Severity::Blocking, format!("Django DEBUG = True en {}:{}", path.display(), line), "Configura DEBUG = os.environ.get('DEBUG', 'False') == 'True'", )]; } } vec![] } ``

Django con DEBUG = True en produccion expone trazas de pila completas, consultas de base de datos y detalles de configuracion a cualquier visitante que provoque un error. No es una advertencia -- es un problema bloqueante.

PY002 -- Django SECRET_KEY codificado detecta SECRET_KEY = "..." en archivos de configuracion. Una clave secreta codificada significa que cada despliegue comparte el mismo material criptografico, y cualquiera con acceso al codigo fuente puede falsificar sesiones.

PY005 -- Uvicorn con --reload detecta uvicorn main:app --reload en comandos de inicio de produccion. El flag --reload vigila cambios en archivos y reinicia el servidor -- util en desarrollo, un problema de rendimiento y fiabilidad en produccion.

Reglas de Go, Java, Build y Configuracion

Las categorias restantes siguen el mismo patron:

  • GO001-GO003: Direcciones de escucha codificadas, go.mod ausente, ausencia de manejo de shutdown graceful
  • JAVA001-JAVA004: Consola H2 expuesta, Spring Actuator sin seguridad, perfil de desarrollo activo, flags de memoria JVM ausentes
  • BUILD001-BUILD003: TypeScript configurado pero no compilado, script de build ausente, lockfile ausente
  • CFG001-CFG004: Comando de inicio ausente, puerto codificado, binding a localhost, .dockerignore ausente

El sistema de puntuacion

Despues de que todas las reglas aplicables se han ejecutado, el motor calcula una puntuacion de salud:

rustpub fn compute_score(issues: &[HealthIssue]) -> u8 {
    let blocking = issues.iter().filter(|i| i.severity == Severity::Blocking).count();
    let warnings = issues.iter().filter(|i| i.severity == Severity::Warning).count();
    let info = issues.iter().filter(|i| i.severity == Severity::Info).count();

    let penalty = (blocking * 20) + (warnings * 5) + (info * 1);
    100u8.saturating_sub(penalty as u8)
}

La formula: partir de 100, restar 20 por cada problema bloqueante, 5 por cada advertencia, 1 por cada hallazgo informativo. La puntuacion se limita a 0.

Un proyecto con una clave API codificada (bloqueante, -20) y un .dockerignore ausente (advertencia, -5) obtiene 75. Un proyecto con tres problemas bloqueantes de seguridad obtiene 40. La puntuacion da a los usuarios una sensacion instantanea y cuantificable de su preparacion para el despliegue.

El motor tambien calcula la complejidad del proyecto basandose en el conteo de archivos:

rustpub fn compute_complexity(file_count: usize) -> Complexity {
    let level = if file_count < 20 {
        ComplexityLevel::Simple
    } else if file_count < 100 {
        ComplexityLevel::Medium
    } else {
        ComplexityLevel::Complex
    };

    Complexity { level, file_count }
}

La complejidad es informativa, no punitiva. Ayuda a los usuarios a entender el alcance del escaneo.

Por que Rust puro, sin LLM

Tomamos una decision deliberada de implementar cada regla como coincidencia de patrones determinista, no analisis basado en LLM.

Velocidad. El escaneo completo se ejecuta en milisegundos. Una llamada a un LLM toma segundos, como minimo. Cuando un desarrollador sube codigo, quiere saber inmediatamente si algo esta mal -- no despues de una llamada API de 10 segundos a un modelo de lenguaje.

Determinismo. El mismo codigo produce los mismos hallazgos cada vez. No hay problemas alucinados, ni detecciones omitidas porque el modelo tuvo un dia malo, ni inconsistencias "funciona en GPT-4o pero no en GPT-4o-mini". Si SEC001 se activa, hay una cadena que coincide con AKIA en la linea 42 de tu archivo fuente. Punto.

Operacion offline. sh0 es una plataforma auto-hospedada. Podria ejecutarse en un servidor sin acceso a internet, detras de un firewall corporativo, o en una red aislada. Una dependencia de un servicio LLM externo romperia estos despliegues.

Coste. Cada llamada a un LLM cuesta dinero. Los usuarios de sh0 podrian subir codigo docenas de veces al dia. Cobrar por los health checks (o asumir el coste nosotros) seria insostenible.

La contrapartida es que la coincidencia de patrones pura no puede detectar problemas semanticos. No puede decirte que tu logica de autenticacion tiene un bug de time-of-check-to-time-of-use, o que tus consultas SQL son vulnerables a inyeccion. Pero puede detectar los 34 errores de despliegue mas comunes, y eso cubre la gran mayoria de los problemas que los usuarios reales encuentran en produccion.

Integracion: no bloqueante por defecto

El motor de health checks se integra en el pipeline de build como un paso informativo:

rustimpl Builder {
    pub async fn build(&self, opts: BuildOpts) -> Result<BuildOutput, BuilderError> {
        let detected = detect(&opts.source_path);

        // El health check se ejecuta despues de la deteccion, antes del build
        let health = check_health(&opts.source_path, &detected).await?;

        // El build continua independientemente de la puntuacion de salud
        let dockerfile = generate_dockerfile(&detected);
        let image_id = self.docker.build_image(/* ... */).await?;

        Ok(BuildOutput {
            image_id,
            stack: detected,
            health_report: Some(health),
            // ...
        })
    }
}

El informe de salud se adjunta a la salida del build pero no bloquea el build. Un proyecto con puntuacion 40 aun se despliega. Los hallazgos se presentan al usuario en el dashboard, dejandole decidir que corregir y cuando.

Tambien hay una funcion check() independiente para ejecutar health checks sin construir:

rustpub async fn check(path: &Path) -> Result<HealthReport, BuilderError> {
    let detected = detect(path);
    check_health(path, &detected).await
}

Esto alimenta el comando sh0 check del CLI, que los desarrolladores pueden ejecutar localmente antes de subir codigo.

Verificacion: 82 pruebas

El motor de health checks anadio 59 nuevas pruebas sobre las 23 existentes del motor de build, llevando el crate sh0-builder a 82 pruebas en total. Cada regla tiene al menos una prueba positiva (el patron esta presente, la regla se activa) y una prueba negativa (el patron esta ausente, la regla no se activa). Los casos extremos como patrones en archivos de prueba, patrones en comentarios y coincidencias parciales estan cubiertos.

cargo test -p sh0-builder     82 pruebas pasaron
cargo clippy -p sh0-builder   0 advertencias

Lo que vino despues

Con 34 reglas cubriendo seguridad, configuracion, problemas especificos de frameworks e higiene de build, el motor de health checks de sh0 da a los usuarios retroalimentacion accionable antes de que su codigo llegue a produccion. No es un reemplazo para una auditoria de seguridad o una revision exhaustiva del codigo. Es una red de seguridad rapida y determinista que atrapa los errores obvios -- los que representan el 80% de los tickets de soporte "por que esta roto mi despliegue?" en cada plataforma PaaS.

Las siguientes fases del desarrollo de sh0 pasaron del analisis estatico a la infraestructura en tiempo de ejecucion: el reverse proxy con SSL automatico (Fase 7), el pipeline de despliegue completo (Fase 8) y el sistema de autenticacion (Fase 9). Esas son historias para los proximos articulos de esta serie.


Esta es la Parte 4 de la serie "Como construimos sh0.dev".

Navegacion de la serie: - [1] Dia Cero: 10 crates Rust en 24 horas - [2] Escribir un cliente Docker Engine desde cero en Rust - [3] Deteccion automatica de 19 stacks tecnologicos desde el codigo fuente - [4] 34 reglas para detectar errores de despliegue antes de que ocurran (estas aqui)

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles