211 lines
7.6 KiB
Python
211 lines
7.6 KiB
Python
from datetime import UTC, datetime
|
|
from typing import Any
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlmodel import Session
|
|
|
|
from ..core.redaction import redact_message
|
|
from ..db.session import get_session
|
|
from ..services import ai_security_reviewer, knowledge_service
|
|
|
|
router = APIRouter(prefix='/api/ai')
|
|
|
|
|
|
_AI_TOOLS = [
|
|
{
|
|
'id': 'read_container_status',
|
|
'name': 'read_container_status',
|
|
'description': 'Leest containerstatus via de Portainer read facade.',
|
|
'type': 'read',
|
|
'permissionLevel': 1,
|
|
'requiresApproval': False,
|
|
'rateLimit': '60/min',
|
|
'tool': 'read_container_status',
|
|
'enabled': True,
|
|
'allowedByDefault': True,
|
|
'scope': 'read',
|
|
},
|
|
{
|
|
'id': 'read_unraid_health',
|
|
'name': 'read_unraid_health',
|
|
'description': 'Leest Unraid array, cache, parity en docker health zonder mutaties.',
|
|
'type': 'read',
|
|
'permissionLevel': 1,
|
|
'requiresApproval': False,
|
|
'rateLimit': '30/min',
|
|
'tool': 'read_unraid_health',
|
|
'enabled': True,
|
|
'allowedByDefault': True,
|
|
'scope': 'read',
|
|
},
|
|
{
|
|
'id': 'read_audit',
|
|
'name': 'read_audit',
|
|
'description': 'Leest geredigeerde audit events voor analyse en incidentcontext.',
|
|
'type': 'read',
|
|
'permissionLevel': 1,
|
|
'requiresApproval': False,
|
|
'rateLimit': '30/min',
|
|
'tool': 'read_audit',
|
|
'enabled': True,
|
|
'allowedByDefault': True,
|
|
'scope': 'read',
|
|
},
|
|
{
|
|
'id': 'query_knowledge',
|
|
'name': 'query_knowledge',
|
|
'description': 'Zoekt lokale documentatie en actuele, geredigeerde AppOps-context met bronverwijzingen.',
|
|
'type': 'read',
|
|
'permissionLevel': 1,
|
|
'requiresApproval': False,
|
|
'rateLimit': '30/min',
|
|
'tool': 'query_knowledge',
|
|
'enabled': True,
|
|
'allowedByDefault': True,
|
|
'scope': 'read',
|
|
},
|
|
{
|
|
'id': 'draft_playbook_run',
|
|
'name': 'draft_playbook_run',
|
|
'description': 'Maakt alleen een voorstel voor playbook-uitvoering; uitvoering loopt via policy en approval.',
|
|
'type': 'mutate',
|
|
'permissionLevel': 3,
|
|
'requiresApproval': True,
|
|
'rateLimit': '5/min',
|
|
'tool': 'draft_playbook_run',
|
|
'enabled': True,
|
|
'allowedByDefault': False,
|
|
'scope': 'mutate',
|
|
},
|
|
]
|
|
|
|
|
|
def _redact_payload(value: Any) -> Any:
|
|
if isinstance(value, str):
|
|
return redact_message(value)
|
|
if isinstance(value, list):
|
|
return [_redact_payload(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {str(key): _redact_payload(item) for key, item in value.items()}
|
|
return value
|
|
|
|
@router.get('/status')
|
|
def status(session: Session = Depends(get_session)):
|
|
summary = ai_security_reviewer.get_summary(session)
|
|
current_risks = int(summary.get('criticalFindings') or 0) + int(summary.get('highFindings') or 0) + int(summary.get('mediumFindings') or 0)
|
|
recommended_actions = int(summary.get('recommendationsCount') or 0)
|
|
data_completeness = summary.get('dataCompleteness') if isinstance(summary.get('dataCompleteness'), dict) else {}
|
|
missing_sources = data_completeness.get('missingSources') or []
|
|
if missing_sources:
|
|
storage_forecast = f"Coverage partial; ontbrekende bronnen: {', '.join(str(item) for item in missing_sources[:4])}."
|
|
else:
|
|
storage_forecast = 'Geen storage forecast beschikbaar in de advisory AI status.'
|
|
|
|
return {
|
|
'dailySummary': redact_message(
|
|
f"AI advisor draait read-only. Security status: {summary.get('status', 'unknown')}; "
|
|
f"{current_risks} open risico(s), {recommended_actions} aanbeveling(en)."
|
|
),
|
|
'currentRisks': current_risks,
|
|
'recommendedActions': recommended_actions,
|
|
'storageForecast': redact_message(storage_forecast),
|
|
'securityScore': int(summary.get('score') or 0),
|
|
'autopilotStatus': 'off',
|
|
}
|
|
|
|
@router.post('/chat')
|
|
def chat(payload: dict, session: Session = Depends(get_session)):
|
|
content = redact_message(str(payload.get('content') or ''))
|
|
try:
|
|
grounded = knowledge_service.query(session, content, limit=5)
|
|
except ValueError:
|
|
grounded = {
|
|
'answer': 'Stel een concrete vraag over AppOps, policies, apps, audit of platformwerking.',
|
|
'citations': [],
|
|
'confidence': 'none',
|
|
}
|
|
citations = grounded.get('citations') or []
|
|
evidence = '; '.join(
|
|
f"[{item.get('rank')}] {item.get('title')} — {item.get('uri')}"
|
|
for item in citations[:5]
|
|
if isinstance(item, dict)
|
|
)
|
|
return {
|
|
'id': f"chat-{uuid.uuid4().hex[:12]}",
|
|
'role': 'assistant',
|
|
'content': grounded['answer'],
|
|
'timestamp': datetime.now(UTC).isoformat(),
|
|
'metadata': {
|
|
'evidence': evidence or 'Geen relevante bron gevonden; er is niet gegokt.',
|
|
'risk': 'low',
|
|
'policyResult': 'allowed',
|
|
'nextStep': 'RAG blijft read-only. Uitvoering loopt uitsluitend via Command Center, policy en approvals.',
|
|
'citations': citations,
|
|
'confidence': grounded.get('confidence', 'none'),
|
|
},
|
|
}
|
|
|
|
@router.get('/recommendations')
|
|
def recommendations(session: Session = Depends(get_session)):
|
|
results: list[dict[str, Any]] = []
|
|
for recommendation in ai_security_reviewer.get_recommendations(session):
|
|
evidence = recommendation.get('evidence') or []
|
|
evidence_text = '; '.join(
|
|
redact_message(f"{item.get('source', 'source')}:{item.get('label', 'evidence')}={item.get('value', item.get('observed', ''))}")
|
|
for item in evidence[:3]
|
|
if isinstance(item, dict)
|
|
) or redact_message(str(recommendation.get('description') or 'Security reviewer recommendation.'))
|
|
status = recommendation.get('status') or 'open'
|
|
if status == 'accepted':
|
|
status = 'applied'
|
|
confidence = recommendation.get('confidence') or 'medium'
|
|
confidence_score = {'low': 0.45, 'medium': 0.7, 'high': 0.9}.get(str(confidence), 0.7)
|
|
severity = str(recommendation.get('severity') or 'medium')
|
|
if severity == 'info':
|
|
severity = 'low'
|
|
results.append({
|
|
'id': recommendation.get('id'),
|
|
'title': redact_message(str(recommendation.get('title') or 'AI recommendation')),
|
|
'severity': severity,
|
|
'confidence': confidence_score,
|
|
'evidence': evidence_text,
|
|
'affectedApps': [str(item) for item in recommendation.get('affectedResources', [])],
|
|
'suggestedAction': redact_message(str(recommendation.get('suggestedAction') or 'Review in AI Security Reviewer')),
|
|
'status': status,
|
|
})
|
|
return results
|
|
|
|
@router.post('/analyze/incident/{incident_id}')
|
|
def analyze_incident(incident_id: str):
|
|
return {'incident': incident_id, 'analysis': 'No blocking issues.'}
|
|
|
|
@router.post('/summarize/logs')
|
|
def summarize_logs(payload: dict):
|
|
return {'summary': 'No critical events.'}
|
|
|
|
@router.get('/tools')
|
|
def tools():
|
|
return _AI_TOOLS
|
|
|
|
|
|
@router.get('/history')
|
|
def history():
|
|
return []
|
|
|
|
@router.get('/memory')
|
|
def memory():
|
|
return []
|
|
|
|
@router.post('/memory')
|
|
def create_memory(payload: dict):
|
|
return {'success': True, 'memory': _redact_payload(payload)}
|
|
|
|
@router.patch('/memory/{memory_id}')
|
|
def update_memory(memory_id: str, payload: dict):
|
|
return {'success': True, 'id': memory_id, 'memory': _redact_payload(payload)}
|
|
|
|
@router.delete('/memory/{memory_id}')
|
|
def delete_memory(memory_id: str):
|
|
return {'success': True, 'id': memory_id}
|