144 lines
5.4 KiB
Python
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'])
|