← Volver al blog

Programación y scripting para ciberseguridad: parte 4 – flujos de trabajo de monitoreo multiplataforma

Publicado el
7 min de lectura
--- vistas

Introducción

La seguridad operativa rara vez vive en un único sistema operativo. En la parte 4 extendemos nuestro kit de herramientas de automatización más allá de Linux y diseñamos un flujo de trabajo de monitoreo que corre en ambas plataformas dentro de una red empresarial. Crearás un script de Python para parsear logs de acceso de Linux y un script de PowerShell para interrogar eventos de seguridad de Windows, y luego reflexionarás sobre los principios de diseño compartidos que hacen confiables a ambas soluciones.

Objetivos de aprendizaje

  • Construir un parser de logs en Python que filtre entradas específicas, produzca un reporte estructurado y maneje con elegancia escenarios de archivos faltantes.
  • Desarrollar una automatización en PowerShell que recupere eventos de seguridad recientes, los exporte a CSV y valide el éxito antes de salir.
  • Comparar construcciones de control (if, bucles, try/except, try/catch) entre lenguajes para asegurar un manejo de errores consistente.
  • Aplicar buenas prácticas de portabilidad y seguridad para que los scripts puedan desplegarse con confianza en entornos mixtos.

Escenario: monitoreo entre Linux y Windows en ACME

ACME, nuestra empresa de referencia, ahora opera una flota híbrida de servidores Linux y estaciones de trabajo Windows. El equipo de automatización quiere herramientas ligeras que revelen comportamiento sospechoso sin esperar el despliegue de un SIEM completo. Hay dos tareas sobre la mesa:

  1. Linux – Identificar cada ocurrencia de sudo en un log de acceso y almacenar la evidencia junto con un conteo total en un reporte.
  2. Windows – Obtener las últimas 50 entradas del log de eventos de Security, exportarlas y confirmar que el artefacto exista.

Los scripts a continuación sirven como primera iteración y están listos para revisión de calidad y despliegue.

Módulo Python — log_monitor.py

Resumen de requisitos

  • Leer accesos.log (o autodetectar un valor por defecto razonable como /var/log/auth.log).
  • Capturar cada línea que contenga la cadena literal sudo.
  • Escribir los resultados en reporte_sudo.txt, terminando con un total, y admitir opciones de línea de comandos para mayor flexibilidad.

Implementación

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from typing import Iterable, Iterator

DEFAULT_LOG_CANDIDATES: tuple[str, ...] = (
    "accesos.log",
    "access.log",
    "/var/log/apache2/access.log",
    "/var/log/httpd/access_log",
    "/var/log/nginx/access.log",
)

DEFAULT_TARGET = "sudo"
DEFAULT_OUTPUT = "reporte_sudo.txt"


