import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useState, useEffect, useMemo } from 'react' import { useNavigate, Outlet } from 'react-router-dom' import { Shield, ShieldAlert, ShieldCheck, Lock, Activity, ExternalLink, Settings } from 'lucide-react' import { getSecurityStatus, type SecurityStatus } from '../api/security' import { useSecurityConfig, useUpdateSecurityConfig, useGenerateBreakGlassToken } from '../hooks/useSecurity' import { startCrowdsec, stopCrowdsec, statusCrowdsec } from '../api/crowdsec' import { updateSetting } from '../api/settings' import { toast } from '../utils/toast' import { ConfigReloadOverlay } from '../components/LoadingStates' import { LiveLogViewer } from '../components/LiveLogViewer' import { SecurityNotificationSettingsModal } from '../components/SecurityNotificationSettingsModal' import { PageShell } from '../components/layout/PageShell' import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Button, Badge, Alert, Switch, Skeleton, Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, } from '../components/ui' // Skeleton loader for security layer cards function SecurityCardSkeleton() { return (
) } // Loading skeleton for the entire security page function SecurityPageSkeleton() { return (
) } export default function Security() { const navigate = useNavigate() const { data: status, isLoading } = useQuery({ queryKey: ['security-status'], queryFn: getSecurityStatus, }) const { data: securityConfig } = useSecurityConfig() 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) // Stable reference to prevent WebSocket reconnection loops in LiveLogViewer const emptySecurityFilters = useMemo(() => ({}), []) // 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) => { // Update setting first await updateSetting('security.crowdsec.enabled', enabled ? 'true' : 'false', 'security', 'bool') if (enabled) { toast.info('Starting CrowdSec... This may take up to 30 seconds') const result = await startCrowdsec() // VERIFY: Check if it actually started const status = await statusCrowdsec() if (!status.running) { // Revert the setting since process didn't start await updateSetting('security.crowdsec.enabled', 'false', 'security', 'bool') throw new Error('CrowdSec process failed to start. Check server logs for details.') } return result } else { await stopCrowdsec() // VERIFY: Check if it actually stopped (with brief delay for cleanup) await new Promise(resolve => setTimeout(resolve, 500)) const status = await statusCrowdsec() if (status.running) { throw new Error('CrowdSec process still running. Check server logs for details.') } return { enabled: false } } }, // NO optimistic updates - wait for actual confirmation onError: (err: unknown, enabled: boolean) => { const msg = err instanceof Error ? err.message : String(err) toast.error(enabled ? `Failed to start CrowdSec: ${msg}` : `Failed to stop CrowdSec: ${msg}`) // Force refresh status from backend to ensure UI matches reality queryClient.invalidateQueries({ queryKey: ['security-status'] }) fetchCrowdsecStatus() }, onSuccess: async (result: { lapi_ready?: boolean; enabled?: boolean } | boolean) => { // Refresh all related queries to ensure consistency await Promise.all([ queryClient.invalidateQueries({ queryKey: ['security-status'] }), queryClient.invalidateQueries({ queryKey: ['settings'] }), fetchCrowdsecStatus(), ]) if (typeof result === 'object' && result.lapi_ready === true) { toast.success('CrowdSec started and LAPI is ready') } else if (typeof result === 'object' && result.lapi_ready === false) { toast.warning('CrowdSec started but LAPI is still initializing. Please wait before enrolling.') } else if (typeof result === 'object' && result.enabled === false) { toast.success('CrowdSec stopped') } else { toast.success('CrowdSec started') } }, }) // 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 } if (!status) { return ( Failed to load security configuration. Please try refreshing the page. ) } const cerberusDisabled = !status.cerberus?.enabled const crowdsecToggleDisabled = cerberusDisabled || crowdsecPowerMutation.isPending const crowdsecControlsDisabled = cerberusDisabled || crowdsecPowerMutation.isPending // Header actions const headerActions = (
) return ( {isApplyingConfig && ( )} {/* Cerberus Status Header */}

Cerberus Dashboard

{status.cerberus?.enabled ? 'Active' : 'Disabled'}

{status.cerberus?.enabled ? 'All security heads are ready for configuration' : 'Enable Cerberus in System Settings to activate security features'}

{/* Cerberus Disabled Alert */} {!status.cerberus?.enabled && (

Cerberus powers CrowdSec, Coraza WAF, Access Control, and Rate Limiting. Enable the Cerberus toggle in System Settings to activate these features.

)} {/* Admin Whitelist Section */} {status.cerberus?.enabled && ( Admin Whitelist Configure IP addresses that bypass security checks
setAdminWhitelist(e.target.value)} placeholder="192.168.1.0/24, 10.0.0.1" />

Generate a break-glass token for emergency access

)} {/* Security Layer Cards */}
{/* CrowdSec - Layer 1: IP Reputation */}
Layer 1 IDS
{(crowdsecStatus?.running ?? status.crowdsec.enabled) ? 'Enabled' : 'Disabled'}
CrowdSec IP Reputation & Threat Intelligence

{(crowdsecStatus?.running ?? status.crowdsec.enabled) ? 'Protects against: Known attackers, botnets, brute-force' : 'Intrusion Prevention System powered by community threat intelligence'}

{crowdsecStatus && (

{crowdsecStatus.running ? `Running (PID ${crowdsecStatus.pid})` : 'Process stopped'}

)}
crowdsecPowerMutation.mutate(e.target.checked)} data-testid="toggle-crowdsec" />

{cerberusDisabled ? 'Enable Cerberus first' : 'Toggle CrowdSec protection'}

{/* ACL - Layer 2: Access Control */}
Layer 2 ACL
{status.acl.enabled ? 'Enabled' : 'Disabled'}
Access Control IP & Geo-based filtering

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

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

{cerberusDisabled ? 'Enable Cerberus first' : 'Toggle Access Control'}

{/* Coraza - Layer 3: Request Inspection */}
Layer 3 WAF
{status.waf.enabled ? 'Enabled' : 'Disabled'}
Coraza WAF Request inspection & filtering

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

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

{cerberusDisabled ? 'Enable Cerberus first' : 'Toggle Coraza WAF'}

{/* Rate Limiting - Layer 4: Volume Control */}
Layer 4 Rate
{status.rate_limit.enabled ? 'Enabled' : 'Disabled'}
Rate Limiting Request volume control

Protects against: DDoS attacks, credential stuffing, API abuse

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

{cerberusDisabled ? 'Enable Cerberus first' : 'Toggle Rate Limiting'}

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