import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useState, useEffect } from 'react' import { useNavigate, Outlet } from 'react-router-dom' import { Shield, ShieldAlert, ShieldCheck, Lock, Activity, ExternalLink } from 'lucide-react' import { getSecurityStatus, type SecurityStatus } from '../api/security' import { useSecurityConfig, useUpdateSecurityConfig, useGenerateBreakGlassToken, useRuleSets } from '../hooks/useSecurity' import { startCrowdsec, stopCrowdsec, statusCrowdsec } from '../api/crowdsec' import { updateSetting } from '../api/settings' import { Switch } from '../components/ui/Switch' import { toast } from '../utils/toast' import { Card } from '../components/ui/Card' import { Button } from '../components/ui/Button' import { ConfigReloadOverlay } from '../components/LoadingStates' import { LiveLogViewer } from '../components/LiveLogViewer' import { SecurityNotificationSettingsModal } from '../components/SecurityNotificationSettingsModal' export default function Security() { const navigate = useNavigate() const { data: status, isLoading } = useQuery({ queryKey: ['security-status'], queryFn: getSecurityStatus, }) const { data: securityConfig } = useSecurityConfig() const { data: ruleSetsData } = useRuleSets() const [adminWhitelist, setAdminWhitelist] = useState('') const [showNotificationSettings, setShowNotificationSettings] = useState(false) useEffect(() => { if (securityConfig && securityConfig.config) { setAdminWhitelist(securityConfig.config.admin_whitelist || '') } }, [securityConfig]) const updateSecurityConfigMutation = useUpdateSecurityConfig() const generateBreakGlassMutation = useGenerateBreakGlassToken() const queryClient = useQueryClient() const [crowdsecStatus, setCrowdsecStatus] = useState<{ running: boolean; pid?: number } | null>(null) // Generic toggle mutation for per-service settings const toggleServiceMutation = useMutation({ mutationFn: async ({ key, enabled }: { key: string; enabled: boolean }) => { await updateSetting(key, enabled ? 'true' : 'false', 'security', 'bool') }, onMutate: async ({ key, enabled }: { key: string; enabled: boolean }) => { await queryClient.cancelQueries({ queryKey: ['security-status'] }) const previous = queryClient.getQueryData(['security-status']) queryClient.setQueryData(['security-status'], (old: unknown) => { if (!old || typeof old !== 'object') return old const parts = key.split('.') const section = parts[1] as keyof SecurityStatus const field = parts[2] const copy = { ...(old as SecurityStatus) } if (copy[section] && typeof copy[section] === 'object') { copy[section] = { ...copy[section], [field]: enabled } as never } return copy }) return { previous } }, onError: (_err, _vars, context: unknown) => { if (context && typeof context === 'object' && 'previous' in context) { queryClient.setQueryData(['security-status'], context.previous) } const msg = _err instanceof Error ? _err.message : String(_err) toast.error(`Failed to update setting: ${msg}`) }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['settings'] }) queryClient.invalidateQueries({ queryKey: ['security-status'] }) toast.success('Security setting updated') }, }) const fetchCrowdsecStatus = async () => { try { const s = await statusCrowdsec() setCrowdsecStatus(s) } catch { setCrowdsecStatus(null) } } useEffect(() => { fetchCrowdsecStatus() }, []) const crowdsecPowerMutation = useMutation({ mutationFn: async (enabled: boolean) => { await updateSetting('security.crowdsec.enabled', enabled ? 'true' : 'false', 'security', 'bool') if (enabled) { await startCrowdsec() } else { await stopCrowdsec() } return enabled }, onMutate: async (enabled: boolean) => { await queryClient.cancelQueries({ queryKey: ['security-status'] }) const previous = queryClient.getQueryData(['security-status']) queryClient.setQueryData(['security-status'], (old: unknown) => { if (!old || typeof old !== 'object') return old const copy = { ...(old as SecurityStatus) } if (copy.crowdsec && typeof copy.crowdsec === 'object') { copy.crowdsec = { ...copy.crowdsec, enabled } as never } return copy }) setCrowdsecStatus(prev => prev ? { ...prev, running: enabled } : prev) return { previous } }, onError: (err: unknown, enabled: boolean, context: unknown) => { if (context && typeof context === 'object' && 'previous' in context) { queryClient.setQueryData(['security-status'], context.previous) } const msg = err instanceof Error ? err.message : String(err) toast.error(enabled ? `Failed to start CrowdSec: ${msg}` : `Failed to stop CrowdSec: ${msg}`) fetchCrowdsecStatus() }, onSuccess: async (enabled: boolean) => { await fetchCrowdsecStatus() queryClient.invalidateQueries({ queryKey: ['security-status'] }) queryClient.invalidateQueries({ queryKey: ['settings'] }) toast.success(enabled ? 'CrowdSec started' : 'CrowdSec stopped') }, }) // Determine if any security operation is in progress const isApplyingConfig = toggleServiceMutation.isPending || updateSecurityConfigMutation.isPending || generateBreakGlassMutation.isPending || crowdsecPowerMutation.isPending // Determine contextual message const getMessage = () => { if (toggleServiceMutation.isPending) { return { message: 'Three heads turn...', submessage: 'Cerberus configuration updating' } } if (crowdsecPowerMutation.isPending) { return crowdsecPowerMutation.variables ? { message: 'Summoning the guardian...', submessage: 'CrowdSec is starting' } : { message: 'Guardian rests...', submessage: 'CrowdSec is stopping' } } return { message: 'Strengthening the guard...', submessage: 'Protective wards activating' } } const { message, submessage } = getMessage() if (isLoading) { return
Loading security status...
} if (!status) { return
Failed to load security status
} const cerberusDisabled = !status.cerberus?.enabled const crowdsecToggleDisabled = cerberusDisabled || crowdsecPowerMutation.isPending const crowdsecControlsDisabled = cerberusDisabled || crowdsecPowerMutation.isPending // const suiteDisabled = !(status?.cerberus?.enabled ?? false) // Replace the previous early-return that instructed enabling via env vars. // If allDisabled, show a banner and continue to render the dashboard with disabled controls. const headerBanner = (!status.cerberus?.enabled) ? (

