← Назад в блог

Программирование и скриптинг для кибербезопасности: часть 4 — кросс-платформенные воркфлоу мониторинга

Опубликовано
6 мин чтения
--- просмотров

Введение

Операционная безопасность редко ограничивается одной операционной системой. В части 4 мы расширяем наш инструментарий автоматизации за пределы Linux и проектируем воркфлоу мониторинга, который работает на обеих платформах в корпоративной сети. Ты создашь Python-скрипт для разбора логов доступа Linux и PowerShell-скрипт для опроса событий безопасности Windows, а затем осмыслишь общие принципы проектирования, которые делают оба решения надёжными.

Цели обучения

  • Собирать Python-парсер логов, который фильтрует нужные записи, формирует структурированный отчёт и корректно обрабатывает сценарии отсутствующего файла.
  • Разрабатывать PowerShell-автоматизацию, которая получает последние события безопасности, экспортирует их в CSV и проверяет успех перед завершением.
  • Сравнивать управляющие конструкции (if, циклы, try/except, try/catch) между языками, чтобы обеспечить единообразную обработку ошибок.
  • Применять лучшие практики портируемости и безопасности, чтобы уверенно деплоить скрипты в смешанных окружениях.

Сценарий: мониторинг Linux и Windows в ACME

ACME, наша компания-эталон, теперь управляет гибридным парком из Linux-серверов и Windows-рабочих станций. Команда автоматизации хочет лёгкие инструменты, которые выявляют подозрительное поведение, не дожидаясь полноценного развёртывания SIEM. На столе две задачи:

  1. Linux — находить каждое вхождение sudo в логе доступа и сохранять доказательства плюс общее количество в отчёте.
  2. Windows — получать последние 50 записей из журнала событий Security, экспортировать их и подтверждать существование артефакта.

Скрипты ниже служат первой итерацией и готовы к проверке качества и деплою.

Python-модуль — log_monitor.py

Резюме требований

  • Читать accesos.log (или автоматически определять разумный вариант по умолчанию вроде /var/log/auth.log).
  • Захватывать каждую строку, содержащую буквальную строку sudo.
  • Записывать результаты в reporte_sudo.txt, заканчивая итоговым значением, и поддерживать опции командной строки для гибкости.

Реализация

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

Запуск скрипта

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

Пример вывода

события log monitor

PowerShell-скрипт — eventos_seguridad.ps1

Резюме требований

  • Получать последние 50 записей из журнала Windows Security.
  • Экспортировать в eventos.csv.
  • Проверять существование файла и возвращать осмысленный код завершения.

Реализация

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
}

Запуск скрипта

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

Пример вывода

события log monitor

Сравнительный анализ

Управляющие конструкции

  • Python использует цикл for для перебора строк лога вместе с конструкциями if и блоками try/except для обработки ошибок.
  • PowerShell полагается на проверки if и блоки try/catch, чтобы реагировать на сбои cmdlet'ов и проверять экспортированный файл.

Дисциплина ввода-вывода

  • Python-скрипт читает с диска через управляемый контекст (with), сохраняя кодировку и автоматически закрывая файлы.
  • PowerShell-скрипт опирается на нативные cmdlet'ы, минимизируя кастомный парсинг, при этом всё равно проверяя успех через Test-Path.

Соображения по портируемости и безопасности

  • Оба скрипта избегают ненужной эскалации привилегий; операторы сами решают, когда запускать с повышенными правами.
  • Структурированные выходные данные (reporte_sudo.txt, eventos.csv) помогают прослеживаемости и могут отправляться в централизованные системы логирования.
  • Опциональные параметры (--log, --target, -Cantidad, -RutaSalida) делают скрипты переиспользуемыми на разных хостах и в разных окружениях.
  • Понятные сообщения об ошибках и коды завершения способствуют поведению, дружественному к автоматизации.

Размышление

Проектирование автоматизации для нескольких платформ раскрывает паттерны, которые выходят за рамки языков: валидируй входные данные заранее, разделяй каналы успеха и ошибок и создавай артефакты, которые могут потреблять последующие инструменты. Расширяя эти скрипты — возможно, добавляя хуки оповещений, обогащение данных или политики хранения — не забывай возвращаться к основам, рассмотренным здесь.

Следующие шаги

  • Улучшить log_monitor.py поддержкой regex или интеграцией с journalctl на хостах на базе systemd.
  • Добавить валидацию параметров и структурированное логирование (ConvertTo-Json) в eventos_seguridad.ps1 для более насыщенных пайплайнов.
  • Планировать оба скрипта через cron и Windows Task Scheduler, обеспечивая ротацию логов и ограниченные права доступа.
  • Передавать сгенерированные артефакты в SIEM или лёгкий дашборд для отслеживания трендов во времени.

Эти улучшения помогут твоим кросс-платформенным скриптам мониторинга соответствовать реальным операционным потребностям.

Открыт для работы по контракту

Я доступен для работы по контракту. Если у вас есть интересная идея проекта — запишитесь на звонок через Calendly.

Записаться на 30-минутный звонок