All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 32s
CI/CD / test-python-backend-compliance (push) Successful in 34s
CI/CD / test-python-document-crawler (push) Successful in 23s
CI/CD / test-python-dsms-gateway (push) Successful in 21s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s
Module 2: Extended Compliance Dashboard with roadmap, module-status, next-actions, snapshots, score-history Module 3: 7 German security document templates (IT-Sicherheitskonzept, Datenschutz, Backup, Logging, Incident-Response, Zugriff, Risikomanagement) Module 4: Compliance Process Manager with CRUD, complete/skip/seed, ~50 seed tasks, 3-tab UI Module 5: Evidence Collector Extended with automated checks, control-mapping, coverage report, 4-tab UI Also includes: canonical control library enhancements (verification method, categories, dedup), control generator improvements, RAG client extensions 52 tests pass, frontend builds clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1073 lines
40 KiB
Python
1073 lines
40 KiB
Python
"""
|
|
FastAPI routes for Compliance Process Manager — recurring compliance tasks.
|
|
|
|
Endpoints:
|
|
GET /process-tasks — list with filters (status, category, frequency, overdue, etc.)
|
|
GET /process-tasks/stats — counts by status, category, due windows
|
|
GET /process-tasks/upcoming — tasks due in next N days
|
|
POST /process-tasks — create task
|
|
GET /process-tasks/{id} — single task
|
|
PUT /process-tasks/{id} — update task
|
|
DELETE /process-tasks/{id} — delete task
|
|
POST /process-tasks/{id}/complete — complete a task (with history + next_due recalc)
|
|
POST /process-tasks/{id}/skip — skip a task (with reason + next_due recalc)
|
|
GET /process-tasks/{id}/history — task history entries
|
|
POST /process-tasks/seed — seed ~50 standard tasks (idempotent)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional, List, Any, Dict
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Header
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from classroom_engine.database import get_db
|
|
from .tenant_utils import get_tenant_id as _get_tenant_id
|
|
from .db_utils import row_to_dict as _row_to_dict
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/process-tasks", tags=["process-tasks"])
|
|
|
|
# =============================================================================
|
|
# Constants
|
|
# =============================================================================
|
|
|
|
VALID_CATEGORIES = {"dsgvo", "nis2", "bsi", "iso27001", "ai_act", "internal"}
|
|
VALID_PRIORITIES = {"critical", "high", "medium", "low"}
|
|
VALID_FREQUENCIES = {"weekly", "monthly", "quarterly", "semi_annual", "yearly", "once"}
|
|
VALID_STATUSES = {"pending", "in_progress", "completed", "overdue", "skipped"}
|
|
|
|
FREQUENCY_DAYS = {
|
|
"weekly": 7,
|
|
"monthly": 30,
|
|
"quarterly": 90,
|
|
"semi_annual": 182,
|
|
"yearly": 365,
|
|
"once": None,
|
|
}
|
|
|
|
# =============================================================================
|
|
# Pydantic Schemas
|
|
# =============================================================================
|
|
|
|
|
|
class ProcessTaskCreate(BaseModel):
|
|
task_code: str
|
|
title: str
|
|
description: Optional[str] = None
|
|
category: str
|
|
priority: str = "medium"
|
|
frequency: str = "yearly"
|
|
assigned_to: Optional[str] = None
|
|
responsible_team: Optional[str] = None
|
|
linked_control_ids: Optional[List] = []
|
|
linked_module: Optional[str] = None
|
|
next_due_date: Optional[str] = None
|
|
due_reminder_days: int = 14
|
|
notes: Optional[str] = None
|
|
tags: Optional[List] = []
|
|
|
|
|
|
class ProcessTaskUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
description: Optional[str] = None
|
|
category: Optional[str] = None
|
|
priority: Optional[str] = None
|
|
frequency: Optional[str] = None
|
|
assigned_to: Optional[str] = None
|
|
responsible_team: Optional[str] = None
|
|
linked_control_ids: Optional[List] = None
|
|
linked_module: Optional[str] = None
|
|
next_due_date: Optional[str] = None
|
|
due_reminder_days: Optional[int] = None
|
|
status: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
tags: Optional[List] = None
|
|
|
|
|
|
class ProcessTaskComplete(BaseModel):
|
|
completed_by: Optional[str] = None
|
|
result: Optional[str] = None
|
|
evidence_id: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
|
|
|
|
class ProcessTaskSkip(BaseModel):
|
|
reason: str
|
|
|
|
|
|
# =============================================================================
|
|
# Routes
|
|
# =============================================================================
|
|
|
|
|
|
@router.get("")
|
|
async def list_tasks(
|
|
status: Optional[str] = Query(None),
|
|
category: Optional[str] = Query(None),
|
|
frequency: Optional[str] = Query(None),
|
|
assigned_to: Optional[str] = Query(None),
|
|
overdue: Optional[bool] = Query(None),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
"""List process tasks with optional filters."""
|
|
|
|
where_clauses = ["tenant_id = :tenant_id"]
|
|
params: Dict[str, Any] = {"tenant_id": tenant_id, "limit": limit, "offset": offset}
|
|
|
|
if status:
|
|
where_clauses.append("status = :status")
|
|
params["status"] = status
|
|
if category:
|
|
where_clauses.append("category = :category")
|
|
params["category"] = category
|
|
if frequency:
|
|
where_clauses.append("frequency = :frequency")
|
|
params["frequency"] = frequency
|
|
if assigned_to:
|
|
where_clauses.append("assigned_to ILIKE :assigned_to")
|
|
params["assigned_to"] = f"%{assigned_to}%"
|
|
if overdue:
|
|
where_clauses.append("next_due_date < CURRENT_DATE AND status NOT IN ('completed','skipped')")
|
|
|
|
where_sql = " AND ".join(where_clauses)
|
|
|
|
total_row = db.execute(
|
|
text(f"SELECT COUNT(*) FROM compliance_process_tasks WHERE {where_sql}"),
|
|
params,
|
|
).fetchone()
|
|
total = total_row[0] if total_row else 0
|
|
|
|
rows = db.execute(
|
|
text(f"""
|
|
SELECT * FROM compliance_process_tasks
|
|
WHERE {where_sql}
|
|
ORDER BY
|
|
CASE priority
|
|
WHEN 'critical' THEN 0
|
|
WHEN 'high' THEN 1
|
|
WHEN 'medium' THEN 2
|
|
ELSE 3
|
|
END,
|
|
next_due_date ASC NULLS LAST,
|
|
created_at DESC
|
|
LIMIT :limit OFFSET :offset
|
|
"""),
|
|
params,
|
|
).fetchall()
|
|
|
|
return {
|
|
"tasks": [_row_to_dict(r) for r in rows],
|
|
"total": total,
|
|
}
|
|
|
|
|
|
@router.get("/stats")
|
|
async def get_stats(
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
"""Return task counts by status, category, and due windows."""
|
|
|
|
row = db.execute(text("""
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
COUNT(*) FILTER (WHERE status = 'pending') AS pending,
|
|
COUNT(*) FILTER (WHERE status = 'in_progress') AS in_progress,
|
|
COUNT(*) FILTER (WHERE status = 'completed') AS completed,
|
|
COUNT(*) FILTER (WHERE status = 'overdue') AS overdue,
|
|
COUNT(*) FILTER (WHERE status = 'skipped') AS skipped,
|
|
COUNT(*) FILTER (WHERE next_due_date < CURRENT_DATE
|
|
AND status NOT IN ('completed','skipped')) AS overdue_count,
|
|
COUNT(*) FILTER (WHERE next_due_date <= CURRENT_DATE + 7
|
|
AND next_due_date >= CURRENT_DATE
|
|
AND status NOT IN ('completed','skipped')) AS due_7_days,
|
|
COUNT(*) FILTER (WHERE next_due_date <= CURRENT_DATE + 14
|
|
AND next_due_date >= CURRENT_DATE
|
|
AND status NOT IN ('completed','skipped')) AS due_14_days,
|
|
COUNT(*) FILTER (WHERE next_due_date <= CURRENT_DATE + 30
|
|
AND next_due_date >= CURRENT_DATE
|
|
AND status NOT IN ('completed','skipped')) AS due_30_days
|
|
FROM compliance_process_tasks
|
|
WHERE tenant_id = :tenant_id
|
|
"""), {"tenant_id": tenant_id}).fetchone()
|
|
|
|
by_status = {}
|
|
by_category = {}
|
|
if row:
|
|
d = dict(row._mapping)
|
|
by_status = {
|
|
"pending": d.get("pending", 0) or 0,
|
|
"in_progress": d.get("in_progress", 0) or 0,
|
|
"completed": d.get("completed", 0) or 0,
|
|
"overdue": d.get("overdue", 0) or 0,
|
|
"skipped": d.get("skipped", 0) or 0,
|
|
}
|
|
else:
|
|
d = {}
|
|
|
|
# Category counts
|
|
cat_rows = db.execute(text("""
|
|
SELECT category, COUNT(*) AS cnt
|
|
FROM compliance_process_tasks
|
|
WHERE tenant_id = :tenant_id
|
|
GROUP BY category
|
|
"""), {"tenant_id": tenant_id}).fetchall()
|
|
by_category = {r._mapping["category"]: r._mapping["cnt"] for r in cat_rows}
|
|
|
|
return {
|
|
"total": (d.get("total", 0) or 0),
|
|
"by_status": by_status,
|
|
"by_category": by_category,
|
|
"overdue_count": (d.get("overdue_count", 0) or 0),
|
|
"due_7_days": (d.get("due_7_days", 0) or 0),
|
|
"due_14_days": (d.get("due_14_days", 0) or 0),
|
|
"due_30_days": (d.get("due_30_days", 0) or 0),
|
|
}
|
|
|
|
|
|
@router.get("/upcoming")
|
|
async def get_upcoming(
|
|
days: int = Query(30, ge=1, le=365),
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
"""Tasks due in next N days."""
|
|
|
|
rows = db.execute(text("""
|
|
SELECT * FROM compliance_process_tasks
|
|
WHERE tenant_id = :tenant_id
|
|
AND next_due_date IS NOT NULL
|
|
AND next_due_date <= CURRENT_DATE + :days
|
|
AND next_due_date >= CURRENT_DATE
|
|
AND status NOT IN ('completed','skipped')
|
|
ORDER BY next_due_date ASC
|
|
"""), {"tenant_id": tenant_id, "days": days}).fetchall()
|
|
|
|
return {"tasks": [_row_to_dict(r) for r in rows]}
|
|
|
|
|
|
@router.post("", status_code=201)
|
|
async def create_task(
|
|
payload: ProcessTaskCreate,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
x_user_id: Optional[str] = Header(None),
|
|
):
|
|
"""Create a new process task."""
|
|
logger.info("create_task user_id=%s tenant_id=%s code=%s", x_user_id, tenant_id, payload.task_code)
|
|
|
|
if payload.category not in VALID_CATEGORIES:
|
|
raise HTTPException(status_code=400, detail=f"Invalid category. Must be one of: {', '.join(sorted(VALID_CATEGORIES))}")
|
|
if payload.priority not in VALID_PRIORITIES:
|
|
raise HTTPException(status_code=400, detail=f"Invalid priority. Must be one of: {', '.join(sorted(VALID_PRIORITIES))}")
|
|
if payload.frequency not in VALID_FREQUENCIES:
|
|
raise HTTPException(status_code=400, detail=f"Invalid frequency. Must be one of: {', '.join(sorted(VALID_FREQUENCIES))}")
|
|
|
|
row = db.execute(text("""
|
|
INSERT INTO compliance_process_tasks
|
|
(tenant_id, task_code, title, description, category, priority, frequency,
|
|
assigned_to, responsible_team, linked_control_ids, linked_module,
|
|
next_due_date, due_reminder_days, notes, tags)
|
|
VALUES
|
|
(:tenant_id, :task_code, :title, :description, :category, :priority, :frequency,
|
|
:assigned_to, :responsible_team, CAST(:linked_control_ids AS jsonb), :linked_module,
|
|
:next_due_date, :due_reminder_days, :notes, CAST(:tags AS jsonb))
|
|
RETURNING *
|
|
"""), {
|
|
"tenant_id": tenant_id,
|
|
"task_code": payload.task_code,
|
|
"title": payload.title,
|
|
"description": payload.description,
|
|
"category": payload.category,
|
|
"priority": payload.priority,
|
|
"frequency": payload.frequency,
|
|
"assigned_to": payload.assigned_to,
|
|
"responsible_team": payload.responsible_team,
|
|
"linked_control_ids": json.dumps(payload.linked_control_ids or []),
|
|
"linked_module": payload.linked_module,
|
|
"next_due_date": payload.next_due_date,
|
|
"due_reminder_days": payload.due_reminder_days,
|
|
"notes": payload.notes,
|
|
"tags": json.dumps(payload.tags or []),
|
|
}).fetchone()
|
|
db.commit()
|
|
return _row_to_dict(row)
|
|
|
|
|
|
@router.get("/{task_id}")
|
|
async def get_task(
|
|
task_id: str,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
"""Get single process task."""
|
|
row = db.execute(text("""
|
|
SELECT * FROM compliance_process_tasks
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""), {"id": task_id, "tenant_id": tenant_id}).fetchone()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
return _row_to_dict(row)
|
|
|
|
|
|
@router.put("/{task_id}")
|
|
async def update_task(
|
|
task_id: str,
|
|
payload: ProcessTaskUpdate,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
x_user_id: Optional[str] = Header(None),
|
|
):
|
|
"""Update a process task."""
|
|
logger.info("update_task user_id=%s tenant_id=%s id=%s", x_user_id, tenant_id, task_id)
|
|
|
|
updates: Dict[str, Any] = {"id": task_id, "tenant_id": tenant_id, "updated_at": datetime.utcnow()}
|
|
set_clauses = ["updated_at = :updated_at"]
|
|
|
|
data = payload.model_dump(exclude_unset=True)
|
|
|
|
# Validate enum fields if provided
|
|
if "category" in data and data["category"] not in VALID_CATEGORIES:
|
|
raise HTTPException(status_code=400, detail=f"Invalid category. Must be one of: {', '.join(sorted(VALID_CATEGORIES))}")
|
|
if "priority" in data and data["priority"] not in VALID_PRIORITIES:
|
|
raise HTTPException(status_code=400, detail=f"Invalid priority. Must be one of: {', '.join(sorted(VALID_PRIORITIES))}")
|
|
if "frequency" in data and data["frequency"] not in VALID_FREQUENCIES:
|
|
raise HTTPException(status_code=400, detail=f"Invalid frequency. Must be one of: {', '.join(sorted(VALID_FREQUENCIES))}")
|
|
if "status" in data and data["status"] not in VALID_STATUSES:
|
|
raise HTTPException(status_code=400, detail=f"Invalid status. Must be one of: {', '.join(sorted(VALID_STATUSES))}")
|
|
|
|
for field, value in data.items():
|
|
if field in ("linked_control_ids", "tags", "follow_up_actions"):
|
|
updates[field] = json.dumps(value or [])
|
|
set_clauses.append(f"{field} = CAST(:{field} AS jsonb)")
|
|
else:
|
|
updates[field] = value
|
|
set_clauses.append(f"{field} = :{field}")
|
|
|
|
if len(set_clauses) == 1:
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
|
|
|
row = db.execute(text(f"""
|
|
UPDATE compliance_process_tasks
|
|
SET {', '.join(set_clauses)}
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
RETURNING *
|
|
"""), updates).fetchone()
|
|
db.commit()
|
|
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
return _row_to_dict(row)
|
|
|
|
|
|
@router.delete("/{task_id}", status_code=204)
|
|
async def delete_task(
|
|
task_id: str,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
x_user_id: Optional[str] = Header(None),
|
|
):
|
|
"""Delete a process task."""
|
|
logger.info("delete_task user_id=%s tenant_id=%s id=%s", x_user_id, tenant_id, task_id)
|
|
result = db.execute(text("""
|
|
DELETE FROM compliance_process_tasks
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""), {"id": task_id, "tenant_id": tenant_id})
|
|
db.commit()
|
|
if result.rowcount == 0:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
|
|
@router.post("/{task_id}/complete")
|
|
async def complete_task(
|
|
task_id: str,
|
|
payload: ProcessTaskComplete,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
x_user_id: Optional[str] = Header(None),
|
|
):
|
|
"""Complete a task: insert history, update task, calculate next due date."""
|
|
logger.info("complete_task user_id=%s tenant_id=%s id=%s", x_user_id, tenant_id, task_id)
|
|
|
|
# Fetch current task
|
|
task_row = db.execute(text("""
|
|
SELECT * FROM compliance_process_tasks
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""), {"id": task_id, "tenant_id": tenant_id}).fetchone()
|
|
|
|
if not task_row:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
task = dict(task_row._mapping)
|
|
|
|
# Insert history entry
|
|
db.execute(text("""
|
|
INSERT INTO compliance_process_task_history
|
|
(task_id, completed_by, completed_at, result, evidence_id, notes, status)
|
|
VALUES
|
|
(:task_id, :completed_by, NOW(), :result, :evidence_id, :notes, 'completed')
|
|
"""), {
|
|
"task_id": task_id,
|
|
"completed_by": payload.completed_by,
|
|
"result": payload.result,
|
|
"evidence_id": payload.evidence_id,
|
|
"notes": payload.notes,
|
|
})
|
|
|
|
# Calculate next due date
|
|
freq = task.get("frequency", "yearly")
|
|
days_delta = FREQUENCY_DAYS.get(freq)
|
|
|
|
if days_delta is not None:
|
|
# Recurring task — set next due and reset to pending
|
|
row = db.execute(text("""
|
|
UPDATE compliance_process_tasks
|
|
SET last_completed_at = NOW(),
|
|
status = 'pending',
|
|
completion_date = NOW(),
|
|
completion_result = :result,
|
|
completion_evidence_id = :evidence_id,
|
|
next_due_date = CURRENT_DATE + :days_delta,
|
|
updated_at = NOW()
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
RETURNING *
|
|
"""), {
|
|
"id": task_id,
|
|
"tenant_id": tenant_id,
|
|
"result": payload.result,
|
|
"evidence_id": payload.evidence_id,
|
|
"days_delta": days_delta,
|
|
}).fetchone()
|
|
else:
|
|
# One-time task — mark completed, no next due
|
|
row = db.execute(text("""
|
|
UPDATE compliance_process_tasks
|
|
SET last_completed_at = NOW(),
|
|
status = 'completed',
|
|
completion_date = NOW(),
|
|
completion_result = :result,
|
|
completion_evidence_id = :evidence_id,
|
|
next_due_date = NULL,
|
|
updated_at = NOW()
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
RETURNING *
|
|
"""), {
|
|
"id": task_id,
|
|
"tenant_id": tenant_id,
|
|
"result": payload.result,
|
|
"evidence_id": payload.evidence_id,
|
|
}).fetchone()
|
|
|
|
db.commit()
|
|
return _row_to_dict(row)
|
|
|
|
|
|
@router.post("/{task_id}/skip")
|
|
async def skip_task(
|
|
task_id: str,
|
|
payload: ProcessTaskSkip,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
x_user_id: Optional[str] = Header(None),
|
|
):
|
|
"""Skip a task with reason: insert history, calculate next due date."""
|
|
logger.info("skip_task user_id=%s tenant_id=%s id=%s", x_user_id, tenant_id, task_id)
|
|
|
|
# Fetch current task
|
|
task_row = db.execute(text("""
|
|
SELECT * FROM compliance_process_tasks
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""), {"id": task_id, "tenant_id": tenant_id}).fetchone()
|
|
|
|
if not task_row:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
task = dict(task_row._mapping)
|
|
|
|
# Insert history entry
|
|
db.execute(text("""
|
|
INSERT INTO compliance_process_task_history
|
|
(task_id, completed_by, completed_at, result, notes, status)
|
|
VALUES
|
|
(:task_id, :completed_by, NOW(), :reason, :reason, 'skipped')
|
|
"""), {
|
|
"task_id": task_id,
|
|
"completed_by": x_user_id,
|
|
"reason": payload.reason,
|
|
})
|
|
|
|
# Calculate next due date
|
|
freq = task.get("frequency", "yearly")
|
|
days_delta = FREQUENCY_DAYS.get(freq)
|
|
|
|
if days_delta is not None:
|
|
row = db.execute(text("""
|
|
UPDATE compliance_process_tasks
|
|
SET status = 'pending',
|
|
next_due_date = CURRENT_DATE + :days_delta,
|
|
updated_at = NOW()
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
RETURNING *
|
|
"""), {
|
|
"id": task_id,
|
|
"tenant_id": tenant_id,
|
|
"days_delta": days_delta,
|
|
}).fetchone()
|
|
else:
|
|
row = db.execute(text("""
|
|
UPDATE compliance_process_tasks
|
|
SET status = 'skipped',
|
|
next_due_date = NULL,
|
|
updated_at = NOW()
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
RETURNING *
|
|
"""), {
|
|
"id": task_id,
|
|
"tenant_id": tenant_id,
|
|
}).fetchone()
|
|
|
|
db.commit()
|
|
return _row_to_dict(row)
|
|
|
|
|
|
@router.get("/{task_id}/history")
|
|
async def get_task_history(
|
|
task_id: str,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
):
|
|
"""Return history entries for a task."""
|
|
# Verify task belongs to tenant
|
|
task_row = db.execute(text("""
|
|
SELECT id FROM compliance_process_tasks
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""), {"id": task_id, "tenant_id": tenant_id}).fetchone()
|
|
|
|
if not task_row:
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
rows = db.execute(text("""
|
|
SELECT * FROM compliance_process_task_history
|
|
WHERE task_id = :task_id
|
|
ORDER BY completed_at DESC
|
|
"""), {"task_id": task_id}).fetchall()
|
|
|
|
return {"history": [_row_to_dict(r) for r in rows]}
|
|
|
|
|
|
@router.post("/seed")
|
|
async def seed_tasks(
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant_id),
|
|
x_user_id: Optional[str] = Header(None),
|
|
):
|
|
"""Seed ~50 standard compliance tasks. Idempotent via ON CONFLICT."""
|
|
logger.info("seed_tasks user_id=%s tenant_id=%s", x_user_id, tenant_id)
|
|
|
|
seeds = _get_seed_tasks()
|
|
inserted = 0
|
|
|
|
for s in seeds:
|
|
result = db.execute(text("""
|
|
INSERT INTO compliance_process_tasks
|
|
(tenant_id, task_code, title, description, category, priority, frequency,
|
|
linked_module, is_seed, next_due_date)
|
|
VALUES
|
|
(:tenant_id, :task_code, :title, :description, :category, :priority, :frequency,
|
|
:linked_module, TRUE, CURRENT_DATE + :initial_days)
|
|
ON CONFLICT (tenant_id, project_id, task_code) DO NOTHING
|
|
"""), {
|
|
"tenant_id": tenant_id,
|
|
"task_code": s["task_code"],
|
|
"title": s["title"],
|
|
"description": s["description"],
|
|
"category": s["category"],
|
|
"priority": s["priority"],
|
|
"frequency": s["frequency"],
|
|
"linked_module": s.get("linked_module"),
|
|
"initial_days": FREQUENCY_DAYS.get(s["frequency"], 365) or 365,
|
|
})
|
|
if result.rowcount > 0:
|
|
inserted += 1
|
|
|
|
db.commit()
|
|
|
|
return {"seeded": inserted, "total_available": len(seeds)}
|
|
|
|
|
|
# =============================================================================
|
|
# Seed Data
|
|
# =============================================================================
|
|
|
|
def _get_seed_tasks() -> List[Dict[str, Any]]:
|
|
"""Return ~50 standard compliance tasks for seeding."""
|
|
return [
|
|
# ─── DSGVO (~15) ─────────────────────────────────────────────
|
|
{
|
|
"task_code": "DSGVO-VVT-REVIEW",
|
|
"title": "VVT-Review und Aktualisierung",
|
|
"description": "Jaehrliche Ueberpruefung und Aktualisierung des Verzeichnisses von Verarbeitungstaetigkeiten gemaess Art. 30 DSGVO.",
|
|
"category": "dsgvo",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": "vvt",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-TOM-REVIEW",
|
|
"title": "TOM-Review und Aktualisierung",
|
|
"description": "Jaehrliche Ueberpruefung der technischen und organisatorischen Massnahmen gemaess Art. 32 DSGVO.",
|
|
"category": "dsgvo",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": "tom",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-LOESCHFRISTEN",
|
|
"title": "Loeschfristen-Pruefung",
|
|
"description": "Quartalspruefung aller Loeschfristen und Durchfuehrung faelliger Loeschungen.",
|
|
"category": "dsgvo",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": "loeschfristen",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-DSB-BERICHT",
|
|
"title": "DSB-Taetigkeitsbericht",
|
|
"description": "Jaehrlicher Taetigkeitsbericht des Datenschutzbeauftragten an die Geschaeftsfuehrung.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "DSGVO-DSFA-UPDATE",
|
|
"title": "DSFA-Aktualisierung",
|
|
"description": "Jaehrliche Ueberpruefung und Aktualisierung der Datenschutz-Folgenabschaetzungen.",
|
|
"category": "dsgvo",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": "dsfa",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-SCHULUNG",
|
|
"title": "Datenschutz-Schulung Mitarbeiter",
|
|
"description": "Jaehrliche Datenschutz-Schulung fuer alle Mitarbeiter mit Nachweis.",
|
|
"category": "dsgvo",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": "training",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-BETROFFENENRECHTE",
|
|
"title": "Betroffenenrechte-Prozess testen",
|
|
"description": "Quartalstest der Prozesse fuer Auskunft, Berichtigung, Loeschung und Datenportabilitaet.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "quarterly",
|
|
"linked_module": "dsr",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-AV-VERTRAEGE",
|
|
"title": "AV-Vertraege pruefen",
|
|
"description": "Jaehrliche Pruefung aller Auftragsverarbeitungsvertraege auf Aktualitaet und Vollstaendigkeit.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": "vendor-compliance",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-DATENPANNE-TEST",
|
|
"title": "Datenpannen-Meldeprozess testen",
|
|
"description": "Halbjahrestest des Meldeprozesses fuer Datenschutzverletzungen gemaess Art. 33/34 DSGVO.",
|
|
"category": "dsgvo",
|
|
"priority": "high",
|
|
"frequency": "semi_annual",
|
|
"linked_module": "incident-response",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-COOKIE-PRUEFUNG",
|
|
"title": "Cookie-Banner und Consent pruefen",
|
|
"description": "Quartalspruefung des Cookie-Banners und der Einwilligungsmechanismen.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "quarterly",
|
|
"linked_module": "consent",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-DSE-REVIEW",
|
|
"title": "Datenschutzerklaerung Review",
|
|
"description": "Halbjaehrliche Ueberpruefung der Datenschutzerklaerung auf Aktualitaet.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "semi_annual",
|
|
"linked_module": "document-generator",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-EINWILLIGUNG-REVIEW",
|
|
"title": "Einwilligungen Review",
|
|
"description": "Quartalspruefung der eingeholten Einwilligungen auf Gueltigkeit und Widerrufbarkeit.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "quarterly",
|
|
"linked_module": "consent",
|
|
},
|
|
{
|
|
"task_code": "DSGVO-DRITTLAND",
|
|
"title": "Drittlandsuebermittlung pruefen",
|
|
"description": "Jaehrliche Pruefung aller Datenuebermittlungen in Drittlaender und deren Rechtsgrundlage.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "DSGVO-VERPFLICHTUNG",
|
|
"title": "Mitarbeiter-Verpflichtung Datengeheimnis",
|
|
"description": "Jaehrliche Pruefung, ob alle Mitarbeiter auf das Datengeheimnis verpflichtet wurden.",
|
|
"category": "dsgvo",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "DSGVO-DATENKATEGORIEN",
|
|
"title": "Datenkategorien-Review",
|
|
"description": "Jaehrliche Ueberpruefung der erfassten Datenkategorien und deren Zuordnung.",
|
|
"category": "dsgvo",
|
|
"priority": "low",
|
|
"frequency": "yearly",
|
|
"linked_module": "vvt",
|
|
},
|
|
|
|
# ─── NIS2 (~10) ──────────────────────────────────────────────
|
|
{
|
|
"task_code": "NIS2-RISIKOBEWERTUNG",
|
|
"title": "NIS2 Risikobewertung",
|
|
"description": "Jaehrliche umfassende Risikobewertung der Netz- und Informationssicherheit.",
|
|
"category": "nis2",
|
|
"priority": "critical",
|
|
"frequency": "yearly",
|
|
"linked_module": "risks",
|
|
},
|
|
{
|
|
"task_code": "NIS2-LIEFERKETTE",
|
|
"title": "Lieferketten-Sicherheitspruefung",
|
|
"description": "Jaehrliche Ueberpruefung der Sicherheitsmassnahmen bei Zulieferern und Dienstleistern.",
|
|
"category": "nis2",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": "vendor-compliance",
|
|
},
|
|
{
|
|
"task_code": "NIS2-INCIDENT-UEBUNG",
|
|
"title": "Incident-Response-Uebung",
|
|
"description": "Jaehrliche Uebung des Incident-Response-Prozesses mit Dokumentation.",
|
|
"category": "nis2",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": "incident-response",
|
|
},
|
|
{
|
|
"task_code": "NIS2-VULNSCAN",
|
|
"title": "Vulnerability-Scan",
|
|
"description": "Monatlicher automatisierter Vulnerability-Scan aller IT-Systeme.",
|
|
"category": "nis2",
|
|
"priority": "critical",
|
|
"frequency": "monthly",
|
|
"linked_module": "security-backlog",
|
|
},
|
|
{
|
|
"task_code": "NIS2-PATCHMGMT",
|
|
"title": "Patch-Management-Review",
|
|
"description": "Monatliche Pruefung des Patch-Status aller Systeme.",
|
|
"category": "nis2",
|
|
"priority": "high",
|
|
"frequency": "monthly",
|
|
"linked_module": "security-backlog",
|
|
},
|
|
{
|
|
"task_code": "NIS2-BCM-TEST",
|
|
"title": "Business-Continuity-Test",
|
|
"description": "Jaehrlicher Test des Business-Continuity-Plans.",
|
|
"category": "nis2",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "NIS2-ZUGANGSKONTROLLE",
|
|
"title": "Zugangskontrollen-Review",
|
|
"description": "Quartalspruefung aller Zugangsberechtigungen und Accounts.",
|
|
"category": "nis2",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "NIS2-KRYPTOGRAFIE",
|
|
"title": "Kryptografie-Review",
|
|
"description": "Jaehrliche Ueberpruefung der eingesetzten kryptografischen Verfahren.",
|
|
"category": "nis2",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "NIS2-MELDEPFLICHT",
|
|
"title": "Meldepflicht-Prozess testen",
|
|
"description": "Halbjaehrlicher Test des NIS2-Meldeprozesses (24h/72h Fristen).",
|
|
"category": "nis2",
|
|
"priority": "high",
|
|
"frequency": "semi_annual",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "NIS2-NETZSEGMENT",
|
|
"title": "Netzwerksegmentierung-Review",
|
|
"description": "Jaehrliche Ueberpruefung der Netzwerksegmentierung und Firewall-Regeln.",
|
|
"category": "nis2",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
|
|
# ─── BSI (~10) ───────────────────────────────────────────────
|
|
{
|
|
"task_code": "BSI-GRUNDSCHUTZ",
|
|
"title": "IT-Grundschutz-Check",
|
|
"description": "Jaehrlicher IT-Grundschutz-Check nach BSI-Standard 200-2.",
|
|
"category": "bsi",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-BAUSTEIN",
|
|
"title": "Baustein-Review",
|
|
"description": "Quartalspruefung der implementierten BSI-Bausteine.",
|
|
"category": "bsi",
|
|
"priority": "medium",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-NOTFALLPLAN",
|
|
"title": "Notfallplan-Test",
|
|
"description": "Jaehrlicher Test des IT-Notfallplans mit Uebungsszenario.",
|
|
"category": "bsi",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": "notfallplan",
|
|
},
|
|
{
|
|
"task_code": "BSI-BACKUP-TEST",
|
|
"title": "Backup-Restore-Test",
|
|
"description": "Quartalstest der Backup-Wiederherstellung fuer kritische Systeme.",
|
|
"category": "bsi",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-FIREWALL",
|
|
"title": "Firewall-Rule-Review",
|
|
"description": "Quartalspruefung aller Firewall-Regeln auf Aktualitaet und Minimalitaet.",
|
|
"category": "bsi",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-LOGGING",
|
|
"title": "Logging-Review",
|
|
"description": "Monatliche Pruefung der Log-Einstellungen und Log-Auswertung.",
|
|
"category": "bsi",
|
|
"priority": "medium",
|
|
"frequency": "monthly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-HAERTUNG",
|
|
"title": "Haertungs-Check",
|
|
"description": "Quartalspruefung der System-Haertung nach BSI-Empfehlungen.",
|
|
"category": "bsi",
|
|
"priority": "medium",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-ZERTIFIKATE",
|
|
"title": "Zertifikats-Erneuerung pruefen",
|
|
"description": "Monatliche Pruefung ablaufender TLS/SSL-Zertifikate.",
|
|
"category": "bsi",
|
|
"priority": "high",
|
|
"frequency": "monthly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-RAUMSICHERHEIT",
|
|
"title": "Raumsicherheit-Pruefung",
|
|
"description": "Jaehrliche Pruefung der physischen Sicherheit der Serverraeume.",
|
|
"category": "bsi",
|
|
"priority": "low",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "BSI-MEDIEN",
|
|
"title": "Medienentsorgung-Kontrolle",
|
|
"description": "Quartalskontrolle der ordnungsgemaessen Entsorgung von Datentraegern.",
|
|
"category": "bsi",
|
|
"priority": "medium",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
|
|
# ─── ISO 27001 (~8) ──────────────────────────────────────────
|
|
{
|
|
"task_code": "ISO-MGMT-REVIEW",
|
|
"title": "Management-Review",
|
|
"description": "Jaehrliches Management-Review des ISMS gemaess ISO 27001 Kap. 9.3.",
|
|
"category": "iso27001",
|
|
"priority": "critical",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "ISO-INT-AUDIT",
|
|
"title": "Internes ISMS-Audit",
|
|
"description": "Jaehrliches internes Audit des ISMS gemaess ISO 27001 Kap. 9.2.",
|
|
"category": "iso27001",
|
|
"priority": "critical",
|
|
"frequency": "yearly",
|
|
"linked_module": "audit",
|
|
},
|
|
{
|
|
"task_code": "ISO-KORREKTUR",
|
|
"title": "Korrekturmassnahmen-Review",
|
|
"description": "Quartalspruefung offener Korrekturmassnahmen aus Audits.",
|
|
"category": "iso27001",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "ISO-RISK-OWNER",
|
|
"title": "Risiko-Owner-Review",
|
|
"description": "Quartalspruefung der Risiko-Zuordnungen und Verantwortlichkeiten.",
|
|
"category": "iso27001",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": "risks",
|
|
},
|
|
{
|
|
"task_code": "ISO-KENNZAHLEN",
|
|
"title": "Kennzahlen-Erhebung",
|
|
"description": "Monatliche Erhebung der ISMS-Kennzahlen und KPIs.",
|
|
"category": "iso27001",
|
|
"priority": "medium",
|
|
"frequency": "monthly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "ISO-SCOPE",
|
|
"title": "ISMS-Scope-Review",
|
|
"description": "Jaehrliche Ueberpruefung des ISMS-Geltungsbereichs.",
|
|
"category": "iso27001",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": "compliance-scope",
|
|
},
|
|
{
|
|
"task_code": "ISO-POLICY",
|
|
"title": "Policy-Aktualisierung",
|
|
"description": "Jaehrliche Ueberpruefung und Aktualisierung aller ISMS-Policies.",
|
|
"category": "iso27001",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": "document-generator",
|
|
},
|
|
{
|
|
"task_code": "ISO-ZERTIFIZIERUNG",
|
|
"title": "Zertifizierungs-Vorbereitung",
|
|
"description": "Jaehrliche Vorbereitung auf externes Zertifizierungsaudit.",
|
|
"category": "iso27001",
|
|
"priority": "high",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
|
|
# ─── AI Act (~7) ─────────────────────────────────────────────
|
|
{
|
|
"task_code": "AIACT-INVENTAR",
|
|
"title": "KI-Inventar aktualisieren",
|
|
"description": "Quartalsaktualisierung des Inventars aller KI-Systeme im Einsatz.",
|
|
"category": "ai_act",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": "ai-act",
|
|
},
|
|
{
|
|
"task_code": "AIACT-RISIKO",
|
|
"title": "KI-Risikobewertung",
|
|
"description": "Jaehrliche Risikobewertung aller KI-Systeme gemaess EU AI Act.",
|
|
"category": "ai_act",
|
|
"priority": "critical",
|
|
"frequency": "yearly",
|
|
"linked_module": "ai-act",
|
|
},
|
|
{
|
|
"task_code": "AIACT-TRANSPARENZ",
|
|
"title": "Transparenzpflicht-Check",
|
|
"description": "Quartalspruefung der Transparenzpflichten fuer KI-Systeme.",
|
|
"category": "ai_act",
|
|
"priority": "high",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "AIACT-OVERSIGHT",
|
|
"title": "Human-Oversight-Validierung",
|
|
"description": "Halbjaehrliche Validierung der menschlichen Aufsicht ueber KI-Systeme.",
|
|
"category": "ai_act",
|
|
"priority": "high",
|
|
"frequency": "semi_annual",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "AIACT-BIAS",
|
|
"title": "Bias-Monitoring",
|
|
"description": "Quartalspruefung auf Bias und Diskriminierung in KI-Ausgaben.",
|
|
"category": "ai_act",
|
|
"priority": "medium",
|
|
"frequency": "quarterly",
|
|
"linked_module": None,
|
|
},
|
|
{
|
|
"task_code": "AIACT-SCHULUNG",
|
|
"title": "KI-Schulung Mitarbeiter",
|
|
"description": "Jaehrliche Schulung aller Mitarbeiter zu KI-Kompetenz und AI Act.",
|
|
"category": "ai_act",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": "training",
|
|
},
|
|
{
|
|
"task_code": "AIACT-DOKU",
|
|
"title": "KI-Dokumentations-Review",
|
|
"description": "Jaehrliche Ueberpruefung der technischen Dokumentation aller KI-Systeme.",
|
|
"category": "ai_act",
|
|
"priority": "medium",
|
|
"frequency": "yearly",
|
|
"linked_module": None,
|
|
},
|
|
]
|