Add filter discovery endpoints with active/inactive status (Task 2.1)
- Add list_filters() and get_filter() to config_file_service.py:
scans filter.d/, parses [Definition] + [Init] sections, merges .local
overrides, and cross-references running jails to set active/used_by_jails
- Add FilterConfig.active, used_by_jails, source_file, has_local_override
fields to the Pydantic model; add FilterListResponse and FilterNotFoundError
- Add GET /api/config/filters and GET /api/config/filters/{name} to config.py
- Remove the shadowed GET /api/config/filters list route from file_config.py;
rename GET /api/config/filters/{name} raw variant to /filters/{name}/raw
- Update frontend: fetchFilterFiles() adapts FilterListResponse -> ConfFilesResponse;
add fetchFilters() and fetchFilter() to api/config.ts; remove unused
fetchFilterFiles/fetchActionFiles calls from useConfigActiveStatus
- Fix ConfigPageLogPath test mock to include fetchInactiveJails and related
exports introduced by Stage 1
- Backend: 169 tests pass, mypy --strict clean, ruff clean
- Frontend: 63 tests pass, tsc --noEmit clean, eslint clean
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
||||
ConfFileUpdateRequest,
|
||||
FilterConfig,
|
||||
FilterConfigUpdate,
|
||||
FilterListResponse,
|
||||
GlobalConfig,
|
||||
GlobalConfigUpdate,
|
||||
InactiveJailListResponse,
|
||||
@@ -200,15 +201,27 @@ export async function setJailConfigFileEnabled(
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter files (Task 4d)
|
||||
// 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> {
|
||||
return get<ConfFilesResponse>(ENDPOINTS.configFilters);
|
||||
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.configFilter(name));
|
||||
return get<ConfFileContent>(ENDPOINTS.configFilterRaw(name));
|
||||
}
|
||||
|
||||
export async function updateFilterFile(
|
||||
@@ -264,6 +277,32 @@ export async function updateParsedFilter(
|
||||
await put<undefined>(ENDPOINTS.configFilterParsed(name), update);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -85,6 +85,7 @@ export const ENDPOINTS = {
|
||||
`/config/jail-files/${encodeURIComponent(filename)}/parsed`,
|
||||
configFilters: "/config/filters",
|
||||
configFilter: (name: string): string => `/config/filters/${encodeURIComponent(name)}`,
|
||||
configFilterRaw: (name: string): string => `/config/filters/${encodeURIComponent(name)}/raw`,
|
||||
configFilterParsed: (name: string): string =>
|
||||
`/config/filters/${encodeURIComponent(name)}/parsed`,
|
||||
configActions: "/config/actions",
|
||||
|
||||
@@ -99,12 +99,23 @@ vi.mock("../../api/config", () => ({
|
||||
fetchFilterFile: vi.fn(),
|
||||
updateFilterFile: vi.fn(),
|
||||
createFilterFile: vi.fn(),
|
||||
fetchFilters: vi.fn().mockResolvedValue({ filters: [], total: 0 }),
|
||||
fetchFilter: vi.fn(),
|
||||
fetchActionFiles: mockFetchActionFiles,
|
||||
fetchActionFile: vi.fn(),
|
||||
updateActionFile: vi.fn(),
|
||||
createActionFile: vi.fn(),
|
||||
previewLog: vi.fn(),
|
||||
testRegex: vi.fn(),
|
||||
fetchInactiveJails: vi.fn().mockResolvedValue({ jails: [], total: 0 }),
|
||||
activateJail: vi.fn(),
|
||||
deactivateJail: vi.fn(),
|
||||
fetchParsedFilter: vi.fn(),
|
||||
updateParsedFilter: vi.fn(),
|
||||
fetchParsedAction: vi.fn(),
|
||||
updateParsedAction: vi.fn(),
|
||||
fetchParsedJailFile: vi.fn(),
|
||||
updateParsedJailFile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../api/jails", () => ({
|
||||
|
||||
@@ -23,6 +23,10 @@ const mockConfig: FilterConfig = {
|
||||
maxlines: null,
|
||||
datepattern: null,
|
||||
journalmatch: null,
|
||||
active: false,
|
||||
used_by_jails: [],
|
||||
source_file: "/etc/fail2ban/filter.d/sshd.conf",
|
||||
has_local_override: false,
|
||||
};
|
||||
|
||||
function renderForm(name: string) {
|
||||
|
||||
@@ -12,11 +12,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { fetchJails } from "../api/jails";
|
||||
import {
|
||||
fetchActionFiles,
|
||||
fetchFilterFiles,
|
||||
fetchJailConfigs,
|
||||
} from "../api/config";
|
||||
import { fetchJailConfigs } from "../api/config";
|
||||
import type { JailConfig } from "../types/config";
|
||||
import type { JailSummary } from "../types/jail";
|
||||
|
||||
@@ -44,8 +40,10 @@ export interface UseConfigActiveStatusResult {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fetch jails, jail configs, filter files, and action files in parallel and
|
||||
* derive active-status sets for each config type.
|
||||
* Fetch jails and jail configs, then derive active-status sets for each
|
||||
* config type. Active status is computed from live jail data; filter and
|
||||
* action files are not fetched directly because their active state is already
|
||||
* available via {@link fetchFilters} / {@link fetchActions}.
|
||||
*
|
||||
* @returns Active-status sets, loading flag, error, and refresh function.
|
||||
*/
|
||||
@@ -69,10 +67,8 @@ export function useConfigActiveStatus(): UseConfigActiveStatusResult {
|
||||
Promise.all([
|
||||
fetchJails(),
|
||||
fetchJailConfigs(),
|
||||
fetchFilterFiles(),
|
||||
fetchActionFiles(),
|
||||
])
|
||||
.then(([jailsResp, configsResp, _filterResp, _actionResp]) => {
|
||||
.then(([jailsResp, configsResp]) => {
|
||||
if (ctrl.signal.aborted) return;
|
||||
|
||||
const summaries: JailSummary[] = jailsResp.jails;
|
||||
|
||||
@@ -274,6 +274,29 @@ export interface FilterConfig {
|
||||
datepattern: string | null;
|
||||
/** journalmatch, or null. */
|
||||
journalmatch: string | null;
|
||||
/**
|
||||
* True when this filter is referenced by at least one currently running jail.
|
||||
* Defaults to false when the status was not computed (e.g. /parsed endpoint).
|
||||
*/
|
||||
active: boolean;
|
||||
/**
|
||||
* Names of currently enabled jails that reference this filter.
|
||||
* Empty when active is false.
|
||||
*/
|
||||
used_by_jails: string[];
|
||||
/** Absolute path to the .conf source file. Empty string when not computed. */
|
||||
source_file: string;
|
||||
/** True when a .local override file exists alongside the base .conf. */
|
||||
has_local_override: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for GET /api/config/filters.
|
||||
* Lists all discovered filters with active/inactive status.
|
||||
*/
|
||||
export interface FilterListResponse {
|
||||
filters: FilterConfig[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user