Files
Unraid-AppOps-Gateway-Public/backend/app/services/incident_service.py
T

144 lines
5.4 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
from sqlmodel import Session, select
from ..db.models import AppSetting, Connector, Policy
from ..core.time import utc_now
from .inventory_discovery import list_drift_incidents, update_drift_incident_status
from .secret_store import secret_storage_status
_INCIDENT_STATES_SETTING = 'incidents:states'
def _now_iso() -> str:
return datetime.now(UTC).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
def _incident_states(session: Session) -> dict[str, dict[str, Any]]:
row = session.get(AppSetting, _INCIDENT_STATES_SETTING)
value = row.value_json if row else {}
items = value.get('items', {}) if isinstance(value, dict) else {}
return {str(key): value for key, value in items.items() if isinstance(value, dict)}
def _set_incident_states(session: Session, states: dict[str, dict[str, Any]]) -> None:
row = session.get(AppSetting, _INCIDENT_STATES_SETTING)
value = {'items': states}
if not row:
row = AppSetting(key=_INCIDENT_STATES_SETTING, value_json=value)
else:
row.value_json = value
row.updated_at = utc_now()
session.add(row)
def _generated_incidents(session: Session) -> list[dict[str, Any]]:
timestamp = _now_iso()
incidents: list[dict[str, Any]] = []
connectors = session.exec(select(Connector).order_by(Connector.id)).all()
degraded_connectors = [
connector.id
for connector in connectors
if connector.status in {'missing_config', 'disconnected', 'error', 'auth_failed', 'unreachable'}
]
if degraded_connectors:
incidents.append({
'id': 'incident-connectors-config',
'severity': 'high' if len(degraded_connectors) >= 5 else 'medium',
'status': 'open',
'title': 'Connector configuratie of health vraagt aandacht',
'detectedTime': timestamp,
'evidence': {
'source': 'Connector Registry',
'check': 'Connector readiness',
'observed': f"{len(degraded_connectors)} degraded connector(s): {', '.join(degraded_connectors[:8])}",
'expected': 'Alle bekende connectors connected of bewust optioneel.',
'timestamp': timestamp,
},
'suggestedActions': ['connector.configure', 'connector.test'],
})
volume_policy = session.get(Policy, 'policy-volume-delete')
if not (volume_policy and volume_policy.voice == 'blocked' and volume_policy.manual == 'blocked'):
incidents.append({
'id': 'incident-policy-volume-delete',
'severity': 'critical',
'status': 'open',
'title': 'Destructive storage policy niet fail-closed',
'detectedTime': timestamp,
'evidence': {
'source': 'Policy Engine',
'check': 'volume.delete guardrail',
'observed': 'volume.delete is niet volledig blocked',
'expected': 'voice=blocked en manual=blocked',
'timestamp': timestamp,
},
'suggestedActions': ['policy.review'],
})
storage = secret_storage_status()
if not storage.get('productionReady'):
incidents.append({
'id': 'incident-secret-storage',
'severity': 'high',
'status': 'open',
'title': 'Secret storage niet production-ready',
'detectedTime': timestamp,
'evidence': {
'source': 'Secret Store',
'check': 'Encrypted-at-rest readiness',
'observed': 'Secret storage mist production-ready configuratie.',
'expected': 'Encrypted secret storage met persistente sleutel.',
'timestamp': timestamp,
},
'suggestedActions': ['secrets.health'],
})
return incidents
def list_incidents(session: Session) -> list[dict[str, Any]]:
states = _incident_states(session)
incidents: list[dict[str, Any]] = []
for incident in _generated_incidents(session):
state = states.get(incident['id'], {})
status = str(state.get('status') or 'open')
if status == 'resolved':
continue
incident['status'] = status if status in {'open', 'acknowledged', 'muted'} else 'open'
incidents.append(incident)
incidents.extend(list_drift_incidents(session))
return incidents
def update_incident_status(session: Session, incident_id: str, status: str) -> dict[str, Any]:
if status not in {'open', 'acknowledged', 'muted', 'resolved'}:
status = 'acknowledged'
drift_update = update_drift_incident_status(session, incident_id, status)
if drift_update:
return drift_update
current_ids = {incident['id'] for incident in _generated_incidents(session)}
states = _incident_states(session)
if incident_id not in current_ids and incident_id not in states:
return {'success': False, 'message': 'incident_not_found', 'result': 'failed', 'status': 'failed'}
states[incident_id] = {'status': status, 'updatedAt': _now_iso()}
_set_incident_states(session, states)
session.commit()
return {
'success': True,
'message': f'Incident {incident_id} status updated',
'result': 'success',
'status': 'success',
}
def count_open_incidents(session: Session) -> int:
return len([incident for incident in list_incidents(session) if incident.get('status') == 'open'])