Operational security rarely lives on a single operating system. In Part 4 we extend our automation toolkit beyond Linux and design a monitoring workflow that runs on both platforms in an enterprise network. You will create a Python script to parse Linux access logs and a PowerShell script to interrogate Windows security events, then reflect on the shared design principles that make both solutions reliable.
Learning Objectives
Build a Python log parser that filters targeted entries, produces a structured report, and handles missing file scenarios gracefully.
Develop a PowerShell automation that retrieves recent security events, exports them to CSV, and validates success before exiting.
Compare control constructs (if, loops, try/except, try/catch) across languages to ensure consistent error handling.
Apply portability and security best practices so scripts can be deployed confidently in mixed environments.
Scenario: Monitoring Across Linux and Windows at ACME
ACME, our reference company, now operates a hybrid fleet of Linux servers and Windows workstations. The automation team wants lightweight tools that surface suspicious behaviour without waiting for a full SIEM deployment. Two tasks are on the table:
Linux – Identify every occurrence of sudo in an access log and store the evidence plus a total count in a report.
Windows – Pull the latest 50 entries from the Security event log, export them, and confirm the artefact exists.
The scripts below serve as the first iteration and are ready for quality review and deployment.
Python Module — log_monitor.py
Requirements Recap
Read accesos.log (or auto-detect a sensible default like /var/log/auth.log).
Capture each line containing the literal string sudo.
Write results to reporte_sudo.txt, ending with a total, and support command-line options for flexibility.
Implementation
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())
Retrieve the latest 50 entries from the Windows Security log.
Export to eventos.csv.
Verify the file exists and return a meaningful exit code.
Implementation
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
}
Running the Script
# From an elevated PowerShell prompt.\eventos_seguridad.ps1
.\eventos_seguridad.ps1 -Cantidad 100 -RutaSalida C:\Logs\eventos.csv
Sample Output
Comparative Analysis
Control Structures
Python uses a for loop to iterate through log lines, coupled with if statements and try/except blocks for error handling.
PowerShell relies on if checks and try/catch blocks to respond to cmdlet failures and verify the exported file.
Input/Output Discipline
The Python script reads from disk using managed context (with), preserving encoding and automatically closing files.
The PowerShell script leans on native cmdlets, minimizing custom parsing while still validating success through Test-Path.
Portability and Security Considerations
Both scripts avoid unnecessary privilege escalation; operators decide when to run with elevated rights.
Structured outputs (reporte_sudo.txt, eventos.csv) aid traceability and can be shipped to central logging systems.
Optional parameters (--log, --target, -Cantidad, -RutaSalida) make the scripts reusable across hosts and environments.
Clear error messages and exit codes promote automation-friendly behaviour.
Reflection
Designing automation for multiple platforms reveals patterns that transcend languages: validate inputs early, separate success and error channels, and produce artefacts that downstream tools can consume. As you extend these scripts—perhaps adding alerting hooks, enrichment, or retention policies—remember to revisit the foundations covered here.
Next Steps
Enhance log_monitor.py with regex support or integration with journalctl on systemd-based hosts.
Add parameter validation and structured logging (ConvertTo-Json) to eventos_seguridad.ps1 for richer pipelines.
Schedule both scripts via cron and Windows Task Scheduler, ensuring logs rotate and permissions stay restricted.
Feed the generated artefacts into a SIEM or lightweight dashboard to track trends over time.
These improvements will keep your cross-platform monitoring scripts aligned with real-world operational needs.