228 lines
11 KiB
Python
228 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends
|
|
from sqlmodel import Session, select
|
|
|
|
from ..db.models import Connector, Policy
|
|
from ..db.session import get_session
|
|
|
|
router = APIRouter(prefix='/api')
|
|
|
|
|
|
def _connector_names(connectors: list[Connector], ids: set[str]) -> list[str]:
|
|
return [connector.name for connector in connectors if connector.id in ids]
|
|
|
|
|
|
def _policy_risk(session: Session, action: str, target: str, fallback: str = 'medium') -> str:
|
|
policy = session.exec(select(Policy).where(Policy.action == action, Policy.target == target)).first()
|
|
if policy and policy.risk in {'low', 'medium', 'high', 'critical'}:
|
|
return policy.risk
|
|
return fallback
|
|
|
|
|
|
def _connector_labels(connectors: list[Connector], ids: set[str]) -> list[str]:
|
|
labels = _connector_names(connectors, ids)
|
|
known = {connector.id for connector in connectors}
|
|
for connector_id in sorted(ids - known):
|
|
labels.append(connector_id.replace('_', ' ').title())
|
|
return labels
|
|
|
|
|
|
def _success_rate(connectors: dict[str, Connector], ids: set[str], fallback: int = 80) -> int:
|
|
if not ids:
|
|
return fallback
|
|
present = [connectors.get(connector_id) for connector_id in ids]
|
|
connected = sum(1 for connector in present if connector and connector.status == 'connected')
|
|
return round((connected / len(ids)) * 100)
|
|
|
|
|
|
@router.get('/playbooks')
|
|
def list_playbooks(session: Session = Depends(get_session)):
|
|
connectors = session.exec(select(Connector).order_by(Connector.id)).all()
|
|
degraded = [
|
|
connector
|
|
for connector in connectors
|
|
if connector.status in {'missing_config', 'disconnected', 'error', 'auth_failed', 'unreachable'}
|
|
]
|
|
connector_by_id = {connector.id: connector for connector in connectors}
|
|
|
|
playbooks: list[dict] = []
|
|
playbooks.append({
|
|
'id': 'storage-pressure-review',
|
|
'name': 'Storage Pressure Review',
|
|
'description': 'Brengt array-, cache-, disk-, share- en container-mountdruk samen zonder storage-mutaties te starten.',
|
|
'risk': 'medium',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': _connector_labels(connectors, {'unraid', 'portainer'}),
|
|
'steps': [
|
|
'Open Storage & Volumes and review array/cache pressure',
|
|
'Inspect disks and critical shares above threshold',
|
|
'Compare Portainer mount hints with Unraid share usage',
|
|
'Open Safety or Remediation Planner for non-executing follow-up notes',
|
|
'Keep mover, prune and storage mutations outside this playbook',
|
|
],
|
|
'successRate': _success_rate(connector_by_id, {'unraid', 'portainer'}),
|
|
})
|
|
|
|
playbooks.append({
|
|
'id': 'exposure-auth-coverage-review',
|
|
'name': 'Exposure & Auth Coverage Review',
|
|
'description': 'Controleert NPM public hosts, SSL-force, certificaten en Authentik coverage als één securityroute.',
|
|
'risk': 'high',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': _connector_labels(connectors, {'nginx_proxy_manager', 'authentik'}),
|
|
'steps': [
|
|
'Open Exposure Center and sort high-risk public hosts first',
|
|
'Check SSL-force, certificate status and access-list evidence',
|
|
'Open matching Authentik coverage details for unprotected hosts',
|
|
'Create or review a remediation draft before touching external systems',
|
|
'Re-run Safety refresh after operator-side NPM/Auth changes',
|
|
],
|
|
'successRate': _success_rate(connector_by_id, {'nginx_proxy_manager', 'authentik'}),
|
|
})
|
|
|
|
playbooks.append({
|
|
'id': 'media-stalled-download-review',
|
|
'name': 'Media Pipeline Stalled Download Review',
|
|
'description': 'Volgt Sonarr/Radarr wanted items, Prowlarr health en Deluge stalled downloads zonder data te verwijderen.',
|
|
'risk': 'medium',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': _connector_labels(connectors, {'sonarr', 'radarr', 'prowlarr', 'deluge', 'plex', 'tautulli', 'widefrog'}),
|
|
'steps': [
|
|
'Open Media Pipeline summary and review stalled/wanted counters',
|
|
'Check Prowlarr indexers and Sonarr/Radarr queue status',
|
|
'Inspect Deluge stalled downloads and pause/resume policy guidance',
|
|
'Review Plex/Tautulli activity before starting scans',
|
|
'Use approval-gated actions only for non-destructive connector operations',
|
|
],
|
|
'successRate': _success_rate(connector_by_id, {'sonarr', 'radarr', 'prowlarr', 'deluge', 'plex', 'tautulli', 'widefrog'}),
|
|
})
|
|
|
|
playbooks.append({
|
|
'id': 'unhealthy-container-triage',
|
|
'name': 'Unhealthy Container Triage',
|
|
'description': 'Richt de operator op ongezonde of ongemapte containers met logs, Portainer deep-link en policy-aware restartpad.',
|
|
'risk': 'medium',
|
|
'allowedSources': ['manual'],
|
|
'requiredConnectors': _connector_labels(connectors, {'portainer'}),
|
|
'steps': [
|
|
'Open Containers with attention filter',
|
|
'Inspect health, import status, app mapping and redacted logs',
|
|
'Open Portainer deep-link only when live context is available',
|
|
'Request approval before restarting protected apps',
|
|
'Verify the container and app detail after any approved action',
|
|
],
|
|
'successRate': _success_rate(connector_by_id, {'portainer'}),
|
|
})
|
|
|
|
playbooks.append({
|
|
'id': 'app-inventory-normalization',
|
|
'name': 'App Inventory Normalization',
|
|
'description': 'Werkt Unknown, needs-review en no-connector apps systematisch weg zodat Apps minder ruis tonen.',
|
|
'risk': 'low',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': _connector_labels(connectors, {'portainer'}),
|
|
'steps': [
|
|
'Open Apps with attention filter',
|
|
'Review Unknown and needs-review app cards first',
|
|
'Map discovered containers to canonical apps where the match is clear',
|
|
'Bind connector candidates only after connector config has tested connected',
|
|
'Re-run inventory discovery and compare drift after review',
|
|
],
|
|
'successRate': _success_rate(connector_by_id, {'portainer'}),
|
|
})
|
|
|
|
playbooks.append({
|
|
'id': 'backup-readiness-check',
|
|
'name': 'Backup Readiness Check',
|
|
'description': 'Controleert SQLite backup posture, restore-notes en release hygiene zonder restore uit te voeren.',
|
|
'risk': 'low',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': ['AppOps DB'],
|
|
'steps': [
|
|
'Open Settings backups section',
|
|
'Review latest backup metadata and integrity status',
|
|
'Confirm restore remains manual stop-container flow',
|
|
'Open Audit for recent backup events',
|
|
'Update release notes after verified production changes',
|
|
],
|
|
'successRate': 100,
|
|
})
|
|
|
|
playbooks.append({
|
|
'id': 'notification-delivery-review',
|
|
'name': 'Notification Delivery Review',
|
|
'description': 'Controleert dashboard- en Home Assistant notificatiebereik zonder automatisch testberichten te verzenden.',
|
|
'risk': 'low',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': _connector_labels(connectors, {'home_assistant'}),
|
|
'steps': [
|
|
'Open Notifications and review active rules, channels and unread events',
|
|
'Validate Home Assistant readiness only when the operator enabled the channel',
|
|
'Run explicit evaluation for safety/media events when stale notifications appear',
|
|
'Keep external delivery opt-in and audited',
|
|
],
|
|
'successRate': _success_rate(connector_by_id, {'home_assistant'}, fallback=70),
|
|
})
|
|
|
|
if degraded:
|
|
degraded_names = [connector.name for connector in degraded]
|
|
playbooks.append({
|
|
'id': 'connector-readiness-review',
|
|
'name': 'Connector Readiness Review',
|
|
'description': 'Controleert welke connectors configuratie, binding of health-herstel nodig hebben zonder wijzigingen uit te voeren.',
|
|
'risk': 'medium',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': degraded_names,
|
|
'steps': [
|
|
f"Review {len(degraded)} degraded connector(s): {', '.join(degraded_names[:8])}",
|
|
'Open connector setup for missing_config entries',
|
|
'Run read-only connector tests',
|
|
'Review policy and audit status before any follow-up action',
|
|
],
|
|
'successRate': max(0, 100 - (len(degraded) * 10)),
|
|
})
|
|
|
|
if {'portainer', 'widefrog'} <= set(connector_by_id):
|
|
widefrog_ready = connector_by_id['widefrog'].status == 'connected'
|
|
portainer_ready = connector_by_id['portainer'].status == 'connected'
|
|
playbooks.append({
|
|
'id': 'fix-widefrog',
|
|
'name': 'Fix Widefrog',
|
|
'description': 'Bereidt een veilige Widefrog herstelprocedure voor met read-only checks en policy-gated vervolgstappen.',
|
|
'risk': _policy_risk(session, 'widefrog.retry_failed', 'widefrog', 'medium'),
|
|
'allowedSources': ['manual', 'voice'],
|
|
'requiredConnectors': _connector_names(connectors, {'portainer', 'widefrog'}),
|
|
'steps': [
|
|
'Read Portainer container status',
|
|
'Read Widefrog health, queue and failed jobs',
|
|
'Inspect recent redacted logs when available',
|
|
'Draft follow-up action only when policy allows it',
|
|
'Require approval for retry or restart when policy demands it',
|
|
'Write audit result after an approved execution path',
|
|
],
|
|
'successRate': 90 if widefrog_ready and portainer_ready else 50,
|
|
})
|
|
|
|
if 'unraid' in connector_by_id:
|
|
unraid_ready = connector_by_id['unraid'].status == 'connected'
|
|
playbooks.append({
|
|
'id': 'unraid-health-scan',
|
|
'name': 'Unraid Health Scan',
|
|
'description': 'Leest array, disk, cache en docker-status uit zonder muterende acties te starten.',
|
|
'risk': 'low',
|
|
'allowedSources': ['manual', 'automation'],
|
|
'requiredConnectors': _connector_names(connectors, {'unraid'}),
|
|
'steps': [
|
|
'Read array status',
|
|
'Read parity status',
|
|
'Check disk warning count',
|
|
'Check cache usage',
|
|
'Check docker service status',
|
|
'Write read-only audit summary',
|
|
],
|
|
'successRate': 100 if unraid_ready else 60,
|
|
})
|
|
|
|
return playbooks
|