Programming and Scripting for Cybersecurity: Part 4 – Cross-Platform Monitoring Workflows

Published on
6 mins read
--- views

Introduction

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:

  1. Linux – Identify every occurrence of sudo in an access log and store the evidence plus a total count in a report.
  2. 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"


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())

Running the Script

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

Sample Output

log monitor events

PowerShell Script — eventos_seguridad.ps1

Requirements Recap

  • 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

log monitor events

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.