diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index fc93e03..96826b8 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -409,8 +409,10 @@ mode: "0644" loop: - cef_exporter.py + - syslog_forwarder.py - webhook_sender.py - cef-config.yaml + - syslog-forwarder-config.yaml - webhook-config.yaml - requirements.txt when: aw_dlp_integrations_enabled | default(true) | bool @@ -423,6 +425,7 @@ mode: "0755" loop: - cef_exporter.py + - syslog_forwarder.py - webhook_sender.py when: aw_dlp_integrations_enabled | default(true) | bool @@ -466,6 +469,24 @@ mode: "0644" when: aw_dlp_integrations_enabled | default(true) | bool + - name: Установить systemd unit syslog forwarder + ansible.builtin.copy: + src: "{{ aw_repo_root }}/aw-server/dlp-integrations/syslog-forwarder.service" + dest: /etc/systemd/system/aw-dlp-syslog-forwarder.service + owner: root + group: root + mode: "0644" + when: aw_dlp_integrations_enabled | default(true) | bool + + - name: Установить systemd timer syslog forwarder + ansible.builtin.copy: + src: "{{ aw_repo_root }}/aw-server/dlp-integrations/syslog-forwarder.timer" + dest: /etc/systemd/system/aw-dlp-syslog-forwarder.timer + owner: root + group: root + mode: "0644" + when: aw_dlp_integrations_enabled | default(true) | bool + - name: Установить systemd unit webhook sender ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-integrations/webhook-sender.service" @@ -504,6 +525,7 @@ - case_service.py - case_schema.py - case_storage.py + - evidence_chain.py - requirements.txt when: aw_dlp_case_management_enabled | default(true) | bool @@ -559,8 +581,10 @@ mode: "{{ item.mode }}" loop: - { src: "report_generator.py", dest: "/opt/activitywatch/dlp-compliance/report_generator.py", mode: "0755" } + - { src: "compliance_scheduler.py", dest: "/opt/activitywatch/dlp-compliance/compliance_scheduler.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: "templates/pci-dss-report.html", dest: "/opt/activitywatch/dlp-compliance/templates/pci-dss-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 @@ -676,6 +700,13 @@ state: restarted when: aw_dlp_integrations_enabled | default(true) | bool + - name: Включить и перезапустить timer syslog forwarder + ansible.builtin.systemd: + name: aw-dlp-syslog-forwarder.timer + enabled: true + state: restarted + when: aw_dlp_integrations_enabled | default(true) | bool + - name: Включить и перезапустить timer webhook sender ansible.builtin.systemd: name: aw-dlp-webhook-sender.timer diff --git a/ansible/roles/dlp-case-management/tasks/main.yml b/ansible/roles/dlp-case-management/tasks/main.yml index ba06453..0b121a5 100644 --- a/ansible/roles/dlp-case-management/tasks/main.yml +++ b/ansible/roles/dlp-case-management/tasks/main.yml @@ -18,6 +18,7 @@ - case_service.py - case_schema.py - case_storage.py + - evidence_chain.py - requirements.txt - name: Make case service executable diff --git a/ansible/roles/dlp-compliance/tasks/main.yml b/ansible/roles/dlp-compliance/tasks/main.yml index 03faedf..3f681de 100644 --- a/ansible/roles/dlp-compliance/tasks/main.yml +++ b/ansible/roles/dlp-compliance/tasks/main.yml @@ -20,8 +20,10 @@ mode: "{{ item.mode }}" loop: - { src: "report_generator.py", dest: "/opt/activitywatch/dlp-compliance/report_generator.py", mode: "0755" } + - { src: "compliance_scheduler.py", dest: "/opt/activitywatch/dlp-compliance/compliance_scheduler.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: "templates/pci-dss-report.html", dest: "/opt/activitywatch/dlp-compliance/templates/pci-dss-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" } @@ -50,4 +52,3 @@ ansible.builtin.systemd: name: aw-dlp-report-scheduler.service state: started - diff --git a/ansible/roles/dlp-content-analysis/tasks/main.yml b/ansible/roles/dlp-content-analysis/tasks/main.yml new file mode 100644 index 0000000..6a8dde1 --- /dev/null +++ b/ansible/roles/dlp-content-analysis/tasks/main.yml @@ -0,0 +1,37 @@ +--- +- name: Install OCR/system dependencies + ansible.builtin.apt: + name: + - python3-venv + - python3-pip + - tesseract-ocr + - tesseract-ocr-rus + state: present + update_cache: true + +- name: Create content analysis directory + ansible.builtin.file: + path: /opt/activitywatch/dlp-content-analysis + state: directory + owner: "{{ aw_server_user | default('activitywatch') }}" + group: "{{ aw_server_group | default('activitywatch') }}" + mode: "0755" + +- name: Copy content analysis sources + ansible.builtin.copy: + src: "{{ playbook_dir }}/../aw-server/dlp-content-analysis/" + dest: /opt/activitywatch/dlp-content-analysis/ + owner: "{{ aw_server_user | default('activitywatch') }}" + group: "{{ aw_server_group | default('activitywatch') }}" + mode: "0644" + +- name: Create venv for content analysis + ansible.builtin.command: + cmd: python3 -m venv /opt/activitywatch/dlp-content-analysis/.venv + args: + creates: /opt/activitywatch/dlp-content-analysis/.venv/bin/python + +- name: Install Python dependencies for content analysis + ansible.builtin.pip: + requirements: /opt/activitywatch/dlp-content-analysis/requirements.txt + virtualenv: /opt/activitywatch/dlp-content-analysis/.venv diff --git a/ansible/roles/dlp-integrations/tasks/main.yml b/ansible/roles/dlp-integrations/tasks/main.yml index 9676b13..ec1e3bd 100644 --- a/ansible/roles/dlp-integrations/tasks/main.yml +++ b/ansible/roles/dlp-integrations/tasks/main.yml @@ -16,8 +16,10 @@ mode: "0644" loop: - cef_exporter.py + - syslog_forwarder.py - webhook_sender.py - cef-config.yaml + - syslog-forwarder-config.yaml - webhook-config.yaml - requirements.txt @@ -29,6 +31,7 @@ mode: "0755" loop: - cef_exporter.py + - syslog_forwarder.py - webhook_sender.py - name: Create venv for DLP integrations @@ -60,6 +63,8 @@ loop: - { src: "cef-exporter.service", dest: "/etc/systemd/system/aw-dlp-cef-exporter.service" } - { src: "cef-exporter.timer", dest: "/etc/systemd/system/aw-dlp-cef-exporter.timer" } + - { src: "syslog-forwarder.service", dest: "/etc/systemd/system/aw-dlp-syslog-forwarder.service" } + - { src: "syslog-forwarder.timer", dest: "/etc/systemd/system/aw-dlp-syslog-forwarder.timer" } - { src: "webhook-sender.service", dest: "/etc/systemd/system/aw-dlp-webhook-sender.service" } - { src: "webhook-sender.timer", dest: "/etc/systemd/system/aw-dlp-webhook-sender.timer" } @@ -74,4 +79,5 @@ state: restarted loop: - aw-dlp-cef-exporter.timer + - aw-dlp-syslog-forwarder.timer - aw-dlp-webhook-sender.timer diff --git a/aw-server/dlp-case-management/case_storage.py b/aw-server/dlp-case-management/case_storage.py index 5fab597..429be99 100644 --- a/aw-server/dlp-case-management/case_storage.py +++ b/aw-server/dlp-case-management/case_storage.py @@ -8,6 +8,8 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterator +from evidence_chain import evidence_sha256, normalize_evidence_chain + class CaseStorage: def __init__(self, db_path: Path) -> None: @@ -98,6 +100,15 @@ class CaseStorage: def create_case(self, payload: dict[str, Any], actor: str | None = None) -> dict[str, Any]: now = self._now() + normalized_evidence = None + evidence_digest = None + if payload.get("evidence") is not None: + normalized_evidence = normalize_evidence_chain( + payload=payload.get("evidence"), + source_bucket=payload.get("source_bucket"), + source_event_ts=payload.get("source_event_ts"), + ) + evidence_digest = normalized_evidence.get("latest_sha256") or evidence_sha256(payload.get("evidence")) with self.conn() as c: cur = c.execute( """ @@ -114,7 +125,7 @@ class CaseStorage: payload.get("assignee"), payload.get("source_bucket"), payload.get("source_event_ts"), - json.dumps(payload.get("evidence"), ensure_ascii=False) if payload.get("evidence") is not None else None, + json.dumps(normalized_evidence, ensure_ascii=False) if normalized_evidence is not None else None, now, now, ), @@ -125,7 +136,10 @@ class CaseStorage: case_id=case_id, action="create", actor=actor, - details={"fields": {k: v for k, v in payload.items() if k != "evidence"}}, + details={ + "fields": {k: v for k, v in payload.items() if k != "evidence"}, + "evidence_sha256": evidence_digest, + }, ) c.commit() return self.get_case(case_id, c) @@ -252,4 +266,3 @@ class CaseStorage: self._now(), ), ) - diff --git a/aw-server/dlp-case-management/evidence_chain.py b/aw-server/dlp-case-management/evidence_chain.py new file mode 100644 index 0000000..c8c4294 --- /dev/null +++ b/aw-server/dlp-case-management/evidence_chain.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import json +from datetime import datetime, timezone +from typing import Any + + +def _utc_now() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _canonical_json(payload: Any) -> str: + return json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + +def evidence_sha256(payload: Any) -> str: + return hashlib.sha256(_canonical_json(payload).encode("utf-8")).hexdigest() + + +def build_evidence_record( + payload: Any, + source_bucket: str | None = None, + source_event_ts: str | None = None, +) -> dict[str, Any]: + return { + "recorded_at": _utc_now(), + "source_bucket": source_bucket, + "source_event_ts": source_event_ts, + "sha256": evidence_sha256(payload), + "payload": payload, + } + + +def normalize_evidence_chain( + payload: Any, + source_bucket: str | None = None, + source_event_ts: str | None = None, +) -> dict[str, Any]: + if isinstance(payload, dict) and isinstance(payload.get("items"), list): + return payload + record = build_evidence_record( + payload=payload, + source_bucket=source_bucket, + source_event_ts=source_event_ts, + ) + return { + "items": [record], + "latest_sha256": record["sha256"], + "chain_length": 1, + } diff --git a/aw-server/dlp-compliance/compliance_scheduler.py b/aw-server/dlp-compliance/compliance_scheduler.py new file mode 100644 index 0000000..fb55a9a --- /dev/null +++ b/aw-server/dlp-compliance/compliance_scheduler.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json + +from report_generator import generate_report + + +def main() -> None: + parser = argparse.ArgumentParser(description="Run one or more DLP compliance report profiles") + parser.add_argument("--month", help="Month in YYYY-MM format") + parser.add_argument("--profiles", default="152-fz,pci-dss", help="Comma-separated profiles to generate") + parser.add_argument("--stdout-json", action="store_true") + args = parser.parse_args() + + profiles = [item.strip() for item in str(args.profiles).split(",") if item.strip()] + results = [generate_report(month=args.month, profile=profile) for profile in profiles] + if args.stdout_json: + print(json.dumps({"items": results}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/aw-server/dlp-compliance/report-scheduler.service b/aw-server/dlp-compliance/report-scheduler.service index a9395a3..d0ad7da 100644 --- a/aw-server/dlp-compliance/report-scheduler.service +++ b/aw-server/dlp-compliance/report-scheduler.service @@ -6,7 +6,6 @@ After=network-online.target 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 +ExecStart=/opt/activitywatch/dlp-compliance/.venv/bin/python /opt/activitywatch/dlp-compliance/compliance_scheduler.py User=activitywatch Group=activitywatch - diff --git a/aw-server/dlp-compliance/report_generator.py b/aw-server/dlp-compliance/report_generator.py index 0a502f1..9e8fad7 100644 --- a/aw-server/dlp-compliance/report_generator.py +++ b/aw-server/dlp-compliance/report_generator.py @@ -18,7 +18,11 @@ def _env(name: str, default: str) -> str: 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")) +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 @@ -108,10 +112,18 @@ def _render_table(title: str, rows: list[tuple[str, int]]) -> str: return f"
| Параметр | Значение |
|---|