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:
Linux – Identificar cada ocurrencia de sudo en un log de acceso y almacenar la evidencia junto con un conteo total en un reporte.
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"defparse_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))defauto_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])defread_matching_lines( ruta: Path, needle:str,*, encoding:str, ignore_case:bool,)-> Iterator[str]:ifnot 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
defwrite_report( ruta_out: Path, hallazgos: Iterable[str],*, encoding:str, target:str,)->int: total =0if ruta_out.parent andnot 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
defmain(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}")return1except PermissionError as exc:print(f"[ERROR] Permission denied reading {log_path}: {exc}")return2except OSError as exc:print(f"[ERROR] Failed to read {log_path}: {exc}")return3if ns.dry_run: total =sum(1for _ in matches_iter)print(f"[INFO] Matches found: {total} — dry-run mode, no report generated.")return0try: 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}")return4print(f"[OK] Report generated: {ns.out_path} — Occurrences: {total_written} — Source: {log_path}")return0if __name__ =="__main__":raise SystemExit(main())
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
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.