feat(dlp-policy): add approval workflow, full audit trail, and CRUD documentation
This commit is contained in:
@@ -47,6 +47,11 @@ class PolicyActivateRequest(BaseModel):
|
|||||||
actor: str | None = Field(default="api")
|
actor: str | None = Field(default="api")
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyStatusRequest(BaseModel):
|
||||||
|
actor: str | None = Field(default="api")
|
||||||
|
comment: str | None = Field(default=None, max_length=2048)
|
||||||
|
|
||||||
|
|
||||||
class PolicyRecord(BaseModel):
|
class PolicyRecord(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
@@ -64,4 +69,3 @@ class PolicyVersionRecord(BaseModel):
|
|||||||
checksum: str
|
checksum: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
created_by: str | None
|
created_by: str | None
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any
|
|||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
|
|
||||||
from policy_distributor import build_policy_bundle
|
from policy_distributor import build_policy_bundle
|
||||||
from policy_schema import PolicyActivateRequest, PolicyCreateRequest, PolicyUpdateRequest
|
from policy_schema import PolicyActivateRequest, PolicyCreateRequest, PolicyStatusRequest, PolicyUpdateRequest
|
||||||
from policy_storage import PolicyStorage
|
from policy_storage import PolicyStorage
|
||||||
|
|
||||||
|
|
||||||
@@ -157,7 +157,34 @@ def update_policy(policy_id: int, payload: PolicyUpdateRequest) -> dict[str, obj
|
|||||||
|
|
||||||
@app.post("/api/0/dlp/policies/{policy_id}/activate")
|
@app.post("/api/0/dlp/policies/{policy_id}/activate")
|
||||||
def activate_policy(policy_id: int, payload: PolicyActivateRequest) -> dict[str, object]:
|
def activate_policy(policy_id: int, payload: PolicyActivateRequest) -> dict[str, object]:
|
||||||
|
try:
|
||||||
item = storage.activate_policy(policy_id=policy_id, actor=payload.actor)
|
item = storage.activate_policy(policy_id=policy_id, actor=payload.actor)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="policy not found")
|
||||||
|
return {"item": item}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/0/dlp/policies/{policy_id}/submit")
|
||||||
|
def submit_policy_for_approval(policy_id: int, payload: PolicyStatusRequest) -> dict[str, object]:
|
||||||
|
item = storage.set_policy_status(policy_id, "pending_approval", payload.actor, payload.comment)
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="policy not found")
|
||||||
|
return {"item": item}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/0/dlp/policies/{policy_id}/approve")
|
||||||
|
def approve_policy(policy_id: int, payload: PolicyStatusRequest) -> dict[str, object]:
|
||||||
|
item = storage.set_policy_status(policy_id, "approved", payload.actor, payload.comment)
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(status_code=404, detail="policy not found")
|
||||||
|
return {"item": item}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/0/dlp/policies/{policy_id}/draft")
|
||||||
|
def return_policy_to_draft(policy_id: int, payload: PolicyStatusRequest) -> dict[str, object]:
|
||||||
|
item = storage.set_policy_status(policy_id, "draft", payload.actor, payload.comment)
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="policy not found")
|
raise HTTPException(status_code=404, detail="policy not found")
|
||||||
return {"item": item}
|
return {"item": item}
|
||||||
@@ -173,3 +200,15 @@ def delete_policy(policy_id: int) -> dict[str, bool]:
|
|||||||
if not deleted:
|
if not deleted:
|
||||||
raise HTTPException(status_code=404, detail="policy not found")
|
raise HTTPException(status_code=404, detail="policy not found")
|
||||||
return {"deleted": True}
|
return {"deleted": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/0/dlp/policies/audit")
|
||||||
|
def list_policy_audit(limit: int = 200) -> dict[str, object]:
|
||||||
|
return {"items": storage.list_audit(policy_id=None, limit=max(1, min(limit, 1000)))}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/0/dlp/policies/{policy_id}/audit")
|
||||||
|
def list_single_policy_audit(policy_id: int, limit: int = 200) -> dict[str, object]:
|
||||||
|
if not storage.get_policy(policy_id):
|
||||||
|
raise HTTPException(status_code=404, detail="policy not found")
|
||||||
|
return {"items": storage.list_audit(policy_id=policy_id, limit=max(1, min(limit, 1000)))}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class PolicyStorage:
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'draft',
|
||||||
is_active INTEGER NOT NULL DEFAULT 0,
|
is_active INTEGER NOT NULL DEFAULT 0,
|
||||||
current_version INTEGER NOT NULL DEFAULT 1,
|
current_version INTEGER NOT NULL DEFAULT 1,
|
||||||
checksum TEXT NOT NULL,
|
checksum TEXT NOT NULL,
|
||||||
@@ -67,16 +68,55 @@ class PolicyStorage:
|
|||||||
UNIQUE(policy_id, version)
|
UNIQUE(policy_id, version)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS policy_audit (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
policy_id INTEGER,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
actor TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
details_json TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(policy_id) REFERENCES policies(id)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_policies_active ON policies(is_active);
|
CREATE INDEX IF NOT EXISTS idx_policies_active ON policies(is_active);
|
||||||
CREATE INDEX IF NOT EXISTS idx_policy_versions_policy ON policy_versions(policy_id, version DESC);
|
CREATE INDEX IF NOT EXISTS idx_policy_versions_policy ON policy_versions(policy_id, version DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_policy_audit_policy ON policy_audit(policy_id, id DESC);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
cols = [r["name"] for r in conn.execute("PRAGMA table_info(policies)").fetchall()]
|
||||||
|
if "status" not in cols:
|
||||||
|
conn.execute("ALTER TABLE policies ADD COLUMN status TEXT NOT NULL DEFAULT 'draft'")
|
||||||
|
|
||||||
|
def _audit(
|
||||||
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
policy_id: int | None,
|
||||||
|
action: str,
|
||||||
|
actor: str | None,
|
||||||
|
comment: str | None = None,
|
||||||
|
details: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO policy_audit(policy_id, action, actor, comment, details_json, created_at)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
policy_id,
|
||||||
|
action,
|
||||||
|
actor,
|
||||||
|
comment,
|
||||||
|
canonical_policy_json(details) if details is not None else None,
|
||||||
|
utc_now(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def list_policies(self) -> list[dict[str, Any]]:
|
def list_policies(self) -> list[dict[str, Any]]:
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, description, is_active, current_version, checksum, created_at, updated_at
|
SELECT id, name, description, status, is_active, current_version, checksum, created_at, updated_at
|
||||||
FROM policies
|
FROM policies
|
||||||
ORDER BY is_active DESC, updated_at DESC, id DESC
|
ORDER BY is_active DESC, updated_at DESC, id DESC
|
||||||
"""
|
"""
|
||||||
@@ -87,7 +127,7 @@ class PolicyStorage:
|
|||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
policy_row = conn.execute(
|
policy_row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, description, is_active, current_version, checksum, created_at, updated_at
|
SELECT id, name, description, status, is_active, current_version, checksum, created_at, updated_at
|
||||||
FROM policies
|
FROM policies
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
@@ -123,16 +163,17 @@ class PolicyStorage:
|
|||||||
def create_policy(self, name: str, description: str | None, policy: dict[str, Any], activate: bool, actor: str | None) -> dict[str, Any]:
|
def create_policy(self, name: str, description: str | None, policy: dict[str, Any], activate: bool, actor: str | None) -> dict[str, Any]:
|
||||||
checksum = checksum_policy(policy)
|
checksum = checksum_policy(policy)
|
||||||
now = utc_now()
|
now = utc_now()
|
||||||
|
status = "deployed" if activate else "draft"
|
||||||
policy_json = canonical_policy_json(policy)
|
policy_json = canonical_policy_json(policy)
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
if activate:
|
if activate:
|
||||||
conn.execute("UPDATE policies SET is_active = 0")
|
conn.execute("UPDATE policies SET is_active = 0")
|
||||||
cursor = conn.execute(
|
cursor = conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO policies(name, description, is_active, current_version, checksum, created_at, updated_at)
|
INSERT INTO policies(name, description, status, is_active, current_version, checksum, created_at, updated_at)
|
||||||
VALUES(?, ?, ?, 1, ?, ?, ?)
|
VALUES(?, ?, ?, ?, 1, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(name, description, 1 if activate else 0, checksum, now, now),
|
(name, description, status, 1 if activate else 0, checksum, now, now),
|
||||||
)
|
)
|
||||||
policy_id = int(cursor.lastrowid)
|
policy_id = int(cursor.lastrowid)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -142,6 +183,7 @@ class PolicyStorage:
|
|||||||
""",
|
""",
|
||||||
(policy_id, policy_json, checksum, now, actor),
|
(policy_id, policy_json, checksum, now, actor),
|
||||||
)
|
)
|
||||||
|
self._audit(conn, policy_id, "create", actor, details={"activate": activate, "status": status})
|
||||||
return self.get_policy(policy_id) # type: ignore[return-value]
|
return self.get_policy(policy_id) # type: ignore[return-value]
|
||||||
|
|
||||||
def update_policy(
|
def update_policy(
|
||||||
@@ -162,10 +204,12 @@ class PolicyStorage:
|
|||||||
new_description = description if description is not None else current["description"]
|
new_description = description if description is not None else current["description"]
|
||||||
new_version = int(current["current_version"])
|
new_version = int(current["current_version"])
|
||||||
new_checksum = current["checksum"]
|
new_checksum = current["checksum"]
|
||||||
|
new_status = current.get("status", "draft")
|
||||||
|
|
||||||
if policy is not None:
|
if policy is not None:
|
||||||
new_version += 1
|
new_version += 1
|
||||||
new_checksum = checksum_policy(policy)
|
new_checksum = checksum_policy(policy)
|
||||||
|
new_status = "draft"
|
||||||
policy_json = canonical_policy_json(policy)
|
policy_json = canonical_policy_json(policy)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -177,16 +221,18 @@ class PolicyStorage:
|
|||||||
|
|
||||||
if activate:
|
if activate:
|
||||||
conn.execute("UPDATE policies SET is_active = 0")
|
conn.execute("UPDATE policies SET is_active = 0")
|
||||||
|
new_status = "deployed"
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE policies
|
UPDATE policies
|
||||||
SET name = ?, description = ?, is_active = ?, current_version = ?, checksum = ?, updated_at = ?
|
SET name = ?, description = ?, status = ?, is_active = ?, current_version = ?, checksum = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
new_name,
|
new_name,
|
||||||
new_description,
|
new_description,
|
||||||
|
new_status,
|
||||||
1 if activate else current["is_active"],
|
1 if activate else current["is_active"],
|
||||||
new_version,
|
new_version,
|
||||||
new_checksum,
|
new_checksum,
|
||||||
@@ -194,19 +240,23 @@ class PolicyStorage:
|
|||||||
policy_id,
|
policy_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
self._audit(conn, policy_id, "update", actor, details={"activate": activate, "status": new_status})
|
||||||
return self.get_policy(policy_id)
|
return self.get_policy(policy_id)
|
||||||
|
|
||||||
def activate_policy(self, policy_id: int, actor: str | None) -> dict[str, Any] | None:
|
def activate_policy(self, policy_id: int, actor: str | None) -> dict[str, Any] | None:
|
||||||
current = self.get_policy(policy_id)
|
current = self.get_policy(policy_id)
|
||||||
if not current:
|
if not current:
|
||||||
return None
|
return None
|
||||||
|
if current.get("status") != "approved":
|
||||||
|
raise ValueError("policy must be approved before deploy")
|
||||||
|
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
conn.execute("UPDATE policies SET is_active = 0")
|
conn.execute("UPDATE policies SET is_active = 0")
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE policies SET is_active = 1, updated_at = ? WHERE id = ?",
|
"UPDATE policies SET status = 'deployed', is_active = 1, updated_at = ? WHERE id = ?",
|
||||||
(utc_now(), policy_id),
|
(utc_now(), policy_id),
|
||||||
)
|
)
|
||||||
|
self._audit(conn, policy_id, "deploy", actor)
|
||||||
return self.get_policy(policy_id)
|
return self.get_policy(policy_id)
|
||||||
|
|
||||||
def rollback_active_policy(self, actor: str | None) -> dict[str, Any] | None:
|
def rollback_active_policy(self, actor: str | None) -> dict[str, Any] | None:
|
||||||
@@ -252,11 +302,12 @@ class PolicyStorage:
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE policies
|
UPDATE policies
|
||||||
SET current_version = ?, checksum = ?, updated_at = ?
|
SET status = 'draft', current_version = ?, checksum = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(rollback_version, rollback_checksum, now, active["id"]),
|
(rollback_version, rollback_checksum, now, active["id"]),
|
||||||
)
|
)
|
||||||
|
self._audit(conn, int(active["id"]), "rollback", actor, details={"rollback_to": previous_version})
|
||||||
return self.get_policy(int(active["id"]))
|
return self.get_policy(int(active["id"]))
|
||||||
|
|
||||||
def delete_policy(self, policy_id: int) -> bool:
|
def delete_policy(self, policy_id: int) -> bool:
|
||||||
@@ -267,6 +318,50 @@ class PolicyStorage:
|
|||||||
raise ValueError("cannot delete active policy")
|
raise ValueError("cannot delete active policy")
|
||||||
|
|
||||||
with self.connect() as conn:
|
with self.connect() as conn:
|
||||||
|
self._audit(conn, policy_id, "delete", None)
|
||||||
conn.execute("DELETE FROM policy_versions WHERE policy_id = ?", (policy_id,))
|
conn.execute("DELETE FROM policy_versions WHERE policy_id = ?", (policy_id,))
|
||||||
conn.execute("DELETE FROM policies WHERE id = ?", (policy_id,))
|
conn.execute("DELETE FROM policies WHERE id = ?", (policy_id,))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_policy_status(self, policy_id: int, status: str, actor: str | None, comment: str | None = None) -> dict[str, Any] | None:
|
||||||
|
current = self.get_policy(policy_id)
|
||||||
|
if not current:
|
||||||
|
return None
|
||||||
|
allowed = {"draft", "pending_approval", "approved", "deployed"}
|
||||||
|
if status not in allowed:
|
||||||
|
raise ValueError(f"unsupported status: {status}")
|
||||||
|
with self.connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE policies SET status = ?, updated_at = ? WHERE id = ?",
|
||||||
|
(status, utc_now(), policy_id),
|
||||||
|
)
|
||||||
|
self._audit(conn, policy_id, "status_change", actor, comment=comment, details={"status": status})
|
||||||
|
return self.get_policy(policy_id)
|
||||||
|
|
||||||
|
def list_audit(self, policy_id: int | None = None, limit: int = 200) -> list[dict[str, Any]]:
|
||||||
|
query = """
|
||||||
|
SELECT id, policy_id, action, actor, comment, details_json, created_at
|
||||||
|
FROM policy_audit
|
||||||
|
"""
|
||||||
|
params: tuple[Any, ...]
|
||||||
|
if policy_id is None:
|
||||||
|
query += " ORDER BY id DESC LIMIT ?"
|
||||||
|
params = (limit,)
|
||||||
|
else:
|
||||||
|
query += " WHERE policy_id = ? ORDER BY id DESC LIMIT ?"
|
||||||
|
params = (policy_id, limit)
|
||||||
|
with self.connect() as conn:
|
||||||
|
rows = conn.execute(query, params).fetchall()
|
||||||
|
items: list[dict[str, Any]] = []
|
||||||
|
for row in rows:
|
||||||
|
d = dict(row)
|
||||||
|
if d.get("details_json"):
|
||||||
|
try:
|
||||||
|
d["details"] = json.loads(d["details_json"])
|
||||||
|
except Exception:
|
||||||
|
d["details"] = None
|
||||||
|
else:
|
||||||
|
d["details"] = None
|
||||||
|
d.pop("details_json", None)
|
||||||
|
items.append(d)
|
||||||
|
return items
|
||||||
|
|||||||
+54
-68
@@ -2,88 +2,74 @@
|
|||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
`aw-server/dlp-policy-engine` centralizes DLP policy lifecycle for `AWatch-rus` Windows endpoints.
|
`aw-server/dlp-policy-engine` is the centralized policy lifecycle service for `AWatch-rus` endpoints.
|
||||||
|
|
||||||
It does not replace endpoint-local safety. Endpoints can run in:
|
It supports:
|
||||||
- `local`
|
- full CRUD for policy documents;
|
||||||
- `server`
|
- policy versioning in SQLite;
|
||||||
- `cached` fallback after server outage
|
- approval workflow (`draft -> pending_approval -> approved -> deployed`);
|
||||||
|
- audit trail for every policy mutation;
|
||||||
|
- agent push/pull coordination (`heartbeat` + `desired` refresh hint).
|
||||||
|
|
||||||
## API
|
## Base URL
|
||||||
|
|
||||||
Base URL:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://<aw-server>:5601
|
http://<aw-server>:5601
|
||||||
```
|
```
|
||||||
|
|
||||||
Routes:
|
## REST API
|
||||||
|
|
||||||
|
Health:
|
||||||
- `GET /healthz`
|
- `GET /healthz`
|
||||||
|
|
||||||
|
CRUD:
|
||||||
- `GET /api/0/dlp/policies`
|
- `GET /api/0/dlp/policies`
|
||||||
- `POST /api/0/dlp/policies`
|
- `POST /api/0/dlp/policies`
|
||||||
- `GET /api/0/dlp/policies/active`
|
|
||||||
- `POST /api/0/dlp/policies/rollback`
|
|
||||||
- `GET /api/0/dlp/policies/{id}`
|
- `GET /api/0/dlp/policies/{id}`
|
||||||
- `PUT /api/0/dlp/policies/{id}`
|
- `PUT /api/0/dlp/policies/{id}`
|
||||||
- `POST /api/0/dlp/policies/{id}/activate`
|
|
||||||
- `DELETE /api/0/dlp/policies/{id}`
|
- `DELETE /api/0/dlp/policies/{id}`
|
||||||
|
|
||||||
## Policy create example
|
Active policy:
|
||||||
|
- `GET /api/0/dlp/policies/active`
|
||||||
|
- `GET /api/0/dlp/policies/active/version`
|
||||||
|
- `POST /api/0/dlp/policies/rollback`
|
||||||
|
|
||||||
```json
|
Approval workflow:
|
||||||
{
|
- `POST /api/0/dlp/policies/{id}/submit` -> `pending_approval`
|
||||||
"name": "base-windows-policy",
|
- `POST /api/0/dlp/policies/{id}/approve` -> `approved`
|
||||||
"description": "Primary DLP policy for pilot endpoints",
|
- `POST /api/0/dlp/policies/{id}/draft` -> `draft`
|
||||||
"activate": true,
|
- `POST /api/0/dlp/policies/{id}/activate` -> deploy (allowed only from `approved`)
|
||||||
"actor": "ansible",
|
|
||||||
"policy": {
|
Audit:
|
||||||
"version": 1,
|
- `GET /api/0/dlp/policies/audit?limit=200`
|
||||||
"defaults": {
|
- `GET /api/0/dlp/policies/{id}/audit?limit=200`
|
||||||
"enabled": true,
|
|
||||||
"cooldownSeconds": 300,
|
Agent push/pull sync:
|
||||||
"action": "alert",
|
- `POST /api/0/dlp/policies/agents/{agent_id}/heartbeat`
|
||||||
"severity": "medium"
|
- `GET /api/0/dlp/policies/agents/{agent_id}/desired`
|
||||||
},
|
|
||||||
"endpoint": {
|
## Workflow Example
|
||||||
"clipboard": [],
|
|
||||||
"usb": [],
|
1. Create draft:
|
||||||
"print": []
|
`POST /api/0/dlp/policies`
|
||||||
}
|
2. Submit:
|
||||||
}
|
`POST /api/0/dlp/policies/{id}/submit`
|
||||||
}
|
3. Approve:
|
||||||
|
`POST /api/0/dlp/policies/{id}/approve`
|
||||||
|
4. Deploy:
|
||||||
|
`POST /api/0/dlp/policies/{id}/activate`
|
||||||
|
|
||||||
|
Every step is written to `policy_audit`.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- Service unit: `aw-dlp-policy-engine.service`
|
||||||
|
- DB path: `AW_DLP_POLICY_ENGINE_DB_PATH`
|
||||||
|
- Port: `AW_DLP_POLICY_ENGINE_PORT` (default `5601`)
|
||||||
|
- Ansible role: `ansible/roles/dlp-policy-engine/tasks/main.yml`
|
||||||
|
|
||||||
|
Recommended server deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Active policy response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"active": true,
|
|
||||||
"policyId": 1,
|
|
||||||
"name": "base-windows-policy",
|
|
||||||
"version": 3,
|
|
||||||
"checksum": "sha256...",
|
|
||||||
"updatedAtUtc": "2026-05-11T12:00:00Z",
|
|
||||||
"policy": {
|
|
||||||
"version": 1,
|
|
||||||
"defaults": {
|
|
||||||
"enabled": true,
|
|
||||||
"cooldownSeconds": 300,
|
|
||||||
"action": "alert",
|
|
||||||
"severity": "medium"
|
|
||||||
},
|
|
||||||
"endpoint": {
|
|
||||||
"clipboard": [],
|
|
||||||
"usb": [],
|
|
||||||
"print": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Notes
|
|
||||||
|
|
||||||
- Service runs as `aw-dlp-policy-engine.service`.
|
|
||||||
- SQLite path is controlled by `AW_DLP_POLICY_ENGINE_DB_PATH`.
|
|
||||||
- Default port is `5601`.
|
|
||||||
- Endpoints should use `server` mode only after `GET /healthz` and `GET /api/0/dlp/policies/active` are confirmed.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user