WAF (ModSecurity)¶
ModSecurity-based Web Application Firewall: per-site modes, custom rules, whitelists, events, StarkOne signature catalogue.
All endpoints below are mounted under /api. Authentication: Authorization: Bearer <token> unless noted as public.
Endpoint count: 19
Endpoints¶
| Method | Path | Handler | Guards |
|---|---|---|---|
GET |
/api/waf/events |
getAllWAFEvents |
RequireSuperuser() |
POST |
/api/waf/events/parse |
parseAllWAFEvents |
RequireSuperuser() |
POST |
/api/waf/install |
installWAF |
RequireSuperuser() |
GET |
/api/waf/rules |
getWAFRules |
RequireSuperuser() |
GET |
/api/waf/rules/:id |
getWAFRuleInfo |
RequireSuperuser() |
GET |
/api/waf/settings |
getWAFSettings |
RequireSuperuser() |
POST |
/api/waf/settings |
saveWAFSettings |
RequireSuperuser() |
GET |
/api/waf/sites |
listWAFSites |
RequireSuperuser() |
GET |
/api/waf/sites/:id |
getWAFSite |
RequireSuperuser() |
POST |
/api/waf/sites/:id/disable |
disableWAFSite |
RequireSuperuser() |
POST |
/api/waf/sites/:id/enable |
enableWAFSite |
RequireSuperuser() |
DELETE |
/api/waf/sites/:id/events |
clearWAFSiteEvents |
RequireSuperuser() |
GET |
/api/waf/sites/:id/events |
getWAFSiteEvents |
RequireSuperuser() |
POST |
/api/waf/sites/:id/events/parse |
parseWAFSiteEvents |
RequireSuperuser() |
POST |
/api/waf/sites/:id/mode |
setWAFSiteMode |
RequireSuperuser() |
POST |
/api/waf/sites/:id/whitelist |
addWAFWhitelist |
RequireSuperuser() |
DELETE |
/api/waf/sites/:id/whitelist/:ruleId |
removeWAFWhitelist |
RequireSuperuser() |
GET |
/api/waf/status |
getWAFStatus |
RequireSuperuser() |
POST |
/api/waf/uninstall |
uninstallWAF |
RequireSuperuser() |
Source¶
Handlers for this section live in api/routes/handlers_*.go in the StarkOne source. Each handler binds its request body with gin.ShouldBindJSON(&req) and responds with the standard envelope:
On error:
Note: the WAF handlers do not use the
{ "error": { "code": ... } }envelope described above. Each handler returns errors as a flat{ "error": "<message>" }object (a plain string, with nocodefield), and most success responses return a flat/non-enveloped body rather than{ "data": { ... } }. The per-endpoint blocks below document the actual shapes emitted by the source.
Reference¶
GET /api/waf/events¶
Returns a paginated, time-ordered list of WAF events across all sites, read from the waf_events table.
Guards: RequireSuperuser()
Query parameters
| Name | Type | Optional | Description |
|---|---|---|---|
limit |
integer (string) | yes | Maximum rows to return. Defaults to 100. |
offset |
integer (string) | yes | Row offset for pagination. Defaults to 0. |
Response - 200 OK
Flat object (not enveloped). events is an array; total is the full row count of waf_events.
{
"events": [
{
"id": 1,
"site_id": 12,
"domain": "example.com",
"timestamp": "2026-06-17 10:00:00",
"client_ip": "203.0.113.10",
"uri": "/wp-login.php",
"rule_id": 942100,
"rule_message": "SQL Injection Attack Detected via libinjection",
"severity": "CRITICAL",
"action": "blocked"
}
],
"total": 1
}
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none) | Returns { "error": "<db error>" } if the waf_events query fails. |
Example
POST /api/waf/events/parse¶
Triggers a parse/import of ModSecurity audit logs for every enabled WAF site and reports how many events were imported.
Guards: RequireSuperuser()
Response - 200 OK
Flat object. Note: the underlying parseModSecAuditLog is a stub that always returns 0 (actual parsing is performed by a cron job every minute), so imported is 0 in practice.
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none) | Returns { "error": "<db error>" } if listing enabled sites fails. |
Side effects
- Reads enabled sites from SQLite (
webappsjoined withwaf_sites). No file or service changes - the per-site parse function is a no-op stub.
Example
curl -X POST https://panel.example.com:2087/api/waf/events/parse \
-H "Authorization: Bearer $TOKEN"
POST /api/waf/install¶
Installs ModSecurity and the OWASP Core Rule Set for the detected web server (nginx or Apache) and operating system (Debian/Ubuntu or RHEL family), and provisions the WAF logging database.
Guards: RequireSuperuser()
Response - 200 OK
Flat object. message contains the combined stdout/stderr of the install script.
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none) | Returns { "error": "No supported web server found (nginx or apache required)" } when neither nginx nor Apache is detected. |
500 |
(none) | Returns { "error": "<script output>" } when the install shell script exits non-zero. |
Side effects
- Creates the MySQL database
starkone_waf_logsand thewaf_eventstable (MyISAM) viamysql. - Detects the web server by probing
/usr/sbin/nginx,/usr/sbin/apache2,/usr/sbin/httpd, and the package manager via/usr/bin/dnf//usr/bin/yum. - Runs a
bash -cinstall script that: installs distro packages (e.g.libnginx-mod-http-modsecurity,modsecurity-crs, or the RHEL equivalents); creates directories under/etc/modsecurity,/var/log/modsecurity,/etc/nginx/modsec; downloads/writesmodsecurity.conf,unicode.mapping, CRS, and StarkOne custom rules; writesmain-detection.conf,main-prevention.conf,main-learning.conf; writes the blocked-page HTML to/usr/share/starkone/waf/blocked.html; editsnginx.conf/crs-setup.confviased; and runsnginx -t && systemctl reload nginx(or the Apache equivalent). - Creates the
waf_sitestable in SQLite if missing.
Example
GET /api/waf/rules¶
Returns a static catalogue of OWASP CRS rule categories with their top-level rule IDs and descriptions.
Guards: RequireSuperuser()
Response - 200 OK
Flat JSON array (not enveloped). The list is hard-coded in the handler.
[
{ "id": 910000, "category": "Method Enforcement", "description": "Block unusual HTTP methods", "enabled": true },
{ "id": 942000, "category": "SQLi", "description": "SQL Injection attacks", "enabled": true }
]
Example
GET /api/waf/rules/:id¶
Returns detail (category, name, description, severity, remediation) for a single OWASP CRS rule, looked up from the built-in rule database.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The numeric CRS rule ID (e.g. 942100). |
Response - 200 OK
Flat object. Resolution order: exact rule match, then rule family (first three significant digits, e.g. 942000), then a generic "Unknown" fallback - so a 200 is returned even for unrecognised rule IDs.
{
"id": 942100,
"category": "SQLi",
"name": "SQL Injection Attack Detected via libinjection",
"description": "Detects SQL injection using the libinjection library which identifies SQL syntax patterns in user input.",
"severity": "CRITICAL",
"solution": "Use parameterized queries (prepared statements). Never concatenate user input into SQL queries."
}
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none) | Returns { "error": "Invalid rule ID" } when :id is not a valid integer. |
Example
GET /api/waf/settings¶
Returns the global WAF settings, merging hard-coded defaults with any overrides stored in the waf_settings table.
Guards: RequireSuperuser()
Response - 200 OK
Flat object (not enveloped).
{
"default_mode": "detection",
"paranoia_level": 1,
"anomaly_threshold": 5,
"learning_duration": 7,
"blocked_countries": "",
"whitelisted_ips": "",
"enable_geo_blocking": false
}
Example
POST /api/waf/settings¶
Persists global WAF settings by upserting each provided key/value into the waf_settings table.
Guards: RequireSuperuser()
Request body
The body is bound to a free-form map[string]interface{}; there is no fixed schema or binding:"required" validation. Only string, number (stored as integer), and boolean values are persisted - any other value type for a key is skipped.
{
"default_mode": "prevention",
"paranoia_level": 2,
"anomaly_threshold": 5,
"enable_geo_blocking": true,
"blocked_countries": "CN,RU",
"whitelisted_ips": "203.0.113.10"
}
| Field | Type | Required | Description |
|---|---|---|---|
| (any key) | string / number / boolean | no | Setting name and value; upserted into waf_settings. Numbers are stored as integers; booleans as "true"/"false". Other types are ignored. |
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none) | Returns { "error": "Invalid request" } when the body is not valid JSON. |
Side effects
- Upserts rows into the SQLite
waf_settingstable (INSERT ... ON CONFLICT(key) DO UPDATE). No service reloads.
Example
curl -X POST https://panel.example.com:2087/api/waf/settings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"default_mode": "prevention", "paranoia_level": 2}'
GET /api/waf/sites¶
Lists all web apps with their WAF status (enabled flag, mode, paranoia level, event count, and last event time).
Guards: RequireSuperuser()
Response - 200 OK
Flat JSON array (not enveloped). last_event is null when the site has no events.
[
{
"site_id": 12,
"domain": "example.com",
"enabled": true,
"mode": "detection",
"paranoia_level": 1,
"events_count": 0,
"last_event": null
}
]
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none) | Returns { "error": "<db error>" } if the join query fails. |
Example
GET /api/waf/sites/:id¶
Returns the WAF configuration and event summary for a single site, including its whitelist rule list.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Response - 200 OK
Flat object. whitelist_rules is a comma-separated string of rule IDs; events_count and last_event come from the MySQL WAF database.
{
"site_id": "12",
"domain": "example.com",
"enabled": false,
"mode": "detection",
"paranoia_level": 1,
"whitelist_rules": "942100,941110",
"events_count": 0,
"last_event": null
}
Errors
| Status | Code | Meaning |
|---|---|---|
404 |
(none) | Returns { "error": "Site not found" } when no webapps row matches :id. |
Example
POST /api/waf/sites/:id/disable¶
Disables WAF protection for a site: clears its enabled flag, removes its generated ModSecurity config, and rewrites the vhost.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Response - 200 OK
Side effects
- Sets
enabled = 0for the site in the SQLitewaf_sitestable. - Deletes
/etc/modsecurity/sites/<domain>.conf. - Runs
cli.Run("rewrite_user_confs", siteID)to rewrite the vhost (removing the WAF include). - Restarts both services:
systemctl restart nginxandsystemctl restart apache2.
Example
curl -X POST https://panel.example.com:2087/api/waf/sites/12/disable \
-H "Authorization: Bearer $TOKEN"
POST /api/waf/sites/:id/enable¶
Enables WAF protection for a site in the requested mode and paranoia level, generates its ModSecurity config, and rewrites the vhost.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Request body
The body is optional. If it is missing/invalid or mode is empty, the handler falls back to mode = "detection" and paranoia_level = 1. No fields use binding:"required".
| Field | Type | Required | Description |
|---|---|---|---|
mode |
string | no | WAF mode. Defaults to detection when empty. |
paranoia_level |
integer | no | CRS paranoia level. Defaults to 1. |
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
404 |
(none) | Returns { "error": "Site not found" } when no webapps row matches :id. |
Side effects
- Upserts the site row in SQLite
waf_siteswithenabled = 1, the chosenmodeandparanoia_level. - Generates
/etc/modsecurity/sites/<domain>.conf(and a<domain>-custom.confif absent) and edits the nginx vhost to include the WAF. - Runs
cli.Run("rewrite_user_confs", siteID). - Restarts both services:
systemctl restart nginxandsystemctl restart apache2.
Example
curl -X POST https://panel.example.com:2087/api/waf/sites/12/enable \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode": "detection", "paranoia_level": 1}'
DELETE /api/waf/sites/:id/events¶
Deletes all WAF events for a single site from the MySQL WAF database and reports the number of rows removed.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none) | Returns { "error": "WAF database not available" } when the MySQL WAF DB connection is nil, or { "error": "<db error>" } if the delete fails. |
Side effects
- Executes
DELETE FROM waf_events WHERE site_id = ?against the MySQL WAF database.
Example
curl -X DELETE https://panel.example.com:2087/api/waf/sites/12/events \
-H "Authorization: Bearer $TOKEN"
GET /api/waf/sites/:id/events¶
Returns paginated WAF events for a single site, grouped by request (unique_id) with aggregated rule IDs, read from the MySQL WAF database.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Query parameters
| Name | Type | Optional | Description |
|---|---|---|---|
limit |
integer (string) | yes | Maximum grouped events to return. Defaults to 100. |
offset |
integer (string) | yes | Offset for pagination. Defaults to 0. |
Response - 200 OK
Flat object. If the MySQL WAF DB is unavailable, returns { "events": [], "total": 0 }. rule_ids is an array of integers; total is the distinct unique_id count for the site.
{
"events": [
{
"unique_id": "ZabC123",
"timestamp": "2026-06-17 10:00:00",
"client_ip": "203.0.113.10",
"uri": "/wp-login.php",
"rule_ids": [942100, 949110],
"rule_message": "SQL Injection Attack Detected via libinjection",
"action": "blocked",
"rule_count": 2
}
],
"total": 1
}
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none) | Returns { "error": "<db error>" } if the grouped events query fails. |
Example
curl "https://panel.example.com:2087/api/waf/sites/12/events?limit=50&offset=0" \
-H "Authorization: Bearer $TOKEN"
POST /api/waf/sites/:id/events/parse¶
Triggers a parse/import of the ModSecurity audit log for a single site and reports how many events were imported.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Response - 200 OK
Flat object. The underlying parse function is a no-op stub returning 0 (actual parsing runs via cron), so imported is 0 in practice.
Errors
| Status | Code | Meaning |
|---|---|---|
404 |
(none) | Returns { "error": "Site not found" } when no webapps row matches :id. |
500 |
(none) | Returns { "error": "<message>" } if the parse function returns an error. |
Example
curl -X POST https://panel.example.com:2087/api/waf/sites/12/events/parse \
-H "Authorization: Bearer $TOKEN"
POST /api/waf/sites/:id/mode¶
Changes the WAF mode for a site (detection / prevention / learning), regenerates its config, and restarts the web servers.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
mode |
string | yes | One of detection, prevention, or learning. Any other value is rejected. |
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none) | Returns { "error": "Invalid request" } when the body is not valid JSON. |
400 |
(none) | Returns { "error": "Invalid mode. Use: detection, prevention, or learning" } for an unrecognised mode. |
Side effects
- Updates
modefor the site in the SQLitewaf_sitestable. - Regenerates
/etc/modsecurity/sites/<domain>.confand updates the nginx vhost include. - Restarts both services:
systemctl restart nginxandsystemctl restart apache2.
Example
curl -X POST https://panel.example.com:2087/api/waf/sites/12/mode \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode": "prevention"}'
POST /api/waf/sites/:id/whitelist¶
Adds a rule ID to a site's whitelist (so the rule is removed via SecRuleRemoveById), regenerates the config, and reloads the web servers.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
rule_id |
integer | yes | The CRS rule ID to whitelist (append to the comma-separated whitelist_rules). |
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none) | Returns { "error": "Invalid request" } when the body is not valid JSON. |
Side effects
- Appends the rule ID to
whitelist_rulesin the SQLitewaf_sitestable. - Regenerates
/etc/modsecurity/sites/<domain>.confwith aSecRuleRemoveByIddirective per whitelisted rule. - Reloads both services:
systemctl reload nginxandsystemctl reload apache2.
Example
curl -X POST https://panel.example.com:2087/api/waf/sites/12/whitelist \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"rule_id": 942100}'
DELETE /api/waf/sites/:id/whitelist/:ruleId¶
Removes a rule ID from a site's whitelist, regenerates the config, and reloads the web servers.
Guards: RequireSuperuser()
Path parameters
| Name | Type | Description |
|---|---|---|
id |
integer | The web app / site ID (webapps.id). |
ruleId |
integer | The CRS rule ID to remove from whitelist_rules. |
Response - 200 OK
Side effects
- Removes the rule ID from
whitelist_rulesin the SQLitewaf_sitestable. - Regenerates
/etc/modsecurity/sites/<domain>.conf. - Reloads both services:
systemctl reload nginxandsystemctl reload apache2.
Example
curl -X DELETE https://panel.example.com:2087/api/waf/sites/12/whitelist/942100 \
-H "Authorization: Bearer $TOKEN"
GET /api/waf/status¶
Returns the global WAF status: whether ModSecurity is installed, the detected web server, CRS version, global mode, count of enabled sites, and event counts.
Guards: RequireSuperuser()
Response - 200 OK
Flat object (not enveloped). Values are derived from filesystem probes, the SQLite waf_sites table, and the MySQL WAF database.
{
"installed": true,
"web_server": "nginx",
"modsec_version": "",
"crs_version": "3.x",
"global_enabled": true,
"mode": "prevention",
"sites_enabled": 3,
"total_events": 120,
"events_today": 7
}
Side effects
- Read-only. Probes paths under
/usr/sbin,/usr/lib,/usr/share/modsecurity-crs, and reads/etc/modsecurity/modsecurity.conf; queries SQLite and the MySQL WAF DB.
Example
POST /api/waf/uninstall¶
Removes ModSecurity from all nginx vhosts, deletes generated config, uninstalls the packages, restarts the web servers, and drops the WAF logging database.
Guards: RequireSuperuser()
Response - 200 OK
Flat object. message contains the uninstall script output.
Side effects
- Strips ModSecurity directives and the
/waf-blocked.htmllocation block from every/etc/nginx/conf.d/*.confvhost. - Runs a
bash -cscript that removes/etc/nginx/modsec/sites/*.conf,/etc/nginx/modsec/main-*.conf,/etc/modsecurity/sites/*.conf; runsapt-get removefor the ModSecurity packages; runssystemctl restart nginxandsystemctl restart apache2; and drops the MySQL databasestarkone_waf_logs. - Clears the SQLite
waf_sitestable (DELETE FROM waf_sites).
Example