Files
BanGUI/frontend/src/api/config.ts
Lukas ab11ece001 Add fail2ban log viewer and service health to Config page
Task 2: adds a new Log tab to the Configuration page.

Backend:
- New Pydantic models: Fail2BanLogResponse, ServiceStatusResponse
  (backend/app/models/config.py)
- New service methods in config_service.py:
    read_fail2ban_log() — queries socket for log target/level, validates the
    resolved path against a safe-prefix allowlist (/var/log) to prevent
    path traversal, then reads the tail of the file via the existing
    _read_tail_lines() helper; optional substring filter applied server-side.
    get_service_status() — delegates to health_service.probe() and appends
    log level/target from the socket.
- New endpoints in routers/config.py:
    GET /api/config/fail2ban-log?lines=200&filter=...
    GET /api/config/service-status
  Both require authentication; log endpoint returns 400 for non-file log
  targets or path-traversal attempts, 502 when fail2ban is unreachable.

Frontend:
- New LogTab.tsx component:
    Service Health panel (Running/Offline badge, version, jail count, bans,
    failures, log level/target, offline warning banner).
    Log viewer with color-coded lines (error=red, warning=yellow,
    debug=grey), toolbar (filter input + debounce, lines selector, manual
    refresh, auto-refresh with interval selector), truncation notice, and
    auto-scroll to bottom on data updates.
  fetchData uses Promise.allSettled so a log-read failure never hides the
  service-health panel.
- Types: Fail2BanLogResponse, ServiceStatusResponse (types/config.ts)
- API functions: fetchFail2BanLog, fetchServiceStatus (api/config.ts)
- Endpoint constants (api/endpoints.ts)
- ConfigPage.tsx: Log tab added after existing tabs

Tests:
- Backend service tests: TestReadFail2BanLog (6), TestGetServiceStatus (2)
- Backend router tests: TestGetFail2BanLog (8), TestGetServiceStatus (3)
- Frontend: LogTab.test.tsx (8 tests)

Docs:
- Features.md: Log section added under Configuration View
- Architekture.md: config.py router and config_service.py descriptions updated
- Tasks.md: Task 2 marked done
2026-03-14 12:54:03 +01:00

