diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index bd6be77..fc93e03 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -308,6 +308,9 @@ AW_DLP_CASE_BIND_HOST={{ aw_dlp_case_bind_host }} AW_DLP_CASE_PORT={{ aw_dlp_case_port }} AW_DLP_CASE_DB_PATH={{ aw_dlp_case_db_path }} + AW_DLP_COMPLIANCE_REPORT_DIR={{ aw_dlp_compliance_report_dir }} + AW_DLP_COMPLIANCE_TEMPLATE={{ aw_dlp_compliance_template_path }} + AW_SERVER_URL=http://127.0.0.1:5600/api/0 XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config @@ -534,6 +537,55 @@ mode: "0644" when: aw_dlp_case_management_enabled | default(true) | bool + - name: Создать каталоги DLP compliance + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0755" + loop: + - /opt/activitywatch/dlp-compliance + - /opt/activitywatch/dlp-compliance/templates + - "{{ aw_dlp_compliance_report_dir }}" + when: aw_dlp_compliance_enabled | default(true) | bool + + - name: Скопировать файлы DLP compliance + ansible.builtin.copy: + src: "{{ aw_repo_root }}/aw-server/dlp-compliance/{{ item.src }}" + dest: "{{ item.dest }}" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "{{ item.mode }}" + loop: + - { src: "report_generator.py", dest: "/opt/activitywatch/dlp-compliance/report_generator.py", mode: "0755" } + - { src: "requirements.txt", dest: "/opt/activitywatch/dlp-compliance/requirements.txt", mode: "0644" } + - { src: "templates/152-fz-report.html", dest: "/opt/activitywatch/dlp-compliance/templates/152-fz-report.html", mode: "0644" } + - { src: "report-scheduler.service", dest: "/etc/systemd/system/aw-dlp-report-scheduler.service", mode: "0644" } + - { src: "report-scheduler.timer", dest: "/etc/systemd/system/aw-dlp-report-scheduler.timer", mode: "0644" } + when: aw_dlp_compliance_enabled | default(true) | bool + + - name: Создать virtualenv DLP compliance + ansible.builtin.command: + cmd: python3 -m venv /opt/activitywatch/dlp-compliance/.venv + args: + creates: /opt/activitywatch/dlp-compliance/.venv/bin/python + when: aw_dlp_compliance_enabled | default(true) | bool + + - name: Установить зависимости DLP compliance + ansible.builtin.pip: + requirements: /opt/activitywatch/dlp-compliance/requirements.txt + virtualenv: /opt/activitywatch/dlp-compliance/.venv + when: aw_dlp_compliance_enabled | default(true) | bool + + - name: Установить dlp-admin-cli + ansible.builtin.copy: + src: "{{ aw_repo_root }}/scripts/dlp-admin-cli.py" + dest: /usr/local/bin/dlp-admin-cli.py + owner: root + group: root + mode: "0755" + - name: Установить скрипт AW worktime API ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py" @@ -638,6 +690,20 @@ state: restarted when: aw_dlp_case_management_enabled | default(true) | bool + - name: Включить и перезапустить timer DLP compliance report + ansible.builtin.systemd: + name: aw-dlp-report-scheduler.timer + enabled: true + state: restarted + when: aw_dlp_compliance_enabled | default(true) | bool + + - name: Выполнить разовый прогон DLP compliance report + ansible.builtin.systemd: + name: aw-dlp-report-scheduler.service + state: started + failed_when: false + when: aw_dlp_compliance_enabled | default(true) | bool + - name: Включить и перезапустить AW worktime API ansible.builtin.systemd: name: aw-worktime-api.service diff --git a/ansible/deploy_dlp_full_stack.yml b/ansible/deploy_dlp_full_stack.yml index 5876679..be654b3 100644 --- a/ansible/deploy_dlp_full_stack.yml +++ b/ansible/deploy_dlp_full_stack.yml @@ -11,3 +11,4 @@ - role: dlp-content-analysis - role: dlp-integrations - role: dlp-case-management + - role: dlp-compliance diff --git a/ansible/group_vars/all.example.yml b/ansible/group_vars/all.example.yml index f6d14d0..c114265 100644 --- a/ansible/group_vars/all.example.yml +++ b/ansible/group_vars/all.example.yml @@ -46,4 +46,7 @@ aw_dlp_case_management_enabled: true aw_dlp_case_bind_host: "0.0.0.0" aw_dlp_case_port: 5602 aw_dlp_case_db_path: "/opt/activitywatch/dlp-case-management/cases.db" +aw_dlp_compliance_enabled: true +aw_dlp_compliance_report_dir: "/opt/activitywatch/dlp-compliance/reports" +aw_dlp_compliance_template_path: "/opt/activitywatch/dlp-compliance/templates/152-fz-report.html" aw_server_post_deploy_health_check_enabled: true diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 6036c77..7ac7650 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -37,6 +37,9 @@ aw_dlp_case_management_enabled: true aw_dlp_case_bind_host: "0.0.0.0" aw_dlp_case_port: 5602 aw_dlp_case_db_path: "/opt/activitywatch/dlp-case-management/cases.db" +aw_dlp_compliance_enabled: true +aw_dlp_compliance_report_dir: "/opt/activitywatch/dlp-compliance/reports" +aw_dlp_compliance_template_path: "/opt/activitywatch/dlp-compliance/templates/152-fz-report.html" aw_server_post_deploy_health_check_enabled: true aw_worktime_from: "08:00" diff --git a/ansible/roles/dlp-compliance/tasks/main.yml b/ansible/roles/dlp-compliance/tasks/main.yml new file mode 100644 index 0000000..03faedf --- /dev/null +++ b/ansible/roles/dlp-compliance/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: Ensure DLP compliance directory tree + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ aw_server_user | default('activitywatch') }}" + group: "{{ aw_server_group | default('activitywatch') }}" + mode: "0755" + loop: + - /opt/activitywatch/dlp-compliance + - /opt/activitywatch/dlp-compliance/templates + - /opt/activitywatch/dlp-compliance/reports + +- name: Copy DLP compliance files + ansible.builtin.copy: + src: "{{ playbook_dir }}/../aw-server/dlp-compliance/{{ item.src }}" + dest: "{{ item.dest }}" + owner: "{{ aw_server_user | default('activitywatch') }}" + group: "{{ aw_server_group | default('activitywatch') }}" + mode: "{{ item.mode }}" + loop: + - { src: "report_generator.py", dest: "/opt/activitywatch/dlp-compliance/report_generator.py", mode: "0755" } + - { src: "requirements.txt", dest: "/opt/activitywatch/dlp-compliance/requirements.txt", mode: "0644" } + - { src: "templates/152-fz-report.html", dest: "/opt/activitywatch/dlp-compliance/templates/152-fz-report.html", mode: "0644" } + - { src: "report-scheduler.service", dest: "/etc/systemd/system/aw-dlp-report-scheduler.service", mode: "0644" } + - { src: "report-scheduler.timer", dest: "/etc/systemd/system/aw-dlp-report-scheduler.timer", mode: "0644" } + +- name: Create venv for DLP compliance + ansible.builtin.command: + cmd: python3 -m venv /opt/activitywatch/dlp-compliance/.venv + args: + creates: /opt/activitywatch/dlp-compliance/.venv/bin/python + +- name: Install Python dependencies for DLP compliance + ansible.builtin.pip: + requirements: /opt/activitywatch/dlp-compliance/requirements.txt + virtualenv: /opt/activitywatch/dlp-compliance/.venv + +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + +- name: Enable and start DLP compliance timer + ansible.builtin.systemd: + name: aw-dlp-report-scheduler.timer + enabled: true + state: restarted + +- name: Run DLP compliance report once + ansible.builtin.systemd: + name: aw-dlp-report-scheduler.service + state: started + diff --git a/aw-server/dlp-compliance/report-scheduler.service b/aw-server/dlp-compliance/report-scheduler.service new file mode 100644 index 0000000..a9395a3 --- /dev/null +++ b/aw-server/dlp-compliance/report-scheduler.service @@ -0,0 +1,12 @@ +[Unit] +Description=AWatch DLP 152-FZ compliance report generator +After=network-online.target + +[Service] +Type=oneshot +EnvironmentFile=-/etc/activitywatch/aw-server.env +WorkingDirectory=/opt/activitywatch/dlp-compliance +ExecStart=/opt/activitywatch/dlp-compliance/.venv/bin/python /opt/activitywatch/dlp-compliance/report_generator.py +User=activitywatch +Group=activitywatch + diff --git a/aw-server/dlp-compliance/report-scheduler.timer b/aw-server/dlp-compliance/report-scheduler.timer new file mode 100644 index 0000000..a17d695 --- /dev/null +++ b/aw-server/dlp-compliance/report-scheduler.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Monthly AWatch DLP 152-FZ compliance report schedule + +[Timer] +OnCalendar=monthly +Persistent=true +RandomizedDelaySec=5m +Unit=aw-dlp-report-scheduler.service + +[Install] +WantedBy=timers.target + diff --git a/aw-server/dlp-compliance/report_generator.py b/aw-server/dlp-compliance/report_generator.py index f10758d..0a502f1 100644 --- a/aw-server/dlp-compliance/report_generator.py +++ b/aw-server/dlp-compliance/report_generator.py @@ -1,20 +1,175 @@ #!/usr/bin/env python3 from __future__ import annotations -from datetime import datetime +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 render_html(period: str) -> str: - return f"""
Период: {period}
Сгенерирован: {datetime.now().isoformat()}
""" +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")) +TEMPLATE_PATH = Path(_env("AW_DLP_COMPLIANCE_TEMPLATE", "/opt/activitywatch/dlp-compliance/templates/152-fz-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"| Параметр | Значение |
|---|