Cerberus Disabled

Cerberus powers CrowdSec, WAF, ACLs, and Rate Limiting. Enable the Cerberus toggle in System Settings to awaken the guardian, then configure each head below.

) : null return ( <> {isApplyingConfig && ( )}
{headerBanner}

Cerberus Dashboard

setAdminWhitelist(e.target.value)} />
{/* CrowdSec - Layer 1: IP Reputation (first line of defense) */}
🛡️ Layer 1: IP Reputation

CrowdSec

{ crowdsecPowerMutation.mutate(e.target.checked) }} data-testid="toggle-crowdsec" />
{status.crowdsec.enabled ? 'Active' : 'Disabled'}

{status.crowdsec.enabled ? `Protects against: Known attackers, botnets, brute-force` : 'Intrusion Prevention System'}

{crowdsecStatus && (

{crowdsecStatus.running ? `Running (pid ${crowdsecStatus.pid})` : 'Stopped'}

)}
{/* ACL - Layer 2: Access Control (IP/Geo filtering) */}
🔒 Layer 2: Access Control

Access Control

toggleServiceMutation.mutate({ key: 'security.acl.enabled', enabled: e.target.checked })} data-testid="toggle-acl" />
{status.acl.enabled ? 'Active' : 'Disabled'}

Protects against: Unauthorized IPs, geo-based attacks, insider threats

{status.acl.enabled && (
)} {!status.acl.enabled && (
)}
{/* WAF - Layer 3: Request Inspection */}
🛡️ Layer 3: Request Inspection

WAF (Coraza)

toggleServiceMutation.mutate({ key: 'security.waf.enabled', enabled: e.target.checked })} data-testid="toggle-waf" />
{status.waf.enabled ? 'Active' : 'Disabled'}

{status.waf.enabled ? `Protects against: SQL injection, XSS, RCE, zero-day exploits*` : 'Web Application Firewall'}

{status.waf.enabled && (
{(!ruleSetsData?.rulesets || ruleSetsData.rulesets.length === 0) && (

No rule sets configured. Add one below.

)}
)}
{/* Rate Limiting - Layer 4: Volume Control */}
⚡ Layer 4: Volume Control

Rate Limiting

toggleServiceMutation.mutate({ key: 'security.rate_limit.enabled', enabled: e.target.checked })} data-testid="toggle-rate-limit" />
{status.rate_limit.enabled ? 'Active' : 'Disabled'}

Protects against: DDoS attacks, credential stuffing, API abuse

{status.rate_limit.enabled && (
)} {!status.rate_limit.enabled && (
)}
{/* Live Activity Section */} {status.cerberus?.enabled && (
)} {/* Notification Settings Modal */} setShowNotificationSettings(false)} />
) }