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:
@@ -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>
|
||||
|
||||
@@ -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(); }}>
|
||||
|
||||
@@ -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 */}
|
||||
{/* ------------------------------------------------------------------ */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user