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'])