def parse_args(argv: Iterable[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Filter an access log, find matches, and generate a report.",
    )
    parser.add_argument(
        "--log",
        dest="log_path",
        type=Path,
        default=None,
        help="Path to the log file (auto-detects common locations by default).",
    )
    parser.add_argument(
        "--target",
        dest="target",
        default=DEFAULT_TARGET,
        help="Literal string to search for in each line.",
    )
    parser.add_argument(
        "--out",
        dest="out_path",
        type=Path,
        default=Path(DEFAULT_OUTPUT),
        help="Report file to generate.",
    )
    parser.add_argument(
        "--encoding",
        dest="encoding",
        default="utf-8",
        help="Encoding used for reading and writing (default: utf-8).",
    )
    parser.add_argument(
        "--ignore-case",
        dest="ignore_case",
        action="store_true",
        help="Perform a case-insensitive search.",
    )
    parser.add_argument(
        "--dry-run",
        dest="dry_run",
        action="store_true",
        help="Show statistics without generating the output file.",
    )
    return parser.parse_args(list(argv))


def auto_detect_log_path() -> Path:
    for candidate in DEFAULT_LOG_CANDIDATES:
        path = Path(candidate)
        if path.exists():
            return path
    # Fall back to the first option (keeps behaviour predictable for the assignment)
    return Path(DEFAULT_LOG_CANDIDATES[0])


def read_matching_lines(
    ruta: Path,
    needle: str,
    *,
    encoding: str,
    ignore_case: bool,
) -> Iterator[str]:
    if not ruta.exists():
        raise FileNotFoundError(f"Log file not found: {ruta}")

    comparator = (lambda text: text.lower()) if ignore_case else (lambda text: text)
    target_cmp = comparator(needle)

    with ruta.open("r", encoding=encoding, errors="replace") as fh:
        for raw_line in fh:
            line = raw_line.rstrip("\n")
            if target_cmp in comparator(line):
                yield line


def write_report(
    ruta_out: Path,
    hallazgos: Iterable[str],
    *,
    encoding: str,
    target: str,
) -> int:
    total = 0
    if ruta_out.parent and not ruta_out.parent.exists():
        ruta_out.parent.mkdir(parents=True, exist_ok=True)
    with ruta_out.open("w", encoding=encoding, errors="replace") as fh:
        for line in hallazgos:
            fh.write(line + "\n")
            total += 1
        fh.write(f"\nTotal occurrences of '{target}': {total}\n")
    return total


def main(argv: Iterable[str] | None = None) -> int:
    ns = parse_args(argv or sys.argv[1:])

    log_path = ns.log_path or auto_detect_log_path()
    try:
        matches_iter = read_matching_lines(
            log_path,
            ns.target,
            encoding=ns.encoding,
            ignore_case=ns.ignore_case,
        )
    except FileNotFoundError as exc:
        print(f"[ERROR] {exc}")
        return 1
    except PermissionError as exc:
        print(f"[ERROR] Permission denied reading {log_path}: {exc}")
        return 2
    except OSError as exc:
        print(f"[ERROR] Failed to read {log_path}: {exc}")
        return 3

    if ns.dry_run:
        total = sum(1 for _ in matches_iter)
        print(
            f"[INFO] Matches found: {total} — dry-run mode, no report generated."
        )
        return 0

    try:
        total_written = write_report(
            ns.out_path,
            matches_iter,
            encoding=ns.encoding,
            target=ns.target,
        )
    except OSError as exc:
        print(f"[ERROR] Unable to write report '{ns.out_path}': {exc}")
        return 4

    print(
        f"[OK] Report generated: {ns.out_path} — Occurrences: {total_written} — Source: {log_path}"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Ejecutando el script

python3 log_monitor.py
python3 log_monitor.py --log /var/log/nginx/access.log --ignore-case --dry-run

Salida de ejemplo

log monitor events

Script de PowerShell — eventos_seguridad.ps1

Resumen de requisitos

  • Recuperar las últimas 50 entradas del log de Security de Windows.
  • Exportar a eventos.csv.
  • Verificar que el archivo exista y devolver un código de salida significativo.

Implementación

param(
    [int]$Cantidad = 50,
    [string]$RutaSalida = ".\eventos.csv"
)

try {
    Write-Host "[INFO] Fetching last $Cantidad events from the 'Security' log..."
    $eventos = Get-EventLog -LogName Security -Newest $Cantidad -ErrorAction Stop

    Write-Host "[INFO] Exporting to CSV: $RutaSalida"
    $eventos | Export-Csv -Path $RutaSalida -NoTypeInformation -Encoding UTF8

    if (Test-Path -Path $RutaSalida) {
        Write-Host "[OK] CSV generated successfully: $RutaSalida"
        exit 0
    } else {
        Write-Host "[ERROR] CSV not found after export." -ForegroundColor Red
        exit 1
    }
}
catch {
    Write-Host "[ERROR] Failed to retrieve or export events: $($_.Exception.Message)" -ForegroundColor Red
    exit 2
}

Ejecutando el script

# From an elevated PowerShell prompt
.\eventos_seguridad.ps1
.\eventos_seguridad.ps1 -Cantidad 100 -RutaSalida C:\Logs\eventos.csv

Salida de ejemplo

log monitor events

Análisis comparativo

Estructuras de control

  • Python usa un bucle for para iterar a través de las líneas del log, junto con sentencias if y bloques try/except para el manejo de errores.
  • PowerShell se apoya en comprobaciones if y bloques try/catch para responder a fallos de cmdlets y verificar el archivo exportado.

Disciplina de entrada/salida

  • El script de Python lee del disco usando un contexto administrado (with), preservando la codificación y cerrando los archivos automáticamente.
  • El script de PowerShell se apoya en cmdlets nativos, minimizando el parsing personalizado a la vez que valida el éxito mediante Test-Path.

Consideraciones de portabilidad y seguridad

  • Ambos scripts evitan la escalada de privilegios innecesaria; los operadores deciden cuándo ejecutar con derechos elevados.
  • Las salidas estructuradas (reporte_sudo.txt, eventos.csv) ayudan a la trazabilidad y pueden enviarse a sistemas de logging centralizados.
  • Los parámetros opcionales (--log, --target, -Cantidad, -RutaSalida) hacen que los scripts sean reutilizables entre hosts y entornos.
  • Los mensajes de error claros y los códigos de salida promueven un comportamiento apto para automatización.

Reflexión

Diseñar automatización para múltiples plataformas revela patrones que trascienden los lenguajes: validar las entradas temprano, separar los canales de éxito y error, y producir artefactos que las herramientas posteriores puedan consumir. A medida que extiendas estos scripts—quizás añadiendo hooks de alertas, enriquecimiento o políticas de retención—recuerda revisar los fundamentos cubiertos aquí.

Siguientes pasos

  • Mejorar log_monitor.py con soporte de regex o integración con journalctl en hosts basados en systemd.
  • Añadir validación de parámetros y logging estructurado (ConvertTo-Json) a eventos_seguridad.ps1 para pipelines más ricos.
  • Programar ambos scripts vía cron y el Programador de tareas de Windows, asegurando que los logs roten y los permisos se mantengan restringidos.
  • Alimentar los artefactos generados a un SIEM o un dashboard ligero para rastrear tendencias a lo largo del tiempo.

Estas mejoras mantendrán tus scripts de monitoreo multiplataforma alineados con las necesidades operativas del mundo real.

Disponible para colaboración por contrato

Estoy disponible para colaborar por contrato. Si tiene una idea de proyecto interesante, reserve una llamada por Calendly.

Agenda una llamada de 30 min