From c977cf619069590f4d84fd852f58e546e51957f5 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 15 Apr 2026 21:04:48 +0000 Subject: [PATCH] feat: add whitelist management functionality to CrowdSecConfig --- frontend/src/pages/CrowdSecConfig.tsx | 208 +++++++++++++++++++++++++- 1 file changed, 206 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/CrowdSecConfig.tsx b/frontend/src/pages/CrowdSecConfig.tsx index 3044347b..e26286c3 100644 --- a/frontend/src/pages/CrowdSecConfig.tsx +++ b/frontend/src/pages/CrowdSecConfig.tsx @@ -6,10 +6,11 @@ import { useTranslation } from 'react-i18next' import { useNavigate, Link } from 'react-router-dom' import { createBackup } from '../api/backups' -import { exportCrowdsecConfig, importCrowdsecConfig, listCrowdsecFiles, readCrowdsecFile, writeCrowdsecFile, listCrowdsecDecisions, banIP, unbanIP, type CrowdSecDecision, statusCrowdsec, type CrowdSecStatus, startCrowdsec } from '../api/crowdsec' +import { exportCrowdsecConfig, importCrowdsecConfig, listCrowdsecFiles, readCrowdsecFile, writeCrowdsecFile, listCrowdsecDecisions, banIP, unbanIP, type CrowdSecDecision, type CrowdSecWhitelistEntry, statusCrowdsec, type CrowdSecStatus, startCrowdsec } from '../api/crowdsec' import { getFeatureFlags } from '../api/featureFlags' import { listCrowdsecPresets, pullCrowdsecPreset, applyCrowdsecPreset, getCrowdsecPresetCache } from '../api/presets' import { getSecurityStatus } from '../api/security' +import { getMyIP } from '../api/system' import { CrowdSecBouncerKeyDisplay } from '../components/CrowdSecBouncerKeyDisplay' import { ConfigReloadOverlay } from '../components/LoadingStates' import { Button } from '../components/ui/Button' @@ -19,6 +20,7 @@ import { Skeleton } from '../components/ui/Skeleton' import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/Tabs' import { CROWDSEC_PRESETS, type CrowdsecPreset } from '../data/crowdsecPresets' import { useConsoleStatus, useEnrollConsole, useClearConsoleEnrollment } from '../hooks/useConsoleEnrollment' +import { useWhitelistEntries, useAddWhitelist, useDeleteWhitelist } from '../hooks/useCrowdSecWhitelist' import { buildCrowdsecExportFilename, downloadCrowdsecExport, promptCrowdsecFilename } from '../utils/crowdsecExport' import { toast } from '../utils/toast' @@ -36,6 +38,8 @@ export default function CrowdSecConfig() { const [showBanModal, setShowBanModal] = useState(false) const [banForm, setBanForm] = useState({ ip: '', duration: '24h', reason: '' }) const [confirmUnban, setConfirmUnban] = useState(null) + const [whitelistForm, setWhitelistForm] = useState<{ ip_or_cidr: string; reason: string }>({ ip_or_cidr: '', reason: '' }) + const [confirmDeleteWhitelist, setConfirmDeleteWhitelist] = useState(null) const [isApplyingPreset, setIsApplyingPreset] = useState(false) const [presetPreview, setPresetPreview] = useState('') const [presetMeta, setPresetMeta] = useState<{ cacheKey?: string; etag?: string; retrievedAt?: string; source?: string } | null>(null) @@ -361,6 +365,25 @@ export default function CrowdSecConfig() { }, }) + const whitelistQuery = useWhitelistEntries() + const addWhitelistMutation = useAddWhitelist() + const deleteWhitelistMutation = useDeleteWhitelist() + + const whitelistInlineError = addWhitelistMutation.error instanceof Error + ? addWhitelistMutation.error.message + : addWhitelistMutation.error != null + ? 'Failed to add entry' + : null + + const handleAddMyIP = async () => { + try { + const result = await getMyIP() + setWhitelistForm((prev) => ({ ...prev, ip_or_cidr: result.ip })) + } catch { + toast.error('Failed to detect your IP address') + } + } + const handleExport = async () => { const defaultName = buildCrowdsecExportFilename() const filename = promptCrowdsecFilename(defaultName) @@ -517,7 +540,9 @@ export default function CrowdSecConfig() { pullPresetMutation.isPending || isApplyingPreset || banMutation.isPending || - unbanMutation.isPending + unbanMutation.isPending || + addWhitelistMutation.isPending || + deleteWhitelistMutation.isPending // Determine contextual message const getMessage = () => { @@ -539,6 +564,12 @@ export default function CrowdSecConfig() { if (unbanMutation.isPending) { return { message: 'Guardian lowers shield...', submessage: 'Unbanning IP address' } } + if (addWhitelistMutation.isPending) { + return { message: 'Guardian updates list...', submessage: 'Adding IP to whitelist' } + } + if (deleteWhitelistMutation.isPending) { + return { message: 'Guardian updates list...', submessage: 'Removing from whitelist' } + } return { message: 'Strengthening the guard...', submessage: 'Configuration in progress' } } @@ -565,6 +596,7 @@ export default function CrowdSecConfig() { {t('security.crowdsec.tabs.config', 'Configuration')} {t('security.crowdsec.tabs.dashboard', 'Dashboard')} + {isLocalMode && {t('crowdsecConfig.whitelist.tabLabel', 'Whitelist')}} @@ -1241,6 +1273,135 @@ export default function CrowdSecConfig() { + + {isLocalMode && ( + + +
+
+ +

