Add better jail configuration: file CRUD, enable/disable, log paths
Task 4 (Better Jail Configuration) implementation:
- Add fail2ban_config_dir setting to app/config.py
- New file_config_service: list/view/edit/create jail.d, filter.d, action.d files
with path-traversal prevention and 512 KB content size limit
- New file_config router: GET/PUT/POST endpoints for jail files, filter files,
and action files; PUT .../enabled for toggle on/off
- Extend config_service with delete_log_path() and add_log_path()
- Add DELETE /api/config/jails/{name}/logpath and POST /api/config/jails/{name}/logpath
- Extend geo router with re-resolve endpoint; add geo_re_resolve background task
- Update blocklist_service with revised scheduling helpers
- Update Docker compose files with BANGUI_FAIL2BAN_CONFIG_DIR env var and
rw volume mount for the fail2ban config directory
- Frontend: new Jail Files, Filters, Actions tabs in ConfigPage; file editor
with accordion-per-file, editable textarea, save/create; add/delete log paths
- Frontend: types in types/config.ts; API calls in api/config.ts and api/endpoints.ts
- 63 new backend tests (test_file_config_service, test_file_config, test_geo_re_resolve)
- 6 new frontend tests in ConfigPageLogPath.test.tsx
- ruff, mypy --strict, tsc --noEmit, eslint: all clean; 617 backend tests pass
This commit is contained in:
@@ -2,12 +2,19 @@
|
||||
* API functions for the configuration and server settings endpoints.
|
||||
*/
|
||||
|
||||
import { get, post, put } from "./client";
|
||||
import { del, get, post, put } from "./client";
|
||||
import { ENDPOINTS } from "./endpoints";
|
||||
import type {
|
||||
AddLogPathRequest,
|
||||
ConfFileContent,
|
||||
ConfFileCreateRequest,
|
||||
ConfFilesResponse,
|
||||
ConfFileUpdateRequest,
|
||||
GlobalConfig,
|
||||
GlobalConfigUpdate,
|
||||
JailConfigFileContent,
|
||||
JailConfigFileEnabledUpdate,
|
||||
JailConfigFilesResponse,
|
||||
JailConfigListResponse,
|
||||
JailConfigResponse,
|
||||
JailConfigUpdate,
|
||||
@@ -88,6 +95,15 @@ export async function addLogPath(
|
||||
await post<undefined>(ENDPOINTS.configJailLogPath(jailName), req);
|
||||
}
|
||||
|
||||
export async function deleteLogPath(
|
||||
jailName: string,
|
||||
logPath: string
|
||||
): Promise<void> {
|
||||
await del<undefined>(
|
||||
`${ENDPOINTS.configJailLogPath(jailName)}?log_path=${encodeURIComponent(logPath)}`
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Log preview
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -139,3 +155,74 @@ export async function updateMapColorThresholds(
|
||||
update,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Jail config files (Task 4a)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function fetchJailConfigFiles(): Promise<JailConfigFilesResponse> {
|
||||
return get<JailConfigFilesResponse>(ENDPOINTS.configJailFiles);
|
||||
}
|
||||
|
||||
export async function fetchJailConfigFileContent(
|
||||
filename: string
|
||||
): Promise<JailConfigFileContent> {
|
||||
return get<JailConfigFileContent>(ENDPOINTS.configJailFile(filename));
|
||||
}
|
||||
|
||||
export async function setJailConfigFileEnabled(
|
||||
filename: string,
|
||||
update: JailConfigFileEnabledUpdate
|
||||
): Promise<void> {
|
||||
await put<undefined>(ENDPOINTS.configJailFileEnabled(filename), update);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter files (Task 4d)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function fetchFilterFiles(): Promise<ConfFilesResponse> {
|
||||
return get<ConfFilesResponse>(ENDPOINTS.configFilters);
|
||||
}
|
||||
|
||||
export async function fetchFilterFile(name: string): Promise<ConfFileContent> {
|
||||
return get<ConfFileContent>(ENDPOINTS.configFilter(name));
|
||||
}
|
||||
|
||||
export async function updateFilterFile(
|
||||
name: string,
|
||||
req: ConfFileUpdateRequest
|
||||
): Promise<void> {
|
||||
await put<undefined>(ENDPOINTS.configFilter(name), req);
|
||||
}
|
||||
|
||||
export async function createFilterFile(
|
||||
req: ConfFileCreateRequest
|
||||
): Promise<ConfFileContent> {
|
||||
return post<ConfFileContent>(ENDPOINTS.configFilters, req);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action files (Task 4e)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function fetchActionFiles(): Promise<ConfFilesResponse> {
|
||||
return get<ConfFilesResponse>(ENDPOINTS.configActions);
|
||||
}
|
||||
|
||||
export async function fetchActionFile(name: string): Promise<ConfFileContent> {
|
||||
return get<ConfFileContent>(ENDPOINTS.configAction(name));
|
||||
}
|
||||
|
||||
export async function updateActionFile(
|
||||
name: string,
|
||||
req: ConfFileUpdateRequest
|
||||
): Promise<void> {
|
||||
await put<undefined>(ENDPOINTS.configAction(name), req);
|
||||
}
|
||||
|
||||
export async function createActionFile(
|
||||
req: ConfFileCreateRequest
|
||||
): Promise<ConfFileContent> {
|
||||
return post<ConfFileContent>(ENDPOINTS.configActions, req);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,17 @@ export const ENDPOINTS = {
|
||||
configPreviewLog: "/config/preview-log",
|
||||
configMapColorThresholds: "/config/map-color-thresholds",
|
||||
|
||||
// File-based config (jail.d, filter.d, action.d)
|
||||
configJailFiles: "/config/jail-files",
|
||||
configJailFile: (filename: string): string =>
|
||||
`/config/jail-files/${encodeURIComponent(filename)}`,
|
||||
configJailFileEnabled: (filename: string): string =>
|
||||
`/config/jail-files/${encodeURIComponent(filename)}/enabled`,
|
||||
configFilters: "/config/filters",
|
||||
configFilter: (name: string): string => `/config/filters/${encodeURIComponent(name)}`,
|
||||
configActions: "/config/actions",
|
||||
configAction: (name: string): string => `/config/actions/${encodeURIComponent(name)}`,
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Server settings
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -16,9 +16,94 @@ import { ConfigPage } from "../../pages/ConfigPage";
|
||||
import type { JailConfig } from "../../types/config";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module mocks
|
||||
// Module mocks — use vi.hoisted so refs are available when vi.mock runs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const {
|
||||
mockAddLogPath,
|
||||
mockDeleteLogPath,
|
||||
mockUpdateJailConfig,
|
||||
mockReloadConfig,
|
||||
mockFetchGlobalConfig,
|
||||
mockFetchServerSettings,
|
||||
mockFetchJailConfigs,
|
||||
mockFetchMapColorThresholds,
|
||||
mockFetchJailConfigFiles,
|
||||
mockFetchFilterFiles,
|
||||
mockFetchActionFiles,
|
||||
mockUpdateMapColorThresholds,
|
||||
mockUpdateGlobalConfig,
|
||||
mockUpdateServerSettings,
|
||||
mockFlushLogs,
|
||||
mockSetJailConfigFileEnabled,
|
||||
} = vi.hoisted(() => ({
|
||||
mockAddLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockDeleteLogPath: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockUpdateJailConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockReloadConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockFetchGlobalConfig: vi.fn().mockResolvedValue({
|
||||
config: {
|
||||
ban_time: 600,
|
||||
max_retry: 5,
|
||||
find_time: 300,
|
||||
backend: "auto",
|
||||
},
|
||||
}),
|
||||
mockFetchServerSettings: vi.fn().mockResolvedValue({
|
||||
settings: {
|
||||
log_level: "INFO",
|
||||
log_target: "STDOUT",
|
||||
syslog_socket: null,
|
||||
db_path: "/var/lib/fail2ban/fail2ban.sqlite3",
|
||||
db_purge_age: 86400,
|
||||
db_max_matches: 10,
|
||||
},
|
||||
}),
|
||||
mockFetchJailConfigs: vi.fn(),
|
||||
mockFetchMapColorThresholds: vi.fn().mockResolvedValue({
|
||||
threshold_high: 100,
|
||||
threshold_medium: 50,
|
||||
threshold_low: 20,
|
||||
}),
|
||||
mockFetchJailConfigFiles: vi.fn().mockResolvedValue({ files: [] }),
|
||||
mockFetchFilterFiles: vi.fn().mockResolvedValue({ files: [] }),
|
||||
mockFetchActionFiles: vi.fn().mockResolvedValue({ files: [] }),
|
||||
mockUpdateMapColorThresholds: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockUpdateGlobalConfig: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockUpdateServerSettings: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
mockFlushLogs: vi.fn().mockResolvedValue({ message: "ok" }),
|
||||
mockSetJailConfigFileEnabled: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../api/config", () => ({
|
||||
addLogPath: mockAddLogPath,
|
||||
deleteLogPath: mockDeleteLogPath,
|
||||
fetchJailConfigs: mockFetchJailConfigs,
|
||||
fetchJailConfig: vi.fn(),
|
||||
updateJailConfig: mockUpdateJailConfig,
|
||||
reloadConfig: mockReloadConfig,
|
||||
fetchGlobalConfig: mockFetchGlobalConfig,
|
||||
updateGlobalConfig: mockUpdateGlobalConfig,
|
||||
fetchServerSettings: mockFetchServerSettings,
|
||||
updateServerSettings: mockUpdateServerSettings,
|
||||
flushLogs: mockFlushLogs,
|
||||
fetchMapColorThresholds: mockFetchMapColorThresholds,
|
||||
updateMapColorThresholds: mockUpdateMapColorThresholds,
|
||||
fetchJailConfigFiles: mockFetchJailConfigFiles,
|
||||
fetchJailConfigFileContent: vi.fn(),
|
||||
setJailConfigFileEnabled: mockSetJailConfigFileEnabled,
|
||||
fetchFilterFiles: mockFetchFilterFiles,
|
||||
fetchFilterFile: vi.fn(),
|
||||
updateFilterFile: vi.fn(),
|
||||
createFilterFile: vi.fn(),
|
||||
fetchActionFiles: mockFetchActionFiles,
|
||||
fetchActionFile: vi.fn(),
|
||||
updateActionFile: vi.fn(),
|
||||
createActionFile: vi.fn(),
|
||||
previewLog: vi.fn(),
|
||||
testRegex: vi.fn(),
|
||||
}));
|
||||
|
||||
/** Minimal jail fixture used across tests. */
|
||||
const MOCK_JAIL: JailConfig = {
|
||||
name: "sshd",
|
||||
@@ -34,77 +119,6 @@ const MOCK_JAIL: JailConfig = {
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const mockAddLogPath = vi.fn().mockResolvedValue(undefined);
|
||||
const mockDeleteLogPath = vi.fn().mockResolvedValue(undefined);
|
||||
const mockUpdateJailConfig = vi.fn().mockResolvedValue(undefined);
|
||||
const mockReloadConfig = vi.fn().mockResolvedValue(undefined);
|
||||
const mockFetchGlobalConfig = vi.fn().mockResolvedValue({
|
||||
config: {
|
||||
ban_time: 600,
|
||||
max_retry: 5,
|
||||
find_time: 300,
|
||||
backend: "auto",
|
||||
},
|
||||
});
|
||||
const mockFetchServerSettings = vi.fn().mockResolvedValue({
|
||||
settings: {
|
||||
log_level: "INFO",
|
||||
log_target: "STDOUT",
|
||||
syslog_socket: null,
|
||||
db_path: "/var/lib/fail2ban/fail2ban.sqlite3",
|
||||
db_purge_age: 86400,
|
||||
db_max_matches: 10,
|
||||
},
|
||||
});
|
||||
const mockFetchJailConfigs = vi.fn().mockResolvedValue({
|
||||
jails: [MOCK_JAIL],
|
||||
total: 1,
|
||||
});
|
||||
const mockFetchMapColorThresholds = vi.fn().mockResolvedValue({
|
||||
threshold_high: 100,
|
||||
threshold_medium: 50,
|
||||
threshold_low: 20,
|
||||
});
|
||||
const mockFetchJailConfigFiles = vi.fn().mockResolvedValue({ files: [] });
|
||||
const mockFetchFilterFiles = vi.fn().mockResolvedValue({ files: [] });
|
||||
const mockFetchActionFiles = vi.fn().mockResolvedValue({ files: [] });
|
||||
const mockUpdateMapColorThresholds = vi.fn().mockResolvedValue({});
|
||||
const mockUpdateGlobalConfig = vi.fn().mockResolvedValue(undefined);
|
||||
const mockUpdateServerSettings = vi.fn().mockResolvedValue(undefined);
|
||||
const mockFlushLogs = vi.fn().mockResolvedValue({ message: "ok" });
|
||||
const mockSetJailConfigFileEnabled = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.mock("../../api/config", () => ({
|
||||
addLogPath: (...args: unknown[]) => mockAddLogPath(...args),
|
||||
deleteLogPath: (...args: unknown[]) => mockDeleteLogPath(...args),
|
||||
fetchJailConfigs: () => mockFetchJailConfigs(),
|
||||
fetchJailConfig: vi.fn(),
|
||||
updateJailConfig: (...args: unknown[]) => mockUpdateJailConfig(...args),
|
||||
reloadConfig: () => mockReloadConfig(),
|
||||
fetchGlobalConfig: () => mockFetchGlobalConfig(),
|
||||
updateGlobalConfig: (...args: unknown[]) => mockUpdateGlobalConfig(...args),
|
||||
fetchServerSettings: () => mockFetchServerSettings(),
|
||||
updateServerSettings: (...args: unknown[]) => mockUpdateServerSettings(...args),
|
||||
flushLogs: () => mockFlushLogs(),
|
||||
fetchMapColorThresholds: () => mockFetchMapColorThresholds(),
|
||||
updateMapColorThresholds: (...args: unknown[]) =>
|
||||
mockUpdateMapColorThresholds(...args),
|
||||
fetchJailConfigFiles: () => mockFetchJailConfigFiles(),
|
||||
fetchJailConfigFileContent: vi.fn(),
|
||||
setJailConfigFileEnabled: (...args: unknown[]) =>
|
||||
mockSetJailConfigFileEnabled(...args),
|
||||
fetchFilterFiles: () => mockFetchFilterFiles(),
|
||||
fetchFilterFile: vi.fn(),
|
||||
updateFilterFile: vi.fn(),
|
||||
createFilterFile: vi.fn(),
|
||||
fetchActionFiles: () => mockFetchActionFiles(),
|
||||
fetchActionFile: vi.fn(),
|
||||
updateActionFile: vi.fn(),
|
||||
createActionFile: vi.fn(),
|
||||
previewLog: vi.fn(),
|
||||
testRegex: vi.fn(),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -144,3 +144,60 @@ export interface MapColorThresholdsUpdate {
|
||||
threshold_medium: number;
|
||||
threshold_low: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Jail config files (Task 4a)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface JailConfigFile {
|
||||
/** File stem, e.g. "sshd". */
|
||||
name: string;
|
||||
/** Actual filename, e.g. "sshd.conf". */
|
||||
filename: string;
|
||||
/** Whether the jail is currently enabled in its config file. */
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface JailConfigFilesResponse {
|
||||
files: JailConfigFile[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface JailConfigFileContent extends JailConfigFile {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface JailConfigFileEnabledUpdate {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic conf-file entry (filter.d / action.d)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ConfFileEntry {
|
||||
/** Base name without extension, e.g. "sshd". */
|
||||
name: string;
|
||||
/** Full filename, e.g. "sshd.conf". */
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface ConfFilesResponse {
|
||||
files: ConfFileEntry[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ConfFileContent {
|
||||
name: string;
|
||||
filename: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ConfFileUpdateRequest {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ConfFileCreateRequest {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user