import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'; import { Card, CardHeader, CardTitle, CardContent } from './ui/card'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Alert, AlertDescription } from './ui/alert'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; import { ShieldAlert, Loader2, AlertCircle, CheckCircle, Ban, X, Trash2, BookOpen, Shield, Activity, Zap, RefreshCw, Globe, } from 'lucide-react'; import { Badge } from './ui/badge'; // Instance provisioning removed — Central-only mode import { useCrowdSec } from '../hooks/useCrowdSec'; import type { AddDecisionRequest, CrowdSecAlert } from '../services/api/networking'; type Timescale = '1h' | '6h' | '24h' | '7d'; const TIMESCALES: { key: Timescale; label: string; ms: number; bucketMs: number }[] = [ { key: '1h', label: '1h', ms: 60 * 60 * 1000, bucketMs: 5 * 60 * 1000 }, { key: '6h', label: '6h', ms: 6 * 60 * 60 * 1000, bucketMs: 30 * 60 * 1000 }, { key: '24h', label: '24h', ms: 24 * 60 * 60 * 1000, bucketMs: 2 * 60 * 60 * 1000 }, { key: '7d', label: '7d', ms: 7 * 24 * 60 * 60 * 1000, bucketMs: 24 * 60 * 60 * 1000 }, ]; const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; function formatBucketLabel(ts: number, scale: Timescale): string { const d = new Date(ts); if (scale === '7d') return d.toLocaleDateString(undefined, { timeZone: tz, weekday: 'short', month: 'short', day: 'numeric' }); return d.toLocaleTimeString(undefined, { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false }); } function binAlerts(alerts: CrowdSecAlert[], scale: Timescale) { const cfg = TIMESCALES.find(t => t.key === scale)!; const now = Date.now(); // Align start to a clean bucket boundary so labels fall on round times const rawStart = now - cfg.ms; const alignedStart = Math.floor(rawStart / cfg.bucketMs) * cfg.bucketMs; const numBuckets = Math.ceil((now - alignedStart) / cfg.bucketMs); const buckets = Array.from({ length: numBuckets }, (_, i) => ({ label: formatBucketLabel(alignedStart + i * cfg.bucketMs, scale), detections: 0, })); for (const a of alerts) { const t = new Date(a.createdAt).getTime(); if (t < alignedStart) continue; const idx = Math.floor((t - alignedStart) / cfg.bucketMs); if (idx >= 0 && idx < numBuckets) buckets[idx].detections++; } return buckets; } function formatRelativeTime(isoString?: string): string { if (!isoString) return 'unknown'; const diff = Date.now() - new Date(isoString).getTime(); const minutes = Math.floor(diff / 60000); if (minutes < 1) return 'just now'; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; return `${Math.floor(hours / 24)}d ago`; } function scenarioHubUrl(scenario: string): string | null { if (!scenario) return null; const parts = scenario.split('/'); // Only link hub scenarios in author/name format (e.g. crowdsecurity/http-probing-classb) // CAPI category labels (http:scan, ssh:bruteforce) have no individual hub pages if (parts.length === 2 && parts[0] !== 'custom') { return `https://app.crowdsec.net/hub/author/${parts[0]}/configurations/name/${parts[1]}`; } return null; } const KNOWN_ABBREV = new Set(['ssh', 'http', 'https', 'tcp', 'udp', 'ftp', 'ip', 'dns', 'tls', 'dos', 'ddos', 'bf']); function titleCaseScenario(s: string): string { return s.replace(/[:\-_]/g, ' ').split(' ').filter(Boolean).map(w => KNOWN_ABBREV.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() ).join(' '); } function friendlyReason(reason: string): string { if (!reason) return 'Community blocklist'; const map: Record = { 'crowdsecurity/http-scan-classb': 'HTTP Scanner', 'crowdsecurity/ssh-slow-bruteforce': 'SSH Brute Force (slow)', 'crowdsecurity/ssh-bf': 'SSH Brute Force', 'crowdsecurity/ssh-bruteforce': 'SSH Brute Force', 'crowdsecurity/http-dos': 'HTTP DoS', 'crowdsecurity/http-bruteforce': 'HTTP Brute Force', 'crowdsecurity/http-exploit': 'HTTP Exploit', 'crowdsecurity/http-crawl': 'HTTP Crawler', 'crowdsecurity/http-bad-user-agent': 'Bad User Agent', 'crowdsecurity/iptables-scan-multi_ports': 'Port Scanner', }; if (map[reason]) return map[reason]; const short = reason.includes('/') ? reason.split('/').slice(1).join(' ') : reason; return titleCaseScenario(short); } function parseDuration(dur?: string): string { if (!dur) return ''; // Go duration: "164h1m31s" → "6d 20h" const h = dur.match(/(\d+)h/); const m = dur.match(/(\d+)m/); const hours = h ? parseInt(h[1]) : 0; const mins = m ? parseInt(m[1]) : 0; if (hours >= 24) { const days = Math.floor(hours / 24); const rem = hours % 24; return rem > 0 ? `${days}d ${rem}h` : `${days}d`; } if (hours > 0) return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; return `${mins}m`; } interface BlockFormValues { ip: string; type: 'ban' | 'allow'; duration: string; reason: string; } export function CrowdSecComponent() { const instances: string[] = []; const { status, isLoadingStatus, summary, isLoadingSummary, alerts, isLoadingAlerts, refetchAlerts, graphAlerts, isLoadingGraphAlerts, decisions, addDecision, isAddingDecision, addDecisionError, deleteDecision, deleteBouncer, deleteMachine, provision, isProvisioning, } = useCrowdSec(); const [provisionResults, setProvisionResults] = useState>({}); const [provisioningInstance, setProvisioningInstance] = useState(null); const [blockSuccess, setBlockSuccess] = useState(null); const [timescale, setTimescale] = useState('24h'); const { register, handleSubmit, reset, setValue, watch, formState: { errors } } = useForm({ defaultValues: { ip: '', type: 'ban', duration: '24h', reason: 'manual' }, }); const selectedType = watch('type'); function handleProvision(instanceName: string) { setProvisioningInstance(instanceName); provision(instanceName, { onSuccess: (data) => { setProvisionResults((prev) => ({ ...prev, [instanceName]: { ok: true, message: data.message } })); setProvisioningInstance(null); }, onError: (err) => { setProvisionResults((prev) => ({ ...prev, [instanceName]: { ok: false, message: (err as Error).message } })); setProvisioningInstance(null); }, }); } function onBlockSubmit(values: BlockFormValues) { setBlockSuccess(null); const req: AddDecisionRequest = { ip: values.ip, type: values.type, reason: values.reason, duration: values.duration }; addDecision(req, { onSuccess: (data) => { setBlockSuccess(data.message); reset(); }, }); } const sortedReasons = summary ? Object.entries(summary.byReason).sort(([, a], [, b]) => b - a) : []; return (