{t('crowdsecConfig.whitelist.title', 'IP Whitelist')}

+
+

+ {t('crowdsecConfig.whitelist.description', 'Whitelisted IPs and CIDRs are never blocked by CrowdSec, even if they trigger alerts.')} +

+ + {/* Add entry form */} +
+
+ { + setWhitelistForm((prev) => ({ ...prev, ip_or_cidr: e.target.value })) + if (addWhitelistMutation.error) addWhitelistMutation.reset() + }} + error={whitelistInlineError ?? undefined} + errorTestId="whitelist-ip-error" + aria-required={true} + data-testid="whitelist-ip-input" + /> +
+
+ setWhitelistForm((prev) => ({ ...prev, reason: e.target.value }))} + data-testid="whitelist-reason-input" + /> +
+
+ + +
+
+ + {/* Entries table */} + {whitelistQuery.isLoading ? ( +
+ + + +
+ ) : !whitelistQuery.data?.length ? ( +

+ {t('crowdsecConfig.whitelist.none', 'No whitelist entries')} +

+ ) : ( +
+ + + + + + + + + + + {whitelistQuery.data.map((entry) => ( + + + + + + + ))} + +
+ {t('crowdsecConfig.whitelist.columnIp', 'IP / CIDR')} + + {t('crowdsecConfig.whitelist.columnReason', 'Reason')} + + {t('crowdsecConfig.whitelist.columnAdded', 'Added')} + + {t('crowdsecConfig.bannedIps.actions')} +
{entry.ip_or_cidr}{entry.reason || '-'} + {entry.created_at ? new Date(entry.created_at).toLocaleString() : '-'} + + +
+
+ )} +
+
+
+ )} + @@ -1386,6 +1547,49 @@ export default function CrowdSecConfig() { )} + + {/* Delete Whitelist Entry Modal */} + {confirmDeleteWhitelist && ( +
+
setConfirmDeleteWhitelist(null)} role="button" tabIndex={-1} aria-label={t('common.close')} /> +
{ + if (e.key === 'Escape') setConfirmDeleteWhitelist(null) + }} + > +

+ {t('crowdsecConfig.whitelist.deleteModal.title', 'Remove Whitelist Entry')} +

+

+ {t('crowdsecConfig.whitelist.deleteModal.body', 'Remove {{ip}} from the whitelist? CrowdSec may then block this IP if it triggers alerts.', { ip: confirmDeleteWhitelist.ip_or_cidr })} +

+
+ + +
+
+
+ )} ) }