Skip to content

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:

{ "data": { ... } }

On error:

{ "error": { "code": "string", "message": "string" } }

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 no code field), 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

curl https://panel.example.com:2087/api/waf/events?limit=50 \
  -H "Authorization: Bearer $TOKEN"

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.

{ "success": true, "imported": 0 }

Errors

Status Code Meaning
500 (none) Returns { "error": "<db error>" } if listing enabled sites fails.

Side effects

  • Reads enabled sites from SQLite (webapps joined with waf_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.

{ "success": true, "message": "ModSecurity installed for Nginx (Debian/Ubuntu)\n" }

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_logs and the waf_events table (MyISAM) via mysql.
  • 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 -c install 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/writes modsecurity.conf, unicode.mapping, CRS, and StarkOne custom rules; writes main-detection.conf, main-prevention.conf, main-learning.conf; writes the blocked-page HTML to /usr/share/starkone/waf/blocked.html; edits nginx.conf / crs-setup.conf via sed; and runs nginx -t && systemctl reload nginx (or the Apache equivalent).
  • Creates the waf_sites table in SQLite if missing.

Example

curl -X POST https://panel.example.com:2087/api/waf/install \
  -H "Authorization: Bearer $TOKEN"

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

curl https://panel.example.com:2087/api/waf/rules \
  -H "Authorization: Bearer $TOKEN"

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

curl https://panel.example.com:2087/api/waf/rules/942100 \
  -H "Authorization: Bearer $TOKEN"

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

curl https://panel.example.com:2087/api/waf/settings \
  -H "Authorization: Bearer $TOKEN"

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

{ "success": true }

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_settings table (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

curl https://panel.example.com:2087/api/waf/sites \
  -H "Authorization: Bearer $TOKEN"

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

curl https://panel.example.com:2087/api/waf/sites/12 \
  -H "Authorization: Bearer $TOKEN"

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

{ "success": true }

Side effects

  • Sets enabled = 0 for the site in the SQLite waf_sites table.
  • 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 nginx and systemctl 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".

{ "mode": "detection", "paranoia_level": 1 }
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

{ "success": true }

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_sites with enabled = 1, the chosen mode and paranoia_level.
  • Generates /etc/modsecurity/sites/<domain>.conf (and a <domain>-custom.conf if absent) and edits the nginx vhost to include the WAF.
  • Runs cli.Run("rewrite_user_confs", siteID).
  • Restarts both services: systemctl restart nginx and systemctl 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

{ "success": true, "deleted": 42 }

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.

{ "success": true, "imported": 0 }

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

{ "mode": "prevention" }
Field Type Required Description
mode string yes One of detection, prevention, or learning. Any other value is rejected.

Response - 200 OK

{ "success": true }

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 mode for the site in the SQLite waf_sites table.
  • Regenerates /etc/modsecurity/sites/<domain>.conf and updates the nginx vhost include.
  • Restarts both services: systemctl restart nginx and systemctl 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

{ "rule_id": 942100 }
Field Type Required Description
rule_id integer yes The CRS rule ID to whitelist (append to the comma-separated whitelist_rules).

Response - 200 OK

{ "success": true }

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_rules in the SQLite waf_sites table.
  • Regenerates /etc/modsecurity/sites/<domain>.conf with a SecRuleRemoveById directive per whitelisted rule.
  • Reloads both services: systemctl reload nginx and systemctl 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

{ "success": true }

Side effects

  • Removes the rule ID from whitelist_rules in the SQLite waf_sites table.
  • Regenerates /etc/modsecurity/sites/<domain>.conf.
  • Reloads both services: systemctl reload nginx and systemctl 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

curl https://panel.example.com:2087/api/waf/status \
  -H "Authorization: Bearer $TOKEN"

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.

{ "success": true, "message": "WAF uninstalled\n" }

Side effects

  • Strips ModSecurity directives and the /waf-blocked.html location block from every /etc/nginx/conf.d/*.conf vhost.
  • Runs a bash -c script that removes /etc/nginx/modsec/sites/*.conf, /etc/nginx/modsec/main-*.conf, /etc/modsecurity/sites/*.conf; runs apt-get remove for the ModSecurity packages; runs systemctl restart nginx and systemctl restart apache2; and drops the MySQL database starkone_waf_logs.
  • Clears the SQLite waf_sites table (DELETE FROM waf_sites).

Example

curl -X POST https://panel.example.com:2087/api/waf/uninstall \
  -H "Authorization: Bearer $TOKEN"