CrowdSec

Collaborative intrusion detection — protecting your infrastructure with community threat intelligence

{/* Educational card */}

CrowdSec analyzes logs from your k8s clusters, detects attacks, and shares threat intelligence with the community. Wild Central runs the LAPI — the hub that agents report to and that bouncers query for the live blocklist. Your cluster contributes to and benefits from a shared blocklist of millions of known malicious IPs.

{/* LAPI Status */}
LAPI Status
{isLoadingStatus ? ( ) : ( {status?.active && } {status?.active ? 'Running' : 'Stopped'} )}
{!status?.active && !isLoadingStatus && (

CrowdSec is not running on Wild Central. Install it with{' '} apt install crowdsec crowdsec-firewall-bouncer {' '}or reinstall wild-cloud-central.

)} {status?.active && ( <>
Perimeter firewall enforcement (nftables on Wild Central)
{status.firewallBouncer && } {status.firewallBouncer ? 'Active' : 'Stopped'}
{!status.firewallBouncer && (

Install the firewall bouncer to block threats at the perimeter:{' '} apt install crowdsec-firewall-bouncer

)}

{status.machines.length > 0 && `${status.machines.length} k8s agent${status.machines.length !== 1 ? 's' : ''} reporting. `} {status.bouncers.length > 0 && `${status.bouncers.length} enforcement point${status.bouncers.length !== 1 ? 's' : ''} active.`}

)}
{status?.active && ( <> {/* Active Protection Summary */}
Active Protection

IPs currently blocked by the community blocklist (CAPI) and local decisions, enforced by your bouncers.

{isLoadingSummary ? (
Loading ban statistics…
) : summary ? (
{summary.total.toLocaleString()}
IPs blocked right now
{sortedReasons.length > 0 && (

By threat type

{sortedReasons.slice(0, 8).map(([reason, count]) => { const pct = Math.round((count / summary.total) * 100); return (
{scenarioHubUrl(reason) ? ( {friendlyReason(reason)} ) : ( {friendlyReason(reason)} )}
{count.toLocaleString()} {pct}%
); })} {sortedReasons.length > 8 && (

+{sortedReasons.length - 8} more categories

)}
)}
) : null} {/* Detections Over Time */}
Detections Over Time
{TIMESCALES.map(t => ( ))}
{isLoadingGraphAlerts ? (
Loading…
) : ( [v, 'detections']} /> )}
{/* Recent Alert Feed */}
Recent Detections

Attack events detected by your k8s agents and reported to this LAPI. Each event results in a ban that is shared with the CrowdSec community.

{isLoadingAlerts ? (
Loading…
) : alerts.length === 0 ? (

No detections yet.

Events appear here when k8s agents detect and report attacks. The community blocklist (CAPI) is already protecting you even before local detections.

) : (
Source IP Threat Agent When
{alerts.map((alert) => (
{alert.sourceIP || '—'} {alert.country && ( {alert.country} )}
{scenarioHubUrl(alert.scenario) ? ( {friendlyReason(alert.scenario)} ) : ( {friendlyReason(alert.scenario)} )} {alert.machineId?.replace(/^wc-/, '') ?? '—'} {formatRelativeTime(alert.createdAt)}
))}
)}
{/* Manual Decisions: Block / Allow */}
Block or Allow an IP

Manually ban an IP (e.g. an attacker you've identified) or add a local allowlist entry to prevent a legitimate IP from being blocked by the community list.

{errors.ip &&

{errors.ip.message}

}
{blockSuccess && ( {blockSuccess} )} {addDecisionError && ( {(addDecisionError as Error).message} )}
{/* Local decisions list */} {decisions.length > 0 && (

Active local decisions

{decisions.map((d) => (
{d.type === 'allow' ? : } {d.value} {d.type} {friendlyReason(d.reason)}
{d.duration && ( {parseDuration(d.duration)} )}
))}
)}
{/* Registered Agents */}
Registered Agents

Agents are CrowdSec engines running inside your k8s clusters. They analyze logs, detect threats, and report to this LAPI. Each provisioned Wild Cloud instance registers one agent here.

{status.machines.length === 0 ? (

No agents registered. Provision an instance below.

) : (
{status.machines.map((m) => (
{m.machineId} {m.isValidated ? 'validated' : 'pending'}
{m.last_heartbeat && heartbeat {formatRelativeTime(m.last_heartbeat)}} {m.version && {m.version}} {m.ipAddress && {m.ipAddress}}
))}
)}
{/* Registered Bouncers */}
Registered Bouncers

Bouncers enforce the blocklist. Wild Central's nftables bouncer blocks at the network perimeter. Each k8s cluster also runs a Traefik bouncer as a second layer.

{status.bouncers.length === 0 ? (

No bouncers registered. Provision an instance to add them.

) : (
{status.bouncers.map((b) => { const isFirewallBouncer = b.type === 'crowdsec-firewall-bouncer'; return (
{b.name} {b.revoked ? 'revoked' : 'active'} {isFirewallBouncer && ( perimeter )}

{isFirewallBouncer ? 'Crowdsec Firewall Bouncer' : b.type?.replace(/-/g, ' ') || ''}

{!isFirewallBouncer && ( )}
); })}
)}
{/* Provision Instances */}
Provision Instances

Provisioning connects a Wild Cloud instance's CrowdSec agent to this LAPI — generating credentials, registering agent and bouncer, and writing the LAPI URL into the instance's app config. After provisioning, redeploy the CrowdSec app on the instance. Safe to re-run.

{instances.length === 0 ? (

No instances found.

) : ( instances.map((instanceName) => { const agentName = `wc-${instanceName}`; const bouncerName = `wc-bouncer-${instanceName}`; const agent = status.machines.find((m) => m.machineId === agentName); const bouncer = status.bouncers.find((b) => b.name === bouncerName); const isActive = provisioningInstance === instanceName; const result = provisionResults[instanceName]; return (
{instanceName}
Agent: {agent ? ( {agent.isValidated ? 'validated' : 'pending'} {agent.last_heartbeat && ` · ${formatRelativeTime(agent.last_heartbeat)}`} ) : ( not provisioned )}
Bouncer: {bouncer ? ( {bouncer.revoked ? 'revoked' : 'active'} ) : ( not provisioned )}
{result && ( {result.ok ? : } {result.message} )}
); }) )}
)}
); }