+
router.push(`/sdk/audit-report/${session.id}`)}>
diff --git a/admin-compliance/app/(sdk)/sdk/controls/page.tsx b/admin-compliance/app/(sdk)/sdk/controls/page.tsx
index 61e4129..2f5d5df 100644
--- a/admin-compliance/app/(sdk)/sdk/controls/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/controls/page.tsx
@@ -1,6 +1,7 @@
'use client'
import React, { useState, useEffect } from 'react'
+import { useRouter } from 'next/navigation'
import { useSDK, Control as SDKControl, ControlType, ImplementationStatus } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
@@ -28,6 +29,7 @@ interface DisplayControl {
displayStatus: DisplayStatus
effectivenessPercent: number
linkedRequirements: string[]
+ linkedEvidence: { id: string; title: string; status: string }[]
lastReview: Date
}
@@ -153,10 +155,12 @@ function ControlCard({
control,
onStatusChange,
onEffectivenessChange,
+ onLinkEvidence,
}: {
control: DisplayControl
onStatusChange: (status: ImplementationStatus) => void
onEffectivenessChange: (effectivenessPercent: number) => void
+ onLinkEvidence: () => void
}) {
const [showEffectivenessSlider, setShowEffectivenessSlider] = useState(false)
@@ -279,6 +283,33 @@ function ControlCard({
{statusLabels[control.displayStatus]}
+
+ {/* Linked Evidence */}
+ {control.linkedEvidence.length > 0 && (
+
+
Nachweise:
+
+ {control.linkedEvidence.map(ev => (
+
+ {ev.title}
+
+ ))}
+
+
+ )}
+
+
+
+
)
}
@@ -400,6 +431,7 @@ function LoadingSkeleton() {
export default function ControlsPage() {
const { state, dispatch } = useSDK()
+ const router = useRouter()
const [filter, setFilter] = useState
('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
@@ -407,6 +439,33 @@ export default function ControlsPage() {
// Track effectiveness locally as it's not in the SDK state type
const [effectivenessMap, setEffectivenessMap] = useState>({})
+ // Track linked evidence per control
+ const [evidenceMap, setEvidenceMap] = useState>({})
+
+ const fetchEvidenceForControls = async (controlIds: string[]) => {
+ try {
+ const res = await fetch('/api/sdk/v1/compliance/evidence')
+ if (res.ok) {
+ const data = await res.json()
+ const allEvidence = data.evidence || data
+ if (Array.isArray(allEvidence)) {
+ const map: Record = {}
+ for (const ev of allEvidence) {
+ const ctrlId = ev.control_id || ''
+ if (!map[ctrlId]) map[ctrlId] = []
+ map[ctrlId].push({
+ id: ev.id,
+ title: ev.title || ev.name || 'Nachweis',
+ status: ev.status || 'pending',
+ })
+ }
+ setEvidenceMap(map)
+ }
+ }
+ } catch {
+ // Silently fail
+ }
+ }
// Fetch controls from backend on mount
useEffect(() => {
@@ -432,6 +491,8 @@ export default function ControlsPage() {
}))
dispatch({ type: 'SET_STATE', payload: { controls: mapped } })
setError(null)
+ // Fetch evidence for all controls
+ fetchEvidenceForControls(mapped.map(c => c.id))
return
}
}
@@ -494,6 +555,7 @@ export default function ControlsPage() {
displayStatus: mapStatusToDisplay(ctrl.implementationStatus),
effectivenessPercent,
linkedRequirements: template?.linkedRequirements || [],
+ linkedEvidence: evidenceMap[ctrl.id] || [],
lastReview: new Date(),
}
})
@@ -673,6 +735,7 @@ export default function ControlsPage() {
control={control}
onStatusChange={(status) => handleStatusChange(control.id, status)}
onEffectivenessChange={(effectiveness) => handleEffectivenessChange(control.id, effectiveness)}
+ onLinkEvidence={() => router.push(`/sdk/evidence?control_id=${control.id}`)}
/>
))}
diff --git a/admin-compliance/app/(sdk)/sdk/evidence/page.tsx b/admin-compliance/app/(sdk)/sdk/evidence/page.tsx
index 2be256b..a66844f 100644
--- a/admin-compliance/app/(sdk)/sdk/evidence/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/evidence/page.tsx
@@ -310,15 +310,19 @@ export default function EvidencePage() {
const [error, setError] = useState
(null)
const [uploading, setUploading] = useState(false)
const fileInputRef = useRef(null)
+ const [page, setPage] = useState(1)
+ const [pageSize] = useState(20)
+ const [total, setTotal] = useState(0)
- // Fetch evidence from backend on mount
+ // Fetch evidence from backend on mount and when page changes
useEffect(() => {
const fetchEvidence = async () => {
try {
setLoading(true)
- const res = await fetch('/api/sdk/v1/compliance/evidence')
+ const res = await fetch(`/api/sdk/v1/compliance/evidence?page=${page}&limit=${pageSize}`)
if (res.ok) {
const data = await res.json()
+ if (data.total !== undefined) setTotal(data.total)
const backendEvidence = data.evidence || data
if (Array.isArray(backendEvidence) && backendEvidence.length > 0) {
const mapped: SDKEvidence[] = backendEvidence.map((e: Record) => ({
@@ -380,7 +384,7 @@ export default function EvidencePage() {
}
fetchEvidence()
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
+ }, [page, pageSize]) // eslint-disable-line react-hooks/exhaustive-deps
// Convert SDK evidence to display evidence
const displayEvidence: DisplayEvidence[] = state.evidence.map(ev => {
@@ -638,6 +642,34 @@ export default function EvidencePage() {
)}
+ {/* Pagination */}
+ {!loading && total > pageSize && (
+
+
+ Zeige {((page - 1) * pageSize) + 1}–{Math.min(page * pageSize, total)} von {total} Nachweisen
+
+
+
+
+ Seite {page} von {Math.ceil(total / pageSize)}
+
+
+
+
+ )}
+
{!loading && filteredEvidence.length === 0 && state.controls.length > 0 && (
diff --git a/admin-compliance/app/(sdk)/sdk/requirements/page.tsx b/admin-compliance/app/(sdk)/sdk/requirements/page.tsx
index d4a27f4..ad172b6 100644
--- a/admin-compliance/app/(sdk)/sdk/requirements/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/requirements/page.tsx
@@ -257,12 +257,14 @@ function AddRequirementForm({
function RequirementCard({
requirement,
onStatusChange,
+ onDelete,
expanded,
onToggleDetails,
linkedControls,
}: {
requirement: DisplayRequirement
onStatusChange: (status: RequirementStatus) => void
+ onDelete: () => void
expanded: boolean
onToggleDetails: () => void
linkedControls: { id: string; name: string }[]
@@ -345,19 +347,27 @@ function RequirementCard({
Keine Kontrollen zugeordnet
)}
-
-
Status-Historie
-
-
- {requirement.status === 'NOT_STARTED' ? 'Nicht begonnen' :
- requirement.status === 'IN_PROGRESS' ? 'In Bearbeitung' :
- requirement.status === 'IMPLEMENTED' ? 'Implementiert' : 'Verifiziert'}
-
+
+
+
Status-Historie
+
+
+ {requirement.status === 'NOT_STARTED' ? 'Nicht begonnen' :
+ requirement.status === 'IN_PROGRESS' ? 'In Bearbeitung' :
+ requirement.status === 'IMPLEMENTED' ? 'Implementiert' : 'Verifiziert'}
+
+
+
)}
@@ -493,6 +503,7 @@ export default function RequirementsPage() {
const nonCompliantCount = displayRequirements.filter(r => r.displayStatus === 'non-compliant').length
const handleStatusChange = async (requirementId: string, status: RequirementStatus) => {
+ const previousStatus = state.requirements.find(r => r.id === requirementId)?.status
dispatch({
type: 'UPDATE_REQUIREMENT',
payload: { id: requirementId, data: { status } },
@@ -500,17 +511,94 @@ export default function RequirementsPage() {
// Persist to backend
try {
- await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
+ const res = await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ status }),
+ body: JSON.stringify({ implementation_status: status.toLowerCase() }),
})
+ if (!res.ok) {
+ // Rollback on failure
+ if (previousStatus) {
+ dispatch({ type: 'UPDATE_REQUIREMENT', payload: { id: requirementId, data: { status: previousStatus } } })
+ }
+ setError('Status-Aenderung konnte nicht gespeichert werden')
+ }
} catch {
- // Silently fail — SDK state is already updated
+ if (previousStatus) {
+ dispatch({ type: 'UPDATE_REQUIREMENT', payload: { id: requirementId, data: { status: previousStatus } } })
+ }
+ setError('Backend nicht erreichbar — Aenderung zurueckgesetzt')
}
}
- const handleAddRequirement = (data: { regulation: string; article: string; title: string; description: string; criticality: RiskSeverity }) => {
+ const handleDeleteRequirement = async (requirementId: string) => {
+ if (!confirm('Anforderung wirklich loeschen?')) return
+
+ try {
+ const res = await fetch(`/api/sdk/v1/compliance/requirements/${requirementId}`, {
+ method: 'DELETE',
+ })
+ if (res.ok) {
+ dispatch({ type: 'SET_STATE', payload: { requirements: state.requirements.filter(r => r.id !== requirementId) } })
+ } else {
+ setError('Loeschen fehlgeschlagen')
+ }
+ } catch {
+ setError('Backend nicht erreichbar')
+ }
+ }
+
+ const handleAddRequirement = async (data: { regulation: string; article: string; title: string; description: string; criticality: RiskSeverity }) => {
+ // Try to resolve regulation_id from backend
+ let regulationId = ''
+ try {
+ const regRes = await fetch(`/api/sdk/v1/compliance/regulations/${data.regulation}`)
+ if (regRes.ok) {
+ const regData = await regRes.json()
+ regulationId = regData.id
+ }
+ } catch {
+ // Regulation not found — still add locally
+ }
+
+ const priorityMap: Record
= { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 }
+
+ if (regulationId) {
+ try {
+ const res = await fetch('/api/sdk/v1/compliance/requirements', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ regulation_id: regulationId,
+ article: data.article,
+ title: data.title,
+ description: data.description,
+ priority: priorityMap[data.criticality] || 2,
+ }),
+ })
+ if (res.ok) {
+ const created = await res.json()
+ const newReq: SDKRequirement = {
+ id: created.id,
+ regulation: data.regulation,
+ article: data.article,
+ title: data.title,
+ description: data.description,
+ criticality: data.criticality,
+ applicableModules: [],
+ status: 'NOT_STARTED',
+ controls: [],
+ }
+ dispatch({ type: 'ADD_REQUIREMENT', payload: newReq })
+ setShowAddForm(false)
+ return
+ }
+ } catch {
+ // Fall through to local-only add
+ }
+ }
+
+ // Fallback: add locally only
const newReq: SDKRequirement = {
id: `req-${Date.now()}`,
regulation: data.regulation,
@@ -651,6 +739,7 @@ export default function RequirementsPage() {
key={requirement.id}
requirement={requirement}
onStatusChange={(status) => handleStatusChange(requirement.id, status)}
+ onDelete={() => handleDeleteRequirement(requirement.id)}
expanded={expandedId === requirement.id}
onToggleDetails={() => setExpandedId(expandedId === requirement.id ? null : requirement.id)}
linkedControls={linkedControls}
diff --git a/admin-compliance/app/(sdk)/sdk/risks/page.tsx b/admin-compliance/app/(sdk)/sdk/risks/page.tsx
index e41cae1..8a69fcf 100644
--- a/admin-compliance/app/(sdk)/sdk/risks/page.tsx
+++ b/admin-compliance/app/(sdk)/sdk/risks/page.tsx
@@ -271,11 +271,14 @@ function RiskCard({
risk,
onEdit,
onDelete,
+ onStatusChange,
}: {
risk: Risk
onEdit: () => void
onDelete: () => void
+ onStatusChange: (status: RiskStatus) => void
}) {
+ const [showMitigations, setShowMitigations] = useState(false)
const severityColors = {
CRITICAL: 'border-red-200 bg-red-50',
HIGH: 'border-orange-200 bg-orange-50',
@@ -335,7 +338,7 @@ function RiskCard({
-
+
Wahrscheinlichkeit:
{risk.likelihood}/5
@@ -345,14 +348,69 @@ function RiskCard({
{risk.impact}/5
- Score:
+ Inherent:
{risk.inherentRiskScore}
+
+ Residual:
+
+ {risk.residualRiskScore}
+
+ {risk.residualRiskScore < risk.inherentRiskScore && (
+
+ ({risk.inherentRiskScore} → {risk.residualRiskScore})
+
+ )}
+
- {risk.mitigation.length > 0 && (
-
-
Mitigationen: {risk.mitigation.length}
+ {/* Status Workflow */}
+
+
+ Status:
+
+
+ {risk.mitigation.length > 0 && (
+
+ )}
+
+
+ {/* Expanded Mitigations */}
+ {showMitigations && risk.mitigation.length > 0 && (
+
+ {risk.mitigation.map((m, idx) => (
+
+
+ {m.controlId || `Mitigation ${idx + 1}`}
+
+ {m.status === 'IMPLEMENTED' ? 'Implementiert' :
+ m.status === 'IN_PROGRESS' ? 'In Bearbeitung' : m.status || 'Geplant'}
+
+
+ {m.description &&
{m.description}
}
+
+ ))}
)}
@@ -516,6 +574,23 @@ export default function RisksPage() {
}
}
+ const handleStatusChange = async (riskId: string, status: RiskStatus) => {
+ dispatch({
+ type: 'UPDATE_RISK',
+ payload: { id: riskId, data: { status } },
+ })
+
+ try {
+ await fetch(`/api/sdk/v1/compliance/risks/${riskId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status }),
+ })
+ } catch {
+ // Silently fail
+ }
+ }
+
const handleEdit = (risk: Risk) => {
setEditingRisk(risk)
setShowForm(true)
@@ -640,6 +715,7 @@ export default function RisksPage() {
risk={risk}
onEdit={() => handleEdit(risk)}
onDelete={() => handleDelete(risk.id)}
+ onStatusChange={(status) => handleStatusChange(risk.id, status)}
/>
))}
diff --git a/backend-compliance/compliance/api/ai_routes.py b/backend-compliance/compliance/api/ai_routes.py
index a943e1c..777c844 100644
--- a/backend-compliance/compliance/api/ai_routes.py
+++ b/backend-compliance/compliance/api/ai_routes.py
@@ -30,7 +30,7 @@ from ..db import (
RequirementRepository,
ControlRepository,
)
-from ..db.models import RegulationDB, RequirementDB
+from ..db.models import RegulationDB, RequirementDB, AISystemDB, AIClassificationEnum, AISystemStatusEnum
from .schemas import (
# AI Assistant schemas
AIInterpretationRequest, AIInterpretationResponse,
@@ -39,6 +39,8 @@ from .schemas import (
AIRiskAssessmentRequest, AIRiskAssessmentResponse, AIRiskFactor,
AIGapAnalysisRequest, AIGapAnalysisResponse,
AIStatusResponse,
+ # AI System schemas
+ AISystemCreate, AISystemUpdate, AISystemResponse, AISystemListResponse,
# PDF extraction schemas
BSIAspectResponse, PDFExtractionResponse,
)
@@ -47,6 +49,361 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=["compliance-ai"])
+# ============================================================================
+# AI System CRUD Endpoints (AI Act Compliance)
+# ============================================================================
+
+@router.get("/ai/systems", response_model=AISystemListResponse)
+async def list_ai_systems(
+ classification: Optional[str] = Query(None, description="Filter by classification"),
+ status: Optional[str] = Query(None, description="Filter by status"),
+ sector: Optional[str] = Query(None, description="Filter by sector"),
+ db: Session = Depends(get_db),
+):
+ """List all registered AI systems."""
+ import uuid as _uuid
+ query = db.query(AISystemDB)
+
+ if classification:
+ try:
+ cls_enum = AIClassificationEnum(classification)
+ query = query.filter(AISystemDB.classification == cls_enum)
+ except ValueError:
+ pass
+
+ if status:
+ try:
+ status_enum = AISystemStatusEnum(status)
+ query = query.filter(AISystemDB.status == status_enum)
+ except ValueError:
+ pass
+
+ if sector:
+ query = query.filter(AISystemDB.sector.ilike(f"%{sector}%"))
+
+ systems = query.order_by(AISystemDB.created_at.desc()).all()
+
+ results = [
+ AISystemResponse(
+ id=s.id,
+ name=s.name,
+ description=s.description,
+ purpose=s.purpose,
+ sector=s.sector,
+ classification=s.classification.value if s.classification else "unclassified",
+ status=s.status.value if s.status else "draft",
+ obligations=s.obligations or [],
+ assessment_date=s.assessment_date,
+ assessment_result=s.assessment_result,
+ risk_factors=s.risk_factors,
+ recommendations=s.recommendations,
+ created_at=s.created_at,
+ updated_at=s.updated_at,
+ )
+ for s in systems
+ ]
+
+ return AISystemListResponse(systems=results, total=len(results))
+
+
+@router.post("/ai/systems", response_model=AISystemResponse)
+async def create_ai_system(
+ data: AISystemCreate,
+ db: Session = Depends(get_db),
+):
+ """Register a new AI system."""
+ import uuid as _uuid
+ from datetime import datetime
+
+ try:
+ cls_enum = AIClassificationEnum(data.classification) if data.classification else AIClassificationEnum.UNCLASSIFIED
+ except ValueError:
+ cls_enum = AIClassificationEnum.UNCLASSIFIED
+
+ try:
+ status_enum = AISystemStatusEnum(data.status) if data.status else AISystemStatusEnum.DRAFT
+ except ValueError:
+ status_enum = AISystemStatusEnum.DRAFT
+
+ system = AISystemDB(
+ id=str(_uuid.uuid4()),
+ name=data.name,
+ description=data.description,
+ purpose=data.purpose,
+ sector=data.sector,
+ classification=cls_enum,
+ status=status_enum,
+ obligations=data.obligations or [],
+ )
+ db.add(system)
+ db.commit()
+ db.refresh(system)
+
+ return AISystemResponse(
+ id=system.id,
+ name=system.name,
+ description=system.description,
+ purpose=system.purpose,
+ sector=system.sector,
+ classification=system.classification.value if system.classification else "unclassified",
+ status=system.status.value if system.status else "draft",
+ obligations=system.obligations or [],
+ assessment_date=system.assessment_date,
+ assessment_result=system.assessment_result,
+ risk_factors=system.risk_factors,
+ recommendations=system.recommendations,
+ created_at=system.created_at,
+ updated_at=system.updated_at,
+ )
+
+
+@router.get("/ai/systems/{system_id}", response_model=AISystemResponse)
+async def get_ai_system(system_id: str, db: Session = Depends(get_db)):
+ """Get a specific AI system by ID."""
+ system = db.query(AISystemDB).filter(AISystemDB.id == system_id).first()
+ if not system:
+ raise HTTPException(status_code=404, detail=f"AI System {system_id} not found")
+
+ return AISystemResponse(
+ id=system.id,
+ name=system.name,
+ description=system.description,
+ purpose=system.purpose,
+ sector=system.sector,
+ classification=system.classification.value if system.classification else "unclassified",
+ status=system.status.value if system.status else "draft",
+ obligations=system.obligations or [],
+ assessment_date=system.assessment_date,
+ assessment_result=system.assessment_result,
+ risk_factors=system.risk_factors,
+ recommendations=system.recommendations,
+ created_at=system.created_at,
+ updated_at=system.updated_at,
+ )
+
+
+@router.put("/ai/systems/{system_id}", response_model=AISystemResponse)
+async def update_ai_system(
+ system_id: str,
+ data: AISystemUpdate,
+ db: Session = Depends(get_db),
+):
+ """Update an AI system."""
+ from datetime import datetime
+
+ system = db.query(AISystemDB).filter(AISystemDB.id == system_id).first()
+ if not system:
+ raise HTTPException(status_code=404, detail=f"AI System {system_id} not found")
+
+ update_data = data.model_dump(exclude_unset=True)
+
+ if "classification" in update_data:
+ try:
+ update_data["classification"] = AIClassificationEnum(update_data["classification"])
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid classification: {update_data['classification']}")
+
+ if "status" in update_data:
+ try:
+ update_data["status"] = AISystemStatusEnum(update_data["status"])
+ except ValueError:
+ raise HTTPException(status_code=400, detail=f"Invalid status: {update_data['status']}")
+
+ for key, value in update_data.items():
+ if hasattr(system, key):
+ setattr(system, key, value)
+
+ system.updated_at = datetime.utcnow()
+ db.commit()
+ db.refresh(system)
+
+ return AISystemResponse(
+ id=system.id,
+ name=system.name,
+ description=system.description,
+ purpose=system.purpose,
+ sector=system.sector,
+ classification=system.classification.value if system.classification else "unclassified",
+ status=system.status.value if system.status else "draft",
+ obligations=system.obligations or [],
+ assessment_date=system.assessment_date,
+ assessment_result=system.assessment_result,
+ risk_factors=system.risk_factors,
+ recommendations=system.recommendations,
+ created_at=system.created_at,
+ updated_at=system.updated_at,
+ )
+
+
+@router.delete("/ai/systems/{system_id}")
+async def delete_ai_system(system_id: str, db: Session = Depends(get_db)):
+ """Delete an AI system."""
+ system = db.query(AISystemDB).filter(AISystemDB.id == system_id).first()
+ if not system:
+ raise HTTPException(status_code=404, detail=f"AI System {system_id} not found")
+
+ db.delete(system)
+ db.commit()
+ return {"success": True, "message": "AI System deleted"}
+
+
+@router.post("/ai/systems/{system_id}/assess", response_model=AISystemResponse)
+async def assess_ai_system(
+ system_id: str,
+ db: Session = Depends(get_db),
+):
+ """Run AI Act risk assessment for an AI system."""
+ from datetime import datetime
+
+ system = db.query(AISystemDB).filter(AISystemDB.id == system_id).first()
+ if not system:
+ raise HTTPException(status_code=404, detail=f"AI System {system_id} not found")
+
+ # Try AI-based assessment
+ assessment_result = None
+ try:
+ from ..services.ai_compliance_assistant import get_ai_assistant
+ assistant = get_ai_assistant()
+ result = await assistant.assess_module_risk(
+ module_name=system.name,
+ service_type="ai_system",
+ description=system.description or "",
+ processes_pii=True,
+ ai_components=True,
+ criticality="high",
+ data_categories=[],
+ regulations=[{"code": "AI-ACT", "relevance": "high"}],
+ )
+ assessment_result = {
+ "overall_risk": result.overall_risk,
+ "risk_factors": result.risk_factors,
+ "recommendations": result.recommendations,
+ "compliance_gaps": result.compliance_gaps,
+ "confidence_score": result.confidence_score,
+ }
+ except Exception as e:
+ logger.warning(f"AI assessment failed for {system_id}, using rule-based: {e}")
+ # Rule-based fallback
+ assessment_result = _rule_based_assessment(system)
+
+ # Update system with assessment results
+ classification = _derive_classification(assessment_result)
+ try:
+ system.classification = AIClassificationEnum(classification)
+ except ValueError:
+ system.classification = AIClassificationEnum.UNCLASSIFIED
+
+ system.assessment_date = datetime.utcnow()
+ system.assessment_result = assessment_result
+ system.obligations = _derive_obligations(classification)
+ system.risk_factors = assessment_result.get("risk_factors", [])
+ system.recommendations = assessment_result.get("recommendations", [])
+ system.status = AISystemStatusEnum.CLASSIFIED
+
+ db.commit()
+ db.refresh(system)
+
+ return AISystemResponse(
+ id=system.id,
+ name=system.name,
+ description=system.description,
+ purpose=system.purpose,
+ sector=system.sector,
+ classification=system.classification.value if system.classification else "unclassified",
+ status=system.status.value if system.status else "draft",
+ obligations=system.obligations or [],
+ assessment_date=system.assessment_date,
+ assessment_result=system.assessment_result,
+ risk_factors=system.risk_factors,
+ recommendations=system.recommendations,
+ created_at=system.created_at,
+ updated_at=system.updated_at,
+ )
+
+
+def _rule_based_assessment(system: AISystemDB) -> dict:
+ """Simple rule-based AI Act classification when AI service is unavailable."""
+ desc = (system.description or "").lower() + " " + (system.purpose or "").lower()
+ sector = (system.sector or "").lower()
+
+ risk_factors = []
+ risk_score = 0
+
+ # Check for prohibited use cases
+ prohibited_keywords = ["social scoring", "biometric surveillance", "emotion recognition", "subliminal manipulation"]
+ for kw in prohibited_keywords:
+ if kw in desc:
+ risk_factors.append({"factor": f"Prohibited use case: {kw}", "severity": "critical", "likelihood": "high"})
+ risk_score += 10
+
+ # Check for high-risk indicators
+ high_risk_keywords = ["education", "employment", "credit scoring", "law enforcement", "migration", "critical infrastructure", "medical", "bildung", "gesundheit"]
+ for kw in high_risk_keywords:
+ if kw in desc or kw in sector:
+ risk_factors.append({"factor": f"High-risk sector: {kw}", "severity": "high", "likelihood": "medium"})
+ risk_score += 5
+
+ # Check for limited-risk indicators
+ limited_keywords = ["chatbot", "deepfake", "emotion", "biometric"]
+ for kw in limited_keywords:
+ if kw in desc:
+ risk_factors.append({"factor": f"Transparency requirement: {kw}", "severity": "medium", "likelihood": "high"})
+ risk_score += 3
+
+ return {
+ "overall_risk": "critical" if risk_score >= 10 else "high" if risk_score >= 5 else "medium" if risk_score >= 3 else "low",
+ "risk_factors": risk_factors,
+ "recommendations": [
+ "Dokumentation des AI-Systems vervollstaendigen",
+ "Risikomanagement-Framework implementieren",
+ "Transparenzpflichten pruefen",
+ ],
+ "compliance_gaps": [],
+ "confidence_score": 0.6,
+ "risk_score": risk_score,
+ }
+
+
+def _derive_classification(assessment: dict) -> str:
+ """Derive AI Act classification from assessment result."""
+ risk = assessment.get("overall_risk", "medium")
+ score = assessment.get("risk_score", 0)
+
+ if score >= 10:
+ return "prohibited"
+ elif risk in ("critical", "high") or score >= 5:
+ return "high-risk"
+ elif risk == "medium" or score >= 3:
+ return "limited-risk"
+ else:
+ return "minimal-risk"
+
+
+def _derive_obligations(classification: str) -> list:
+ """Derive AI Act obligations based on classification."""
+ obligations_map = {
+ "prohibited": ["Einsatz verboten (Art. 5 AI Act)"],
+ "high-risk": [
+ "Risikomanagementsystem (Art. 9)",
+ "Daten-Governance (Art. 10)",
+ "Technische Dokumentation (Art. 11)",
+ "Aufzeichnungspflicht (Art. 12)",
+ "Transparenz (Art. 13)",
+ "Menschliche Aufsicht (Art. 14)",
+ "Genauigkeit & Robustheit (Art. 15)",
+ "Konformitaetsbewertung (Art. 43)",
+ ],
+ "limited-risk": [
+ "Transparenzpflicht (Art. 52)",
+ "Kennzeichnung als KI-System",
+ ],
+ "minimal-risk": [
+ "Freiwillige Verhaltenskodizes (Art. 69)",
+ ],
+ }
+ return obligations_map.get(classification, [])
+
+
# ============================================================================
# AI Assistant Endpoints (Sprint 4)
# ============================================================================
diff --git a/backend-compliance/compliance/api/evidence_routes.py b/backend-compliance/compliance/api/evidence_routes.py
index 5d52f6a..3706410 100644
--- a/backend-compliance/compliance/api/evidence_routes.py
+++ b/backend-compliance/compliance/api/evidence_routes.py
@@ -46,9 +46,11 @@ async def list_evidence(
control_id: Optional[str] = None,
evidence_type: Optional[str] = None,
status: Optional[str] = None,
+ page: Optional[int] = Query(None, ge=1, description="Page number (1-based)"),
+ limit: Optional[int] = Query(None, ge=1, le=500, description="Items per page"),
db: Session = Depends(get_db),
):
- """List evidence with optional filters."""
+ """List evidence with optional filters and pagination."""
repo = EvidenceRepository(db)
if control_id:
@@ -71,6 +73,13 @@ async def list_evidence(
except ValueError:
pass
+ total = len(evidence)
+
+ # Apply pagination if requested
+ if page is not None and limit is not None:
+ offset = (page - 1) * limit
+ evidence = evidence[offset:offset + limit]
+
results = [
EvidenceResponse(
id=e.id,
@@ -95,7 +104,7 @@ async def list_evidence(
for e in evidence
]
- return EvidenceListResponse(evidence=results, total=len(results))
+ return EvidenceListResponse(evidence=results, total=total)
@router.post("/evidence", response_model=EvidenceResponse)
diff --git a/backend-compliance/compliance/api/routes.py b/backend-compliance/compliance/api/routes.py
index 814b4bb..2721f30 100644
--- a/backend-compliance/compliance/api/routes.py
+++ b/backend-compliance/compliance/api/routes.py
@@ -324,6 +324,59 @@ async def list_requirements_paginated(
)
+@router.post("/requirements", response_model=RequirementResponse)
+async def create_requirement(
+ data: RequirementCreate,
+ db: Session = Depends(get_db),
+):
+ """Create a new requirement."""
+ # Verify regulation exists
+ reg_repo = RegulationRepository(db)
+ regulation = reg_repo.get_by_id(data.regulation_id)
+ if not regulation:
+ raise HTTPException(status_code=404, detail=f"Regulation {data.regulation_id} not found")
+
+ req_repo = RequirementRepository(db)
+ requirement = req_repo.create(
+ regulation_id=data.regulation_id,
+ article=data.article,
+ title=data.title,
+ paragraph=data.paragraph,
+ description=data.description,
+ requirement_text=data.requirement_text,
+ breakpilot_interpretation=data.breakpilot_interpretation,
+ is_applicable=data.is_applicable,
+ priority=data.priority,
+ )
+
+ return RequirementResponse(
+ id=requirement.id,
+ regulation_id=requirement.regulation_id,
+ regulation_code=regulation.code,
+ article=requirement.article,
+ paragraph=requirement.paragraph,
+ title=requirement.title,
+ description=requirement.description,
+ requirement_text=requirement.requirement_text,
+ breakpilot_interpretation=requirement.breakpilot_interpretation,
+ is_applicable=requirement.is_applicable,
+ applicability_reason=requirement.applicability_reason,
+ priority=requirement.priority,
+ created_at=requirement.created_at,
+ updated_at=requirement.updated_at,
+ )
+
+
+@router.delete("/requirements/{requirement_id}")
+async def delete_requirement(requirement_id: str, db: Session = Depends(get_db)):
+ """Delete a requirement by ID."""
+ req_repo = RequirementRepository(db)
+ deleted = req_repo.delete(requirement_id)
+ if not deleted:
+ raise HTTPException(status_code=404, detail=f"Requirement {requirement_id} not found")
+ return {"success": True, "message": "Requirement deleted"}
+
+
@router.put("/requirements/{requirement_id}")
async def update_requirement(requirement_id: str, updates: dict, db: Session = Depends(get_db)):
"""Update a requirement with implementation/audit details."""
@@ -818,7 +871,7 @@ async def init_tables(db: Session = Depends(get_db)):
from classroom_engine.database import engine
from ..db.models import (
RegulationDB, RequirementDB, ControlDB, ControlMappingDB,
- EvidenceDB, RiskDB, AuditExportDB
+ EvidenceDB, RiskDB, AuditExportDB, AISystemDB
)
try:
@@ -830,6 +883,7 @@ async def init_tables(db: Session = Depends(get_db)):
EvidenceDB.__table__.create(engine, checkfirst=True)
RiskDB.__table__.create(engine, checkfirst=True)
AuditExportDB.__table__.create(engine, checkfirst=True)
+ AISystemDB.__table__.create(engine, checkfirst=True)
return {"success": True, "message": "Tables created successfully"}
except Exception as e:
diff --git a/backend-compliance/compliance/api/schemas.py b/backend-compliance/compliance/api/schemas.py
index 02676e7..69cd173 100644
--- a/backend-compliance/compliance/api/schemas.py
+++ b/backend-compliance/compliance/api/schemas.py
@@ -385,6 +385,52 @@ class RiskMatrixResponse(BaseModel):
risks: List[RiskResponse]
+# ============================================================================
+# AI System Schemas (AI Act Compliance)
+# ============================================================================
+
+class AISystemBase(BaseModel):
+ name: str
+ description: Optional[str] = None
+ purpose: Optional[str] = None
+ sector: Optional[str] = None
+ classification: str = "unclassified"
+ status: str = "draft"
+ obligations: Optional[List[str]] = None
+
+
+class AISystemCreate(AISystemBase):
+ pass
+
+
+class AISystemUpdate(BaseModel):
+ name: Optional[str] = None
+ description: Optional[str] = None
+ purpose: Optional[str] = None
+ sector: Optional[str] = None
+ classification: Optional[str] = None
+ status: Optional[str] = None
+ obligations: Optional[List[str]] = None
+
+
+class AISystemResponse(AISystemBase):
+ id: str
+ assessment_date: Optional[datetime] = None
+ assessment_result: Optional[Dict[str, Any]] = None
+ risk_factors: Optional[List[Dict[str, Any]]] = None
+ recommendations: Optional[List[str]] = None
+ created_at: datetime
+ updated_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class AISystemListResponse(BaseModel):
+ systems: List[AISystemResponse]
+ total: int
+
+
# ============================================================================
# Dashboard & Export Schemas
# ============================================================================
diff --git a/backend-compliance/compliance/db/models.py b/backend-compliance/compliance/db/models.py
index 422a0e6..5b02d94 100644
--- a/backend-compliance/compliance/db/models.py
+++ b/backend-compliance/compliance/db/models.py
@@ -401,6 +401,60 @@ class RiskDB(Base):
return RiskLevelEnum.LOW
+class AIClassificationEnum(str, enum.Enum):
+ """AI Act risk classification."""
+ PROHIBITED = "prohibited"
+ HIGH_RISK = "high-risk"
+ LIMITED_RISK = "limited-risk"
+ MINIMAL_RISK = "minimal-risk"
+ UNCLASSIFIED = "unclassified"
+
+
+class AISystemStatusEnum(str, enum.Enum):
+ """Status of an AI system in compliance tracking."""
+ DRAFT = "draft"
+ CLASSIFIED = "classified"
+ COMPLIANT = "compliant"
+ NON_COMPLIANT = "non-compliant"
+
+
+class AISystemDB(Base):
+ """
+ AI System registry for AI Act compliance.
+ Tracks AI systems, their risk classification, and compliance status.
+ """
+ __tablename__ = 'compliance_ai_systems'
+
+ id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
+ name = Column(String(300), nullable=False)
+ description = Column(Text)
+ purpose = Column(String(500))
+ sector = Column(String(100))
+
+ # AI Act classification
+ classification = Column(Enum(AIClassificationEnum), default=AIClassificationEnum.UNCLASSIFIED)
+ status = Column(Enum(AISystemStatusEnum), default=AISystemStatusEnum.DRAFT)
+
+ # Assessment
+ assessment_date = Column(DateTime)
+ assessment_result = Column(JSON) # Full assessment result
+ obligations = Column(JSON) # List of AI Act obligations
+ risk_factors = Column(JSON) # Risk factors from assessment
+ recommendations = Column(JSON) # Recommendations from assessment
+
+ # Timestamps
+ created_at = Column(DateTime, default=datetime.utcnow)
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+
+ __table_args__ = (
+ Index('ix_ai_system_classification', 'classification'),
+ Index('ix_ai_system_status', 'status'),
+ )
+
+ def __repr__(self):
+ return f"
"
+
+
class AuditExportDB(Base):
"""
Tracks audit export packages generated for external auditors.
diff --git a/backend-compliance/compliance/db/repository.py b/backend-compliance/compliance/db/repository.py
index eb05f9f..dc3c6d9 100644
--- a/backend-compliance/compliance/db/repository.py
+++ b/backend-compliance/compliance/db/repository.py
@@ -252,6 +252,15 @@ class RequirementRepository:
return items, total
+ def delete(self, requirement_id: str) -> bool:
+ """Delete a requirement."""
+ requirement = self.db.query(RequirementDB).filter(RequirementDB.id == requirement_id).first()
+ if not requirement:
+ return False
+ self.db.delete(requirement)
+ self.db.commit()
+ return True
+
def count(self) -> int:
"""Count all requirements."""
return self.db.query(func.count(RequirementDB.id)).scalar() or 0