572 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* API functions for the configuration and server settings endpoints.
*/
import { del, get, post, put } from "./client";
import { ENDPOINTS } from "./endpoints";
import type {
ActionConfig,
ActionConfigUpdate,
ActionCreateRequest,
ActionListResponse,
ActionUpdateRequest,
ActivateJailRequest,
AddLogPathRequest,
AssignActionRequest,
AssignFilterRequest,
ConfFileContent,
ConfFileCreateRequest,
ConfFilesResponse,
ConfFileUpdateRequest,
Fail2BanLogResponse,
FilterConfig,
FilterConfigUpdate,
FilterCreateRequest,
FilterListResponse,
FilterUpdateRequest,
GlobalConfig,
GlobalConfigUpdate,
InactiveJailListResponse,
JailActivationResponse,
JailConfigFileContent,
JailConfigFileEnabledUpdate,
JailConfigFilesResponse,
JailConfigListResponse,
JailConfigResponse,
JailConfigUpdate,
LogPreviewRequest,
LogPreviewResponse,
MapColorThresholdsResponse,
MapColorThresholdsUpdate,
RegexTestRequest,
RegexTestResponse,
ServerSettingsResponse,
ServerSettingsUpdate,
JailFileConfig,
JailFileConfigUpdate,
ServiceStatusResponse,
} from "../types/config";
// ---------------------------------------------------------------------------
// Jail configuration
// ---------------------------------------------------------------------------
export async function fetchJailConfigs(
): Promise<JailConfigListResponse> {
return get<JailConfigListResponse>(ENDPOINTS.configJails);
}
export async function fetchJailConfig(
name: string
): Promise<JailConfigResponse> {
return get<JailConfigResponse>(ENDPOINTS.configJail(name));
}
export async function updateJailConfig(
name: string,
update: JailConfigUpdate
): Promise<void> {
await put<undefined>(ENDPOINTS.configJail(name), update);
}
// ---------------------------------------------------------------------------
// Global configuration
// ---------------------------------------------------------------------------
export async function fetchGlobalConfig(
): Promise<GlobalConfig> {
return get<GlobalConfig>(ENDPOINTS.configGlobal);
}
export async function updateGlobalConfig(
update: GlobalConfigUpdate
): Promise<void> {
await put<undefined>(ENDPOINTS.configGlobal, update);
}
// ---------------------------------------------------------------------------
// Reload
// ---------------------------------------------------------------------------
export async function reloadConfig(
): Promise<void> {
await post<undefined>(ENDPOINTS.configReload, undefined);
}
// ---------------------------------------------------------------------------
// Regex tester
// ---------------------------------------------------------------------------
export async function testRegex(
req: RegexTestRequest
): Promise<RegexTestResponse> {
return post<RegexTestResponse>(ENDPOINTS.configRegexTest, req);
}
// ---------------------------------------------------------------------------
// Log path management
// ---------------------------------------------------------------------------
export async function addLogPath(
jailName: string,
req: AddLogPathRequest
): Promise<void> {
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
// ---------------------------------------------------------------------------
export async function previewLog(
req: LogPreviewRequest
): Promise<LogPreviewResponse> {
return post<LogPreviewResponse>(ENDPOINTS.configPreviewLog, req);
}
// ---------------------------------------------------------------------------
// Server settings
// ---------------------------------------------------------------------------
export async function fetchServerSettings(
): Promise<ServerSettingsResponse> {
return get<ServerSettingsResponse>(ENDPOINTS.serverSettings);
}
export async function updateServerSettings(
update: ServerSettingsUpdate
): Promise<void> {
await put<undefined>(ENDPOINTS.serverSettings, update);
}
export async function flushLogs(
): Promise<string> {
const resp = await post<{ message: string }>(
ENDPOINTS.serverFlushLogs,
undefined,
);
return resp.message;
}
// ---------------------------------------------------------------------------
// Map color thresholds
// ---------------------------------------------------------------------------
export async function fetchMapColorThresholds(
): Promise<MapColorThresholdsResponse> {
return get<MapColorThresholdsResponse>(ENDPOINTS.configMapColorThresholds);
}
export async function updateMapColorThresholds(
update: MapColorThresholdsUpdate
): Promise<MapColorThresholdsResponse> {
return put<MapColorThresholdsResponse>(
ENDPOINTS.configMapColorThresholds,
update,
);
}
// ---------------------------------------------------------------------------
// Jail config files (Task 4a)
// ---------------------------------------------------------------------------
export async function fetchJailConfigFiles(): Promise<JailConfigFilesResponse> {
return get<JailConfigFilesResponse>(ENDPOINTS.configJailFiles);
}
export async function createJailConfigFile(
req: ConfFileCreateRequest
): Promise<ConfFileContent> {
return post<ConfFileContent>(ENDPOINTS.configJailFiles, req);
}
export async function fetchJailConfigFileContent(
filename: string
): Promise<JailConfigFileContent> {
return get<JailConfigFileContent>(ENDPOINTS.configJailFile(filename));
}
export async function updateJailConfigFile(
filename: string,
req: ConfFileUpdateRequest
): Promise<void> {
await put<undefined>(ENDPOINTS.configJailFile(filename), req);
}
export async function setJailConfigFileEnabled(
filename: string,
update: JailConfigFileEnabledUpdate
): Promise<void> {
await put<undefined>(ENDPOINTS.configJailFileEnabled(filename), update);
}
// ---------------------------------------------------------------------------
// Filter files (Task 4d) — raw file management
// ---------------------------------------------------------------------------
/**
* Return a lightweight name/filename list of all filter files.
*
* Internally calls the enriched ``GET /config/filters`` endpoint (which also
* returns active-status data) and maps the result down to the simpler
* ``ConfFilesResponse`` shape expected by the raw-file editor and export tab.
*/
export async function fetchFilterFiles(): Promise<ConfFilesResponse> {
const result = await fetchFilters();
return {
files: result.filters.map((f) => ({ name: f.name, filename: f.filename })),
total: result.total,
};
}
/** Fetch the raw content of a filter definition file for the raw editor. */
export async function fetchFilterFile(name: string): Promise<ConfFileContent> {
return get<ConfFileContent>(ENDPOINTS.configFilterRaw(name));
}
/** Save raw content to a filter definition file (``PUT /filters/{name}/raw``). */
export async function updateFilterFile(
name: string,
req: ConfFileUpdateRequest
): Promise<void> {
await put<undefined>(ENDPOINTS.configFilterRaw(name), req);
}
/** Create a new raw filter file (``POST /filters/raw``). */
export async function createFilterFile(
req: ConfFileCreateRequest
): Promise<ConfFileContent> {
return post<ConfFileContent>(ENDPOINTS.configFiltersRaw, 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);
}
// ---------------------------------------------------------------------------
// Parsed filter config (Task 2.2 / legacy /parsed endpoint)
// ---------------------------------------------------------------------------
export async function fetchParsedFilter(name: string): Promise<FilterConfig> {
return get<FilterConfig>(ENDPOINTS.configFilterParsed(name));
}
export async function updateParsedFilter(
name: string,
update: FilterConfigUpdate
): Promise<void> {
await put<undefined>(ENDPOINTS.configFilterParsed(name), update);
}
// ---------------------------------------------------------------------------
// Filter structured update / create / delete (Task 2.3)
// ---------------------------------------------------------------------------
/**
* Update a filter's editable fields via the structured endpoint.
*
* Writes only the supplied fields to the ``.local`` override. Fields set
* to ``null`` are cleared; omitted fields are left unchanged.
*
* @param name - Filter base name (e.g. ``"sshd"``)
* @param req - Partial update payload.
*/
export async function updateFilter(
name: string,
req: FilterUpdateRequest
): Promise<void> {
await put<undefined>(ENDPOINTS.configFilter(name), req);
}
/**
* Create a brand-new user-defined filter in ``filter.d/{name}.local``.
*
* @param req - Name and optional regex patterns.
* @returns The newly created FilterConfig.
*/
export async function createFilter(
req: FilterCreateRequest
): Promise<FilterConfig> {
return post<FilterConfig>(ENDPOINTS.configFilters, req);
}
/**
* Delete a filter's ``.local`` override file.
*
* Only custom ``.local``-only filters can be deleted. Attempting to delete a
* filter that is backed by a shipped ``.conf`` file returns 409.
*
* @param name - Filter base name.
*/
export async function deleteFilter(name: string): Promise<void> {
await del<undefined>(ENDPOINTS.configFilter(name));
}
/**
* Assign a filter to a jail by writing ``filter = {filter_name}`` to the
* jail's ``.local`` config file.
*
* @param jailName - Jail name.
* @param req - The filter to assign.
* @param reload - When ``true``, trigger a fail2ban reload after writing.
*/
export async function assignFilterToJail(
jailName: string,
req: AssignFilterRequest,
reload = false
): Promise<void> {
const url = reload
? `${ENDPOINTS.configJailFilter(jailName)}?reload=true`
: ENDPOINTS.configJailFilter(jailName);
await post<undefined>(url, req);
}
// ---------------------------------------------------------------------------
// Filter discovery with active/inactive status (Task 2.1)
// ---------------------------------------------------------------------------
/**
* Fetch all filters from filter.d/ with active/inactive status.
*
* Active filters (those referenced by running jails) are returned first,
* followed by inactive ones. Both groups are sorted alphabetically.
*
* @returns FilterListResponse with all discovered filters and status.
*/
export async function fetchFilters(): Promise<FilterListResponse> {
return get<FilterListResponse>(ENDPOINTS.configFilters);
}
/**
* Fetch full parsed detail for a single filter with active/inactive status.
*
* @param name - Filter base name (e.g. "sshd" or "sshd.conf").
* @returns FilterConfig with active, used_by_jails, source_file populated.
*/
export async function fetchFilter(name: string): Promise<FilterConfig> {
return get<FilterConfig>(ENDPOINTS.configFilter(name));
}
// ---------------------------------------------------------------------------
// Parsed action config (Task 3.2)
// ---------------------------------------------------------------------------
export async function fetchParsedAction(name: string): Promise<ActionConfig> {
return get<ActionConfig>(ENDPOINTS.configActionParsed(name));
}
export async function updateParsedAction(
name: string,
update: ActionConfigUpdate
): Promise<void> {
await put<undefined>(ENDPOINTS.configActionParsed(name), update);
}
// ---------------------------------------------------------------------------
// Action discovery with active/inactive status (Task 3.1 / 3.2)
// ---------------------------------------------------------------------------
/**
* Fetch all actions from action.d/ with active/inactive status.
*
* Active actions (those referenced by running jails) are returned first,
* followed by inactive ones. Both groups are sorted alphabetically.
*
* @returns ActionListResponse with all discovered actions and status.
*/
export async function fetchActions(): Promise<ActionListResponse> {
return get<ActionListResponse>(ENDPOINTS.configActions);
}
/**
* Fetch full parsed detail for a single action.
*
* @param name - Action base name (e.g. "iptables" or "iptables.conf").
* @returns ActionConfig with active, used_by_jails, source_file populated.
*/
export async function fetchAction(name: string): Promise<ActionConfig> {
return get<ActionConfig>(ENDPOINTS.configAction(name));
}
/**
* Update an action's editable fields via the structured endpoint.
*
* Writes only the supplied fields to the ``.local`` override. Fields set
* to ``null`` are cleared; omitted fields are left unchanged.
*
* @param name - Action base name (e.g. ``"iptables"``)
* @param req - Partial update payload.
*/
export async function updateAction(
name: string,
req: ActionUpdateRequest
): Promise<void> {
await put<undefined>(ENDPOINTS.configAction(name), req);
}
/**
* Create a brand-new user-defined action in ``action.d/{name}.local``.
*
* @param req - Name and optional lifecycle commands.
* @returns The newly created ActionConfig.
*/
export async function createAction(
req: ActionCreateRequest
): Promise<ActionConfig> {
return post<ActionConfig>(ENDPOINTS.configActions, req);
}
/**
* Delete an action's ``.local`` override file.
*
* Only custom ``.local``-only actions can be deleted. Attempting to delete an
* action backed by a shipped ``.conf`` file returns 409.
*
* @param name - Action base name.
*/
export async function deleteAction(name: string): Promise<void> {
await del<undefined>(ENDPOINTS.configAction(name));
}
/**
* Assign an action to a jail by appending it to the jail's action list.
*
* @param jailName - Jail name.
* @param req - The action to assign with optional parameters.
* @param reload - When ``true``, trigger a fail2ban reload after writing.
*/
export async function assignActionToJail(
jailName: string,
req: AssignActionRequest,
reload = false
): Promise<void> {
const url = reload
? `${ENDPOINTS.configJailAction(jailName)}?reload=true`
: ENDPOINTS.configJailAction(jailName);
await post<undefined>(url, req);
}
/**
* Remove an action from a jail's action list.
*
* @param jailName - Jail name.
* @param actionName - Action base name to remove.
* @param reload - When ``true``, trigger a fail2ban reload after writing.
*/
export async function removeActionFromJail(
jailName: string,
actionName: string,
reload = false
): Promise<void> {
const url = reload
? `${ENDPOINTS.configJailActionName(jailName, actionName)}?reload=true`
: ENDPOINTS.configJailActionName(jailName, actionName);
await del<undefined>(url);
}
// ---------------------------------------------------------------------------
// Parsed jail file config (Task 6.1 / 6.2)
// ---------------------------------------------------------------------------
export async function fetchParsedJailFile(filename: string): Promise<JailFileConfig> {
return get<JailFileConfig>(ENDPOINTS.configJailFileParsed(filename));
}
export async function updateParsedJailFile(
filename: string,
update: JailFileConfigUpdate
): Promise<void> {
await put<undefined>(ENDPOINTS.configJailFileParsed(filename), update);
}
// ---------------------------------------------------------------------------
// Inactive jails (Stage 1)
// ---------------------------------------------------------------------------
/** Fetch all inactive jails from config files. */
export async function fetchInactiveJails(): Promise<InactiveJailListResponse> {
return get<InactiveJailListResponse>(ENDPOINTS.configJailsInactive);
}
/**
* Activate an inactive jail, optionally providing override values.
*
* @param name - The jail name.
* @param overrides - Optional parameter overrides (bantime, findtime, etc.).
*/
export async function activateJail(
name: string,
overrides?: ActivateJailRequest
): Promise<JailActivationResponse> {
return post<JailActivationResponse>(
ENDPOINTS.configJailActivate(name),
overrides ?? {}
);
}
/** Deactivate an active jail. */
export async function deactivateJail(
name: string
): Promise<JailActivationResponse> {
return post<JailActivationResponse>(
ENDPOINTS.configJailDeactivate(name),
undefined
);
}
// ---------------------------------------------------------------------------
// fail2ban log viewer (Task 2)
// ---------------------------------------------------------------------------
/**
* Fetch the tail of the fail2ban daemon log file.
*
* @param lines - Number of tail lines to return (12000, default 200).
* @param filter - Optional plain-text substring; only matching lines returned.
*/
export async function fetchFail2BanLog(
lines?: number,
filter?: string,
): Promise<Fail2BanLogResponse> {
const params = new URLSearchParams();
if (lines !== undefined) params.set("lines", String(lines));
if (filter !== undefined && filter !== "") params.set("filter", filter);
const query = params.toString() ? `?${params.toString()}` : "";
return get<Fail2BanLogResponse>(`${ENDPOINTS.configFail2BanLog}${query}`);
}
/** Fetch fail2ban service health status with current log configuration. */
export async function fetchServiceStatus(): Promise<ServiceStatusResponse> {
return get<ServiceStatusResponse>(ENDPOINTS.configServiceStatus);
}