WAF Auto-Block is intentionally small. The entire runtime is a single .NET 10 process: an ASP.NET Core minimal host that exposes two HTTP endpoints and runs one background worker. There is no UI, no queue, and no external state dependency beyond Cloudflare and a local SQLite file. Every component has a single, well-scoped responsibility, and the interaction between them follows a straight line from poll to block to cleanup.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/proteo5/waf-autoblock/llms.txt
Use this file to discover all available pages before exploring further.
Component Map
Each service is registered as a singleton in the DI container. The table below lists each component, the interface it implements, and what it is responsible for.| Component | Interface | Responsibility |
|---|---|---|
Worker | BackgroundService | Main polling loop; coordinates detection, blocking, and cleanup on every cycle. |
CloudflareAnalyticsClient | ICloudflareAnalyticsClient | Queries the Cloudflare GraphQL API for firewall events (firewallEventsAdaptiveGroups) and HTTP response-code signals (httpRequestsAdaptiveGroups). |
CloudflareBlocklistClient | ICloudflareBlocklistClient | Adds and removes IPs from the Cloudflare account-level IP list via the Rules Lists REST API. Supports symbolic list name resolution. |
RuleMatcher | IRuleMatcher | Resolves a firewall event’s ruleId against the configured rules dictionary. Only explicitly configured and enabled rules produce a match; all others are silently skipped. |
BlockingExpirationCalculator | IBlockingExpirationCalculator | Computes the block expiry timestamp as blockedAt + TtlMinutes. The effective minimum TTL is 1 minute. |
SqliteBlockedIpStore | IBlockedIpStore | Persists BlockedIpRecord entries to SQLite; provides duplicate-check, TTL-expiry queries, and delete operations. |
AppRuntimeState | (singleton, no interface) | In-memory state bag that tracks StartedAt, LastSuccessfulPollAt, and LastCleanupAt for the /status endpoint. |
All services are registered as singletons. The background worker is registered via
AddHostedService<Worker>().External Integrations
The service communicates with two distinct Cloudflare API surfaces.GraphQL Analytics API
Endpoint:
https://api.cloudflare.com/client/v4/graphqlUsed to query two dataset fields:firewallEventsAdaptiveGroups— WAF block events grouped by source IP and rule ID (limit: 20, ordered by count descending).httpRequestsAdaptiveGroups— HTTP request aggregates grouped by source IP, request path, and response status code (limit: 1000).
Bearer token set on the HttpClient default request headers.Rules Lists REST API
Endpoint:
https://api.cloudflare.com/client/v4/accounts/{accountId}/rules/lists/{listId}/itemsUsed to add and remove IPs from an account-level IP list. Supports two forms of list identification:- A direct UUID passed in
BlocklistId. - A symbolic name prefixed with
$(e.g.$auto_blocked_ips), which is resolved to a UUID via a one-timeGET accounts/{accountId}/rules/listscall.
Data Flow
The following steps describe the complete path from a Cloudflare event to a local block record and its eventual removal.- The
Workerwakes on its configuredIntervalSecondsschedule (plus a small random jitter up to 750 ms) and begins a cycle. CloudflareAnalyticsClient.GetOffendingTrafficAsync()queriesfirewallEventsAdaptiveGroupsfor events withaction = "block"in the lastWindowSeconds. It returns a list ofOffendingTrafficRecordobjects, each carrying aClientIp,RuleId, andCount.CloudflareAnalyticsClient.GetHttpStatusSignalsAsync()querieshttpRequestsAdaptiveGroupsfor the configured status codes over theHttpStatusDetection.WindowSecondsperiod (falling back toPolling.WindowSecondsif not set). It returns a list ofHttpStatusSignalRecordobjects, each carrying aClientIp,RequestPath,StatusCode, andCount.- For each WAF event,
RuleMatcher.TryResolve()maps theRuleIdto a configuredRuleMonitorOptions. Unrecognised rule IDs are skipped. - HTTP signal aggregates are scored per IP against
HttpStatusCodeRuleOptionsthresholds (total errors, distinct paths, and per-code ratio) and against the distributed path detection thresholds. - IPs that pass their respective thresholds and are not already in the local store are added via
CloudflareBlocklistClient.AddIpAsync(). ABlockedIpRecordis then saved viaSqliteBlockedIpStore.SaveAsync(). - On each cycle,
SqliteBlockedIpStore.GetExpiredAsync(DateTimeOffset.UtcNow)returns all records whoseExpiresAthas passed. Each expired entry is removed from Cloudflare viaCloudflareBlocklistClient.RemoveIpAsync()and deleted from SQLite viaSqliteBlockedIpStore.DeleteAsync().
SQLite Schema
The store uses a single table. The schema is created on startup bySqliteBlockedIpStore.InitializeAsync() if it does not already exist.
DateTimeOffset.ToString("O")). The cf_item_id column holds the Cloudflare list item UUID; it may be empty for entries created from asynchronous Cloudflare API responses and is resolved lazily during the cleanup pass.
HTTP Endpoints
The ASP.NET Core minimal host exposes two endpoints.GET /
Service banner. Returns a static JSON object with the service name, status string, and runtime version.
GET /status
Operational state. Returns live values from
AppRuntimeState.lastSuccessfulPollAt and lastCleanupAt are null until the first completed cycle.