Complete tasks 1-5: UI cleanup, pie chart fix, log path allowlist, activation hardening

Task 1: Remove ActiveBansSection from JailsPage
- Delete buildBanColumns, fmtTimestamp, ActiveBansSection
- Remove Dialog/Delete/Dismiss imports, ActiveBan type
- Update JSDoc to reflect three sections

Task 2: Remove JailDistributionChart from Dashboard
- Delete import and JSX block from DashboardPage.tsx

Task 3: Fix transparent pie chart (TopCountriesPieChart)
- Add Cell import and per-slice <Cell fill={slice.fill}> children inside <Pie>
- Suppress @typescript-eslint/no-deprecated (recharts v3 types)

Task 4: Allow /config/log as safe log prefix
- Add '/config/log' to _SAFE_LOG_PREFIXES in config_service.py
- Update error message to list both allowed directories

Task 5: Block jail activation on missing filter/logpath
- activate_jail refuses to proceed when filter/logpath issues found
- ActivateJailDialog treats all validation issues as blocking
- Trigger immediate _run_probe after activation in config router
- /api/health now reports fail2ban online/offline from cached probe
- Add TestActivateJailBlocking tests; fix existing tests to mock validation
This commit is contained in:
2026-03-14 18:57:01 +01:00
parent 68d8056d2e
commit ee7412442a
11 changed files with 425 additions and 656 deletions

View File

@@ -4,6 +4,7 @@
*/
import {
Cell,
Legend,
Pie,
PieChart,
@@ -177,7 +178,12 @@ export function TopCountriesPieChart({
return `${name}: ${(percent * 100).toFixed(0)}%`;
}}
labelLine={false}
/>
>
{slices.map((slice, index) => (
// eslint-disable-next-line @typescript-eslint/no-deprecated
<Cell key={index} fill={slice.fill} />
))}
</Pie>
<Tooltip content={PieTooltip} />
<Legend formatter={legendFormatter} />
</PieChart>

View File

@@ -177,13 +177,9 @@ export function ActivateJailDialog({
if (!jail) return <></>;
// Errors block activation; warnings are advisory only.
const blockingIssues = validationIssues.filter(
(i) => i.field !== "logpath",
);
const advisoryIssues = validationIssues.filter(
(i) => i.field === "logpath",
);
// All validation issues block activation — logpath errors are now critical.
const blockingIssues = validationIssues;
const advisoryIssues: JailValidationIssue[] = [];
return (
<Dialog open={open} onOpenChange={(_ev, data) => { if (!data.open) handleClose(); }}>

View File

@@ -12,7 +12,6 @@ import { BanTable } from "../components/BanTable";
import { BanTrendChart } from "../components/BanTrendChart";
import { ChartStateWrapper } from "../components/ChartStateWrapper";
import { DashboardFilterBar } from "../components/DashboardFilterBar";
import { JailDistributionChart } from "../components/JailDistributionChart";
import { ServerStatusBar } from "../components/ServerStatusBar";
import { TopCountriesBarChart } from "../components/TopCountriesBarChart";
import { TopCountriesPieChart } from "../components/TopCountriesPieChart";
@@ -160,20 +159,6 @@ export function DashboardPage(): React.JSX.Element {
</div>
</div>
{/* ------------------------------------------------------------------ */}
{/* Jail Distribution section */}
{/* ------------------------------------------------------------------ */}
<div className={styles.section}>
<div className={styles.sectionHeader}>
<Text as="h2" size={500} weight="semibold">
Jail Distribution
</Text>
</div>
<div className={styles.tabContent}>
<JailDistributionChart timeRange={timeRange} origin={originFilter} />
</div>
</div>
{/* ------------------------------------------------------------------ */}
{/* Ban list section */}
{/* ------------------------------------------------------------------ */}

View File

@@ -1,12 +1,11 @@
/**
* Jails management page.
*
* Provides four sections in a vertically-stacked layout:
* Provides three sections in a vertically-stacked layout:
* 1. **Jail Overview** — table of all jails with quick status badges and
* per-row start/stop/idle/reload controls.
* 2. **Ban / Unban IP** — form to manually ban or unban an IP address.
* 3. **Currently Banned IPs** — live table of all active bans.
* 4. **IP Lookup** — check whether an IP is currently banned and view its
* 3. **IP Lookup** — check whether an IP is currently banned and view its
* geo-location details.
*/
@@ -20,12 +19,6 @@ import {
DataGridHeader,
DataGridHeaderCell,
DataGridRow,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
Field,
Input,
MessageBar,
@@ -42,8 +35,6 @@ import {
import {
ArrowClockwiseRegular,
ArrowSyncRegular,
DeleteRegular,
DismissRegular,
LockClosedRegular,
LockOpenRegular,
PauseRegular,
@@ -53,7 +44,7 @@ import {
} from "@fluentui/react-icons";
import { Link } from "react-router-dom";
import { useActiveBans, useIpLookup, useJails } from "../hooks/useJails";
import type { ActiveBan, JailSummary } from "../types/jail";
import type { JailSummary } from "../types/jail";
import { ApiError } from "../api/client";
// ---------------------------------------------------------------------------
@@ -160,21 +151,6 @@ function fmtSeconds(s: number): string {
return `${String(Math.round(s / 3600))}h`;
}
function fmtTimestamp(iso: string | null): string {
if (!iso) return "—";
try {
return new Date(iso).toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return iso;
}
}
// ---------------------------------------------------------------------------
// Jail overview columns
// ---------------------------------------------------------------------------
@@ -236,80 +212,6 @@ const jailColumns: TableColumnDefinition<JailSummary>[] = [
}),
];
// ---------------------------------------------------------------------------
// Active bans columns
// ---------------------------------------------------------------------------
function buildBanColumns(
onUnban: (ip: string, jail: string) => void,
): TableColumnDefinition<ActiveBan>[] {
return [
createTableColumn<ActiveBan>({
columnId: "ip",
renderHeaderCell: () => "IP",
renderCell: (b) => (
<Text style={{ fontFamily: "Consolas, 'Courier New', monospace", fontSize: "0.85rem" }}>
{b.ip}
</Text>
),
}),
createTableColumn<ActiveBan>({
columnId: "jail",
renderHeaderCell: () => "Jail",
renderCell: (b) => <Text size={200}>{b.jail}</Text>,
}),
createTableColumn<ActiveBan>({
columnId: "country",
renderHeaderCell: () => "Country",
renderCell: (b) => <Text size={200}>{b.country ?? "—"}</Text>,
}),
createTableColumn<ActiveBan>({
columnId: "bannedAt",
renderHeaderCell: () => "Banned At",
renderCell: (b) => <Text size={200}>{fmtTimestamp(b.banned_at)}</Text>,
}),
createTableColumn<ActiveBan>({
columnId: "expiresAt",
renderHeaderCell: () => "Expires At",
renderCell: (b) => <Text size={200}>{fmtTimestamp(b.expires_at)}</Text>,
}),
createTableColumn<ActiveBan>({
columnId: "count",
renderHeaderCell: () => "Count",
renderCell: (b) => (
<Tooltip
content={`Banned ${String(b.ban_count)} time${b.ban_count === 1 ? "" : "s"}`}
relationship="label"
>
<Badge
appearance="filled"
color={b.ban_count > 3 ? "danger" : b.ban_count > 1 ? "warning" : "informative"}
>
{String(b.ban_count)}
</Badge>
</Tooltip>
),
}),
createTableColumn<ActiveBan>({
columnId: "unban",
renderHeaderCell: () => "",
renderCell: (b) => (
<Button
size="small"
appearance="subtle"
icon={<DismissRegular />}
onClick={() => {
onUnban(b.ip, b.jail);
}}
aria-label={`Unban ${b.ip} from ${b.jail}`}
>
Unban
</Button>
),
}),
];
}
// ---------------------------------------------------------------------------
// Sub-component: Jail overview section
// ---------------------------------------------------------------------------
@@ -646,177 +548,6 @@ function BanUnbanForm({ jailNames, onBan, onUnban }: BanUnbanFormProps): React.J
);
}
// ---------------------------------------------------------------------------
// Sub-component: Active bans section
// ---------------------------------------------------------------------------
function ActiveBansSection(): React.JSX.Element {
const styles = useStyles();
const { bans, total, loading, error, refresh, unbanIp, unbanAll } = useActiveBans();
const [opError, setOpError] = useState<string | null>(null);
const [opSuccess, setOpSuccess] = useState<string | null>(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [clearing, setClearing] = useState(false);
const handleUnban = (ip: string, jail: string): void => {
setOpError(null);
setOpSuccess(null);
unbanIp(ip, jail).catch((err: unknown) => {
setOpError(err instanceof Error ? err.message : String(err));
});
};
const handleClearAll = (): void => {
setClearing(true);
setOpError(null);
setOpSuccess(null);
unbanAll()
.then((res) => {
setOpSuccess(res.message);
setConfirmOpen(false);
})
.catch((err: unknown) => {
setOpError(err instanceof Error ? err.message : String(err));
setConfirmOpen(false);
})
.finally(() => {
setClearing(false);
});
};
const banColumns = buildBanColumns(handleUnban);
return (
<div className={styles.section}>
<div className={styles.sectionHeader}>
<Text as="h2" size={500} weight="semibold">
Currently Banned IPs
{total > 0 && (
<Badge appearance="filled" color="danger" style={{ marginLeft: "8px" }}>
{String(total)}
</Badge>
)}
</Text>
<div style={{ display: "flex", gap: tokens.spacingHorizontalS }}>
<Button
size="small"
appearance="subtle"
icon={<ArrowClockwiseRegular />}
onClick={refresh}
>
Refresh
</Button>
{total > 0 && (
<Button
size="small"
appearance="outline"
icon={<DeleteRegular />}
onClick={() => {
setConfirmOpen(true);
}}
>
Clear All Bans
</Button>
)}
</div>
</div>
{/* Confirmation dialog */}
<Dialog
open={confirmOpen}
onOpenChange={(_ev, data) => {
if (!data.open) setConfirmOpen(false);
}}
>
<DialogSurface>
<DialogBody>
<DialogTitle>Clear All Bans</DialogTitle>
<DialogContent>
<Text>
This will immediately unban <strong>all {String(total)} IP
{total !== 1 ? "s" : ""}</strong> across every jail. This
action cannot be undone fail2ban will no longer block any
of those addresses until they trigger the rate-limit again.
</Text>
</DialogContent>
<DialogActions>
<Button
appearance="secondary"
onClick={() => {
setConfirmOpen(false);
}}
disabled={clearing}
>
Cancel
</Button>
<Button
appearance="primary"
onClick={handleClearAll}
disabled={clearing}
icon={clearing ? <Spinner size="tiny" /> : <DeleteRegular />}
>
{clearing ? "Clearing…" : "Clear All Bans"}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
{opError && (
<MessageBar intent="error">
<MessageBarBody>{opError}</MessageBarBody>
</MessageBar>
)}
{opSuccess && (
<MessageBar intent="success">
<MessageBarBody>{opSuccess}</MessageBarBody>
</MessageBar>
)}
{error && (
<MessageBar intent="error">
<MessageBarBody>Failed to load bans: {error}</MessageBarBody>
</MessageBar>
)}
{loading && bans.length === 0 ? (
<div className={styles.centred}>
<Spinner label="Loading active bans…" />
</div>
) : bans.length === 0 ? (
<div className={styles.centred}>
<Text size={300}>No IPs are currently banned.</Text>
</div>
) : (
<div className={styles.tableWrapper}>
<DataGrid
items={bans}
columns={banColumns}
getRowId={(b: ActiveBan) => `${b.jail}:${b.ip}`}
focusMode="composite"
>
<DataGridHeader>
<DataGridRow>
{({ renderHeaderCell }) => (
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
)}
</DataGridRow>
</DataGridHeader>
<DataGridBody<ActiveBan>>
{({ item }) => (
<DataGridRow<ActiveBan> key={`${item.jail}:${item.ip}`}>
{({ renderCell }) => (
<DataGridCell>{renderCell(item)}</DataGridCell>
)}
</DataGridRow>
)}
</DataGridBody>
</DataGrid>
</div>
)}
</div>
);
}
// ---------------------------------------------------------------------------
// Sub-component: IP Lookup section
// ---------------------------------------------------------------------------
@@ -935,8 +666,7 @@ function IpLookupSection(): React.JSX.Element {
/**
* Jails management page.
*
* Renders four sections: Jail Overview, Ban/Unban IP, Currently Banned IPs,
* and IP Lookup.
* Renders three sections: Jail Overview, Ban/Unban IP, and IP Lookup.
*/
export function JailsPage(): React.JSX.Element {
const styles = useStyles();
@@ -955,8 +685,6 @@ export function JailsPage(): React.JSX.Element {
<BanUnbanForm jailNames={jailNames} onBan={banIp} onUnban={unbanIp} />
<ActiveBansSection />
<IpLookupSection />
</div>
);