#!/usr/bin/env python3 from __future__ import annotations import argparse import json import os from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path from urllib.parse import quote from urllib.request import Request, urlopen def _env(name: str, default: str) -> str: value = os.environ.get(name) return value if value not in (None, "") else default AW_API_BASE = _env("AW_SERVER_URL", "http://127.0.0.1:5600/api/0").rstrip("/") OUTPUT_DIR = Path(_env("AW_DLP_COMPLIANCE_REPORT_DIR", "/opt/activitywatch/dlp-compliance/reports")) BASE_DIR = Path(__file__).resolve().parent PROFILE_TEMPLATE_MAP = { "152-fz": BASE_DIR / "templates" / "152-fz-report.html", "pci-dss": BASE_DIR / "templates" / "pci-dss-report.html", } @dataclass class ReportStats: total_incidents: int high: int medium: int low: int by_host: dict[str, int] channels: dict[str, int] def _http_json(url: str) -> object: req = Request(url, headers={"Accept": "application/json"}) with urlopen(req, timeout=30) as response: return json.loads(response.read().decode("utf-8")) def _parse_ts(value: str | None) -> datetime | None: if not value: return None text = value.replace("Z", "+00:00") try: return datetime.fromisoformat(text).astimezone(UTC) except ValueError: return None def _load_incidents(start: datetime, end: datetime) -> list[dict]: buckets = _http_json(f"{AW_API_BASE}/buckets") if not isinstance(buckets, dict): return [] bucket_ids = sorted([bid for bid in buckets.keys() if str(bid).startswith("aw-dlp-incidents_")]) incidents: list[dict] = [] for bucket_id in bucket_ids: encoded = quote(str(bucket_id), safe="") events = _http_json(f"{AW_API_BASE}/buckets/{encoded}/events?limit=2000") if not isinstance(events, list): continue for event in events: if not isinstance(event, dict): continue ts = _parse_ts(event.get("timestamp")) if ts is None or ts < start or ts > end: continue incidents.append(event) return incidents def _build_stats(incidents: list[dict]) -> ReportStats: by_host: dict[str, int] = {} channels: dict[str, int] = {} high = medium = low = 0 for event in incidents: data = event.get("data") or {} if not isinstance(data, dict): data = {} host = str(data.get("hostname") or "unknown") by_host[host] = by_host.get(host, 0) + 1 severity = str(data.get("severity") or "low").lower() if severity == "high": high += 1 elif severity == "medium": medium += 1 else: low += 1 channel = str(data.get("signalType") or data.get("source") or "unknown") channels[channel] = channels.get(channel, 0) + 1 return ReportStats( total_incidents=len(incidents), high=high, medium=medium, low=low, by_host=dict(sorted(by_host.items(), key=lambda item: item[1], reverse=True)), channels=dict(sorted(channels.items(), key=lambda item: item[1], reverse=True)), ) def _render_table(title: str, rows: list[tuple[str, int]]) -> str: if not rows: return f"
Нет данных
" body = "".join([f"| Параметр | Значение |
|---|