feat(dlp): add enterprise phase scaffolds (policy role, content analysis, siem, case, compliance, cli)

This commit is contained in:
igor04091968
2026-05-11 23:18:20 +03:00
parent 8be0de1c85
commit b213552d9d
19 changed files with 454 additions and 0 deletions
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import Any
from fastapi import FastAPI
from pydantic import BaseModel
DB = Path("/opt/activitywatch/dlp-case-management/cases.db")
APP = FastAPI(title="AWatch DLP Case Management")
class CaseCreate(BaseModel):
incident_id: str
title: str
severity: str = "medium"
assignee: str | None = None
def _conn() -> sqlite3.Connection:
DB.parent.mkdir(parents=True, exist_ok=True)
c = sqlite3.connect(DB)
c.execute(
"CREATE TABLE IF NOT EXISTS cases (id INTEGER PRIMARY KEY, incident_id TEXT, title TEXT, severity TEXT, assignee TEXT, status TEXT DEFAULT 'open')"
)
return c
@APP.post("/api/0/dlp/cases")
def create_case(payload: CaseCreate) -> dict[str, Any]:
c = _conn()
cur = c.cursor()
cur.execute(
"INSERT INTO cases (incident_id,title,severity,assignee,status) VALUES (?,?,?,?,?)",
(payload.incident_id, payload.title, payload.severity, payload.assignee, "open"),
)
c.commit()
case_id = cur.lastrowid
c.close()
return {"id": case_id}
@APP.get("/api/0/dlp/cases")
def list_cases() -> list[dict[str, Any]]:
c = _conn()
rows = c.execute("SELECT id,incident_id,title,severity,assignee,status FROM cases ORDER BY id DESC").fetchall()
c.close()
return [
{"id": r[0], "incident_id": r[1], "title": r[2], "severity": r[3], "assignee": r[4], "status": r[5]}
for r in rows
]
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
from __future__ import annotations
from datetime import datetime
from pathlib import Path
def render_html(period: str) -> str:
return f"""<html><body><h1>Отчет 152-ФЗ</h1><p>Период: {period}</p><p>Сгенерирован: {datetime.now().isoformat()}</p></body></html>"""
def main() -> None:
period = datetime.now().strftime("%Y-%m")
out = Path("/opt/activitywatch/dlp-compliance/reports")
out.mkdir(parents=True, exist_ok=True)
html = out / f"152-fz-{period}.html"
html.write_text(render_html(period), encoding="utf-8")
if __name__ == "__main__":
main()
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
from __future__ import annotations
import re
def validate_inn(value: str) -> bool:
digits = re.sub(r"\D", "", value)
if len(digits) == 10:
coef = [2, 4, 10, 3, 5, 9, 4, 6, 8]
chk = sum(int(digits[i]) * coef[i] for i in range(9)) % 11 % 10
return chk == int(digits[9])
if len(digits) == 12:
c11 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
c12 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
chk11 = sum(int(digits[i]) * c11[i] for i in range(10)) % 11 % 10
chk12 = sum(int(digits[i]) * c12[i] for i in range(11)) % 11 % 10
return chk11 == int(digits[10]) and chk12 == int(digits[11])
return False
def validate_snils(value: str) -> bool:
digits = re.sub(r"\D", "", value)
if len(digits) != 11:
return False
number = digits[:9]
checksum = int(digits[9:])
s = sum(int(number[i]) * (9 - i) for i in range(9))
if s < 100:
expected = s
elif s in (100, 101):
expected = 0
else:
expected = s % 101
if expected == 100:
expected = 0
return checksum == expected
@@ -0,0 +1,17 @@
{
"inn": {
"regex": "\\b\\d{10}\\b|\\b\\d{12}\\b",
"checksum": "inn",
"description": "ИНН"
},
"snils": {
"regex": "\\b\\d{3}-\\d{3}-\\d{3}\\s?\\d{2}\\b",
"checksum": "snils",
"description": "СНИЛС"
},
"passport": {
"regex": "\\b\\d{4}\\s?\\d{6}\\b",
"checksum": "none",
"description": "Паспорт РФ"
}
}
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import pathlib
import re
from typing import Any
from checksum_validator import validate_inn, validate_snils
def _validate(kind: str, value: str) -> bool:
if kind == "inn":
return validate_inn(value)
if kind == "snils":
return validate_snils(value)
return True
def match_text(text: str, dictionary_path: str) -> list[dict[str, Any]]:
rules = json.loads(pathlib.Path(dictionary_path).read_text(encoding="utf-8"))
results: list[dict[str, Any]] = []
for name, rule in rules.items():
regex = re.compile(rule["regex"])
checksum_kind = rule.get("checksum", "none")
for m in regex.finditer(text):
token = m.group(0)
if _validate(checksum_kind, token):
results.append(
{
"name": name,
"description": rule.get("description", name),
"value": token,
"start": m.start(),
"end": m.end(),
}
)
return results
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
from PIL import Image
import pytesseract
def extract_text(image_path: str) -> str:
path = Path(image_path)
if not path.exists():
return ""
img = Image.open(path)
return pytesseract.image_to_string(img, lang="rus+eng")
@@ -0,0 +1,6 @@
{
"rules": [
{ "id": "email", "regex": "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", "severity": "low" },
{ "id": "phone-ru", "regex": "(?:\\+7|8)\\s*\\(?\\d{3}\\)?\\s*\\d{3}[- ]?\\d{2}[- ]?\\d{2}", "severity": "low" }
]
}
@@ -0,0 +1,6 @@
{
"rules": [
{ "id": "card-pan", "regex": "\\b(?:\\d[ -]*?){13,19}\\b", "severity": "high" },
{ "id": "iban", "regex": "\\b[A-Z]{2}\\d{2}[A-Z0-9]{11,30}\\b", "severity": "medium" }
]
}
@@ -0,0 +1,6 @@
{
"rules": [
{ "id": "aws-access-key", "regex": "AKIA[0-9A-Z]{16}", "severity": "high" },
{ "id": "generic-password", "regex": "(?i)(password|пароль)\\s*[:=]\\s*\\S{6,}", "severity": "medium" }
]
}
@@ -0,0 +1,12 @@
[Unit]
Description=AWatch DLP CEF Exporter
After=network-online.target
[Service]
Type=oneshot
User=activitywatch
Group=activitywatch
ExecStart=/usr/bin/python3 /opt/activitywatch/dlp-integrations/cef_exporter.py
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,11 @@
[Unit]
Description=Run AWatch DLP CEF Exporter every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=cef-exporter.service
Persistent=true
[Install]
WantedBy=timers.target
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import logging
import os
import socket
from datetime import datetime, timezone
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
def build_cef(event: dict) -> str:
sev_map = {"low": 3, "medium": 6, "high": 10}
sev = sev_map.get(event.get("severity", "low"), 3)
ts = datetime.now(timezone.utc).isoformat()
msg = event.get("message", "")
host = event.get("hostname", "unknown")
return f"CEF:0|AWatch-rus|DLP|1.0|{event.get('id','dlp')}|{msg}|{sev}|rt={ts} shost={host}"
def send_syslog(line: str, host: str, port: int) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(line.encode("utf-8", errors="ignore"), (host, port))
finally:
sock.close()
def main() -> None:
sample = os.environ.get("AW_DLP_CEF_SAMPLE", "")
event = json.loads(sample) if sample else {"id": "startup", "message": "cef exporter heartbeat", "severity": "low"}
line = build_cef(event)
host = os.environ.get("AW_DLP_SYSLOG_HOST", "127.0.0.1")
port = int(os.environ.get("AW_DLP_SYSLOG_PORT", "514"))
send_syslog(line, host, port)
logging.info("sent CEF event to %s:%d", host, port)
if __name__ == "__main__":
main()
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import time
from urllib import request
def post(url: str, payload: dict, retries: int = 3) -> bool:
body = json.dumps(payload).encode("utf-8")
for i in range(retries):
try:
req = request.Request(url, data=body, headers={"Content-Type": "application/json"}, method="POST")
with request.urlopen(req, timeout=10):
return True
except Exception:
time.sleep(2 ** i)
return False
def main() -> None:
hooks = [h.strip() for h in os.environ.get("AW_DLP_CRITICAL_WEBHOOKS", "").split(",") if h.strip()]
payload = {"text": "AWatch DLP critical incident", "severity": "high"}
for hook in hooks:
post(hook, payload)
if __name__ == "__main__":
main()
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
from __future__ import annotations
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
app = FastAPI(title="AWatch DLP Metrics")
@app.get("/metrics", response_class=PlainTextResponse)
def metrics() -> str:
# Minimal exporter baseline for Prometheus scraping.
return "\n".join(
[
"# HELP aw_dlp_exporter_up Exporter availability.",
"# TYPE aw_dlp_exporter_up gauge",
"aw_dlp_exporter_up 1",
]
)