diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml index 313fb29..7a21ebb 100644 --- a/ansible/deploy_aw_server.yml +++ b/ansible/deploy_aw_server.yml @@ -532,6 +532,7 @@ mode: "0644" loop: - case_service.py + - case_rules.py - case_schema.py - case_storage.py - evidence_chain.py diff --git a/aw-server/aw-ru-patch.js b/aw-server/aw-ru-patch.js index 40cc189..4648c2a 100755 --- a/aw-server/aw-ru-patch.js +++ b/aw-server/aw-ru-patch.js @@ -936,9 +936,10 @@ const hideSuppressed = center.querySelector("[data-aw-ru-hide-suppressed]") && center.querySelector("[data-aw-ru-hide-suppressed]").checked; const rows = []; for (const event of state.events) { + const data = event.data || {}; + if (String(data.signalType || "").toLowerCase() === "self_test") continue; const matchedRule = state.activeRules.find(function (rule) { return ruleMatchesEvent(rule, event); }) || null; if (hideSuppressed && matchedRule) continue; - const data = event.data || {}; const eventKey = buildDlpKey(event); rows.push( '' + @@ -1051,6 +1052,9 @@ async function createCaseFromEvent(host, event, row) { const data = event.data || {}; + if (String(data.signalType || "").toLowerCase() === "self_test") { + throw new Error("self_test не должен превращаться в кейс"); + } const verdict = row.querySelector("[data-aw-ru-dlp-verdict]").value; const category = row.querySelector("[data-aw-ru-dlp-category]").value.trim(); const comment = row.querySelector("[data-aw-ru-dlp-comment]").value.trim(); diff --git a/aw-server/dlp-case-management/case_rules.py b/aw-server/dlp-case-management/case_rules.py new file mode 100644 index 0000000..899d0c2 --- /dev/null +++ b/aw-server/dlp-case-management/case_rules.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +from __future__ import annotations + + +def is_self_test_case(incident_id: str | None, title: str | None) -> bool: + incident = str(incident_id or "").lower() + label = str(title or "").lower() + return "|self_test|" in incident or label.startswith("dlp self_test") diff --git a/aw-server/dlp-case-management/case_service.py b/aw-server/dlp-case-management/case_service.py index 62e7d84..c37baf2 100644 --- a/aw-server/dlp-case-management/case_service.py +++ b/aw-server/dlp-case-management/case_service.py @@ -8,6 +8,7 @@ from typing import Any from fastapi import FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware +from case_rules import is_self_test_case from case_schema import CaseCommentCreate, CaseCreate, CaseHayabusaLink, CaseUpdate from case_storage import CaseStorage @@ -30,6 +31,8 @@ def health() -> dict[str, Any]: @APP.post("/api/0/dlp/cases") def create_case(payload: CaseCreate) -> dict[str, Any]: + if is_self_test_case(payload.incident_id, payload.title): + raise HTTPException(status_code=422, detail="self_test cases are not allowed") return STORE.create_case(payload.model_dump(exclude_none=True), actor="api") diff --git a/aw-server/dlp-case-management/case_storage.py b/aw-server/dlp-case-management/case_storage.py index 43702f3..dd84765 100644 --- a/aw-server/dlp-case-management/case_storage.py +++ b/aw-server/dlp-case-management/case_storage.py @@ -127,6 +127,17 @@ class CaseStorage: ) evidence_digest = normalized_evidence.get("latest_sha256") or evidence_sha256(payload.get("evidence")) with self.conn() as c: + existing = c.execute( + """ + SELECT * FROM cases + WHERE incident_id = ? AND COALESCE(host, '') = COALESCE(?, '') + ORDER BY id DESC + LIMIT 1 + """, + (payload["incident_id"], payload.get("host")), + ).fetchone() + if existing: + return self._to_case_dict(existing) cur = c.execute( """ INSERT INTO cases ( diff --git a/aw-server/dlp-case-management/test_case_rules.py b/aw-server/dlp-case-management/test_case_rules.py new file mode 100644 index 0000000..7ca1eb3 --- /dev/null +++ b/aw-server/dlp-case-management/test_case_rules.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import unittest + +from case_rules import is_self_test_case + + +class CaseRulesTest(unittest.TestCase): + def test_is_self_test_case_by_incident_id(self) -> None: + self.assertTrue(is_self_test_case("2026-05-14T17:00:52.186Z|self_test|Администратор|||", "Normal")) + + def test_is_self_test_case_by_title(self) -> None: + self.assertTrue(is_self_test_case("inc-1", "DLP self_test · Администратор")) + + def test_is_self_test_case_false_for_normal_case(self) -> None: + self.assertFalse(is_self_test_case("inc-1", "DLP print incident")) + + +if __name__ == "__main__": + unittest.main() diff --git a/aw-server/dlp-case-management/test_case_storage.py b/aw-server/dlp-case-management/test_case_storage.py index 24e131d..82a3d56 100644 --- a/aw-server/dlp-case-management/test_case_storage.py +++ b/aw-server/dlp-case-management/test_case_storage.py @@ -46,6 +46,22 @@ class CaseStorageHayabusaLinkTest(unittest.TestCase): audit = storage.list_audit(int(created["id"])) self.assertTrue(any(row.get("action") == "link_hayabusa" for row in audit)) + def test_create_case_deduplicates_by_incident_and_host(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + db_path = Path(tmpdir) / "cases.db" + storage = CaseStorage(db_path) + payload = { + "incident_id": "2026-05-14T17:00:52.186Z|self_test|Администратор|||", + "host": "SHARKON2025", + "title": "DLP self_test · Администратор", + "severity": "medium", + } + created = storage.create_case(payload, actor="test") + duplicate = storage.create_case(payload, actor="test") + self.assertEqual(created["id"], duplicate["id"]) + cases = storage.list_cases(host="SHARKON2025", limit=10) + self.assertEqual(len(cases), 1) + if __name__ == "__main__": unittest.main()