Search blocked requests by IP, plugin, ModSecurity rule or signature.
| Logs | Search-time fields | CIM mappings |
|---|---|---|
| Access · Error · Audit JSON | Plugin denials · Bans · ModSecurity | Web · Intrusion Detection |
Overview · Setup · Searches · Fields · Limits · Repository
flowchart LR
accTitle: BunkerWeb logs, Splunk fields and CIM views
accDescr: Logs flow through a shipper to Splunk. CIM routes differ by log type; audit entries do not prove blocking.
subgraph logs["BunkerWeb logs"]
direction TB
access("Access requests")
audit("ModSecurity audit JSON")
error("Errors and bans")
access ~~~ audit ~~~ error
end
logs --> forwarder("Forwarder / shipper<br/>Read and send files")
forwarder --> addon("TA-bunkerweb<br/>Parse · extract fields")
addon --> views
subgraph views["Splunk searches"]
direction TB
web("Web<br/>All access events")
ids("Intrusion Detection<br/>Blocked access + all audit entries")
context("Error fields<br/>Searchable · no CIM tag")
web ~~~ ids ~~~ context
end
classDef source fill:transparent,stroke:#708696,stroke-width:1px
classDef addon fill:#0b5577,stroke:#0b5577,color:#ffffff,stroke-width:2px
classDef web fill:transparent,stroke:#2eac68,stroke-width:2px
classDef ids fill:transparent,stroke:#3699bd,stroke-width:2px
classDef context fill:transparent,stroke:#708696,stroke-dasharray:4 4
class access,audit,error,forwarder source
class addon addon
class web web
class ids ids
class context context
style logs fill:transparent,stroke:#708696,stroke-dasharray:3 6
style views fill:transparent,stroke:#708696,stroke-dasharray:3 6
The official BunkerWeb add-on for Splunk (TA-bunkerweb), by Bunkerity.
It gives Splunk three sourcetypes for BunkerWeb's logs, with field extraction and
CIM mappings, so a SOC can search blocked requests by
source IP, plugin, ModSecurity rule or signature, and see them in the Web and Intrusion Detection data models.
BunkerWeb needs no code change for this: you set a log format and an audit log format, a Universal Forwarder reads the files, and this app turns them into events. See the BunkerWeb documentation for BunkerWeb itself.
This add-on ships Splunk configuration only: no executable ingestion code or dashboards.
Read the diagram as text
BunkerWeb writes access, error and JSON audit log files. A Universal Forwarder or another shipper sends those logs to Splunk. TA-bunkerweb configures timestamps and line breaking during parsing, then extracts fields at search time. Access events map to CIM Web; access events with action=blocked and all audit entries map to Intrusion Detection. Error events have fields but no CIM tag. See the setup below for real file paths in containers.
Compatibility and recorded validation
| Scope | Detail |
|---|---|
| Sourcetypes | bunkerweb:access, bunkerweb:error, bunkerweb:modsec_audit |
| BunkerWeb | 1.6.17 and later. Events in samples/ and the tests come from 1.6.18-rc3 |
| Splunk tested | Splunk Enterprise 10.2.9 (splunk/splunk:10.2.9, digest in tests/splunk.sh) |
| CIM | 6.x field names: Web and Intrusion Detection (see the limits below) |
| AppInspect (recorded run) | splunk-appinspect 4.4.0, --mode precert: 0 failures, 1 private-app warning |
| License | Apache-2.0, see LICENSE |
AppInspect numbers describe the recorded run, not a fresh test result.
The check_for_updates_disabled warning targets private apps. This public Splunkbase-hosted add-on must keep update checks enabled.
In /etc/bunkerweb/variables.env (or the matching environment variables of the scheduler container):
ACCESS_LOG_1=/var/log/bunkerweb/access-splunk.log
LOG_FORMAT_1=time=$time_iso8601 host=$host client=$remote_addr request_id=$request_id method=$request_method uri="$request_uri" status=$status bytes=$body_bytes_sent ua="$http_user_agent" reason=$reason modsec_rules="$modsecurity_rules" modsec_score=$modsecurity_anomaly_score modsec_id=$modsecurity_unique_id
MODSECURITY_SEC_AUDIT_LOG_PARTS=BFHMODSECURITY_SEC_AUDIT_LOG_PARTS is optional: the default BCFH adds part C (request bodies). Never put a comment after a value in
variables.env: BunkerWeb reads the value literally and drops the setting (Ignoring variable ... doesn't match regex). Comments go on their own line.
And a custom config of type modsec, /etc/bunkerweb/configs/modsec/audit-json.conf:
SecAuditLogFormat JSONThe suffix _1 adds a second access log next to the default access.log, so nothing that reads the default format breaks.
The default error.log and modsec_audit.log need no setting. reason is the id of the BunkerWeb plugin that denied the request;
it is empty for ModSecurity denials. Default CRS anomaly denials populate modsec_rules, modsec_score and modsec_id.
A custom deny rule without the corresponding tx.bunkerweb_* setvar actions can leave those fields unset (- in the log).
The access log alone cannot identify such a denial; consult the error or audit log.
The default MODSECURITY_SEC_AUDIT_ENGINE (RelevantOnly) can also record allowed transactions when a rule logs a match
(including DetectionOnly or a score below the blocking threshold), or when the status matches SecAuditLogRelevantStatus.
An audit entry is not proof that ModSecurity blocked the request. See the ModSecurity directives.
In the container images access.log, error.log and modsec_audit.log are symlinks to the container's stdout and stderr, which a
Universal Forwarder cannot read. ACCESS_LOG_1 above is a real file. For the audit log, point MODSECURITY_SEC_AUDIT_LOG at a file
under /var/log/bunkerweb/ (it must end in .log) and share that directory with the forwarder, for example
MODSECURITY_SEC_AUDIT_LOG=/var/log/bunkerweb/modsec_audit-splunk.log. The error log can be read from the container's stderr by
any shipper, or written to a file with ERROR_LOG=/var/log/bunkerweb/error-splunk.log.
tests/capture/compose.yml is a working example.
- Install
TA-bunkerweb-<version>.tgz(built by./package.sh) on the search head and on the indexers, or on the heavy forwarder that parses the data. All extractions run at search time. Nothing needs configuring in the app. - Add the inputs to the Universal Forwarder (
inputs.conf) and pick your index:
[monitor:///var/log/bunkerweb/access-splunk.log]
sourcetype = bunkerweb:access
index = <your index>
[monitor:///var/log/bunkerweb/error.log]
sourcetype = bunkerweb:error
index = <your index>
[monitor:///var/log/bunkerweb/modsec_audit.log]
sourcetype = bunkerweb:modsec_audit
index = <your index>The error log and the audit log have no time zone. Splunk reads them in the time zone of the indexer. If BunkerWeb runs in another zone,
set TZ = <zone> for [bunkerweb:error] and [bunkerweb:modsec_audit] in TA-bunkerweb/local/props.conf. The access log carries an offset.
Splunkbase upload is manual; see Contributing.
sourcetype=bunkerweb:access action=blocked | stats count by src, signature
sourcetype=bunkerweb:access reason="bad behavior" | timechart count by src
(sourcetype=bunkerweb:access OR sourcetype=bunkerweb:modsec_audit) modsec_id=* | stats values(sourcetype) values(signature) by modsec_id
tag=ids tag=attack | stats count by category, severity
| Sourcetype | Source | Line breaking and time |
|---|---|---|
bunkerweb:access |
access-splunk.log, anchored regex extraction (KV_MODE = none) |
one event per line, time= ISO 8601 with offset |
bunkerweb:error |
error.log, extractions by regex |
one event per line (ModSecurity warning lines reach several KB, TRUNCATE = 100000), YYYY/MM/DD HH:MM:SS |
bunkerweb:modsec_audit |
modsec_audit.log, KV_MODE = json |
one JSON object per line, transaction.time_stamp, TRUNCATE = 100000 |
Access fields and CIM mappings
| Event field | Meaning |
|---|---|
site |
virtual host (host= in the log; the Splunk host field keeps the name of the machine that sent the event) |
client, method, uri, status, bytes, ua, request_id |
as logged |
uri_path, uri_query |
uri split at the first ? |
reason |
plugin id that denied the request (limit, blacklist, bad behavior, ...), missing when there is none |
modsec_rules |
multivalue list of rule ids (deduplicated), modsec_score anomaly score, modsec_id unique id, as exposed by BunkerWeb's CRS integration; unset fields are omitted, not a complete record of every ModSecurity match or denial |
action |
blocked when status is 400 or more and reason or modsec_id identifies a denial, else allowed; custom ModSecurity denials without this metadata cannot be identified from the access log |
CIM mapping:
| CIM field | Web (tag=web, all events) |
Intrusion Detection (tag=ids tag=attack, action=blocked only) |
|---|---|---|
src |
client |
client |
dest |
host= value (site) |
host= value |
http_method, uri_path, uri_query, status |
method, uri split, status |
same |
bytes_out, bytes |
bytes ($body_bytes_sent) |
not mapped |
http_user_agent |
ua |
not mapped |
action |
allowed or blocked |
blocked |
signature |
not mapped | plugin id, or the ModSecurity rule ids |
signature_id |
not mapped | ModSecurity rule ids (not set for plugin blocks) |
category |
not mapped | BunkerWeb plugin or ModSecurity |
ids_type |
not mapped | application |
vendor_product, vendor, product |
BunkerWeb, Bunkerity, BunkerWeb |
same |
Audit fields and CIM mappings
All CIM Intrusion Detection (tag=ids tag=attack, every entry):
| CIM field | JSON source |
|---|---|
src, src_port |
transaction.client_ip, transaction.client_port |
dest, dest_ip, dest_port |
transaction.request.hostname, transaction.host_ip, transaction.host_port |
http_method, uri_path, uri_query, status, http_user_agent |
transaction.request.method, transaction.request.uri split, transaction.response.http_code, transaction.request.headers.User-Agent |
action |
blocked when transaction.is_interrupted is true, else allowed |
signature_id, signature |
transaction.messages{}.details.ruleId, transaction.messages{}.message |
category |
the attack-* tags of the matched rules (attack-xss, attack-sqli, ...) |
severity |
the most severe CRS severity of the matches: 0 and 1 critical, 2 high, 3 medium, 4 low, 5 and above informational |
modsec_id |
transaction.unique_id, usable to join the access and error logs when they carry the same id |
ids_type, vendor_product |
application, BunkerWeb |
Error fields and event types
No CIM tag: the same blocks are already events in the two sourcetypes above, and tagging the error log too would count each one twice.
The fields are there for searching and for the join on modsec_id:
| Field | Event |
|---|---|
log_level, pid, tid, connection, bw_component (ACCESS, BADBEHAVIOR, API, ...) |
every line |
src, dest, http_method, uri_path, uri_query, http_host |
lines with client:, server: and request: |
bw_plugin, bw_message |
message beginning with [ACCESS] denied access from <plugin> : <message>; lookalike text embedded in the request, referrer or host is ignored |
bw_ban_ip, bw_ban_reason, bw_ban_ttl |
message beginning with [ACCESS], such as [ACCESS] IP <ip> is banned with reason <reason> (<ttl>s remaining); lookalike text embedded in the request, referrer or host is ignored |
modsec_verdict (Access denied or Warning), modsec_http_code, modsec_phase, modsec_rule_id, modsec_msg, modsec_severity, modsec_id, crs_version |
The verdict, HTTP code and phase come from the leading ModSecurity message. Rule id, message, severity, unique id and CRS version require a complete canonical ModSecurity metadata block and unambiguous nginx context; malformed or ambiguous records leave these detail fields unset. |
Event types: bunkerweb_access, bunkerweb_access_blocked, bunkerweb_modsec_audit, bunkerweb_error.
- Release notifications. Splunkbase update checks are enabled so Splunk can notify administrators when a new add-on version is available.
- Quoted request data. The app parses the exact documented field order with one anchored extraction and disables automatic KV extraction. Text such as
reason=antibotormodsec_id=spoofinsideuaoruristays request data. Preserve the format above. reasonwith a space. BunkerWeb's id for the bad behavior plugin isbad behavior, with a space, inreasonand in the error log. The app keeps the full id. Searchreason="bad behavior".- ModSecurity or plugin? The app identifies CRS denials by
modsec_id, never byreason, which is empty for ModSecurity denials. Customdenyrules without the correspondingsetvarmetadata cannot be identified this way.modsec_rulesis space-padded and can repeat an id (" 930100 930110 930110 "); the app splits it and deduplicates. -placeholders. nginx writes-for a variable that was not set:modsec_rules="-",modsec_score=-,modsec_id=-. The app drops them, somodsec_id=*finds only ModSecurity events.modsec_rulesnever lists rule 949110 (the blocking rule); the audit log does.hostis a Splunk field.host=of the log format cannot become thehostfield, so the virtual host issiteanddest.\xHHescapes. nginx escapes"and non-printable bytes inside values as\xHH(\x22,\xC3\xA9). The app keeps the raw value.- A 4xx or 5xx without denial metadata is
allowed. This includes backend errors and nginx errors without a plugin reason, but can also include custom ModSecurity denials that do not expose the metadata described above. - A CRS block can appear in two sourcetypes. If both logs contain the denial, the access event and the audit entry are both Intrusion Detection events for one request. Join them on
modsec_idwhen present. WithRelevantOnly, allowed rule matches can also produce audit entries, including inDetectionOnlymode or below the blocking threshold; those entries haveaction=allowedwhentransaction.is_interruptedis false. Access-log metadata depends on the rules'setvaractions. - Severity. The blocking rule 949110 is CRS severity 0, so audit entries containing that rule are
critical. Custom denials need not include that rule and can have a different severity or none. The access log carries no severity, so plugin and ModSecurity blocks from it have none. - CIM check. The tests check the data model constraints (
tag=web,tag=ids tag=attack) and the CIM fields with searches. They also run| datamodelsearches when the Splunk Common Information Model add-on is installed in the container, which needs a download from Splunkbase with a splunk.com login. That add-on was not available for the run reported here, so those checks were skipped. - Personal data. Client IPs, full URIs with their query strings and user agents land in Splunk. Request bodies reach the audit log only with part
CinMODSECURITY_SEC_AUDIT_LOG_PARTS.
Repository layout, building and testing
| Path | Content |
|---|---|
TA-bunkerweb/ |
the app: default/{app,props,eventtypes,tags}.conf, metadata/default.meta, app.manifest, README.txt, LICENSES/LICENSE |
samples/ |
access.log, error.log, modsec_audit.log: real events from BunkerWeb 1.6.18-rc3, client IP replaced by 203.0.113.45, server IPs by 198.51.100.x (documentation ranges). access_adversarial.log: synthetic quoted-field spoofing regressions. error_adversarial.log: 25 synthetic error-log lines, regression fixtures for spoofed denial, ban and ModSecurity fields |
tests/run.sh |
end-to-end test: starts the pinned Splunk container, waits for bootstrap and the authenticated API, installs the package, ingests samples/ with the three sourcetypes, asserts the fields with searches, removes the container. Exit code 1 on any mismatch. SPLTA_SABOTAGE=1 breaks one extraction first, to prove the test fails |
tests/test_ta.py, tests/splunk.sh, tests/lib.sh |
the assertions, the container start and stop, and shared helpers |
tests/capture/ |
the BunkerWeb stack and traffic script that produced samples/ (compose.yml, traffic.sh) |
package.sh |
builds dist/TA-bunkerweb-<version>.tgz for Splunkbase |
./package.sh # dist/TA-bunkerweb-1.0.1.tgz
tests/run.sh # needs docker, curl, python3 and about 5 GB of free memory
python3.11 -m venv .venv && .venv/bin/pip install --require-hashes -r requirements-dev.txt
.venv/bin/splunk-appinspect inspect dist/TA-bunkerweb-1.0.1.tgz --mode precerttests/run.sh starts splunk/splunk with SPLUNK_START_ARGS=--accept-license and SPLUNK_GENERAL_TERMS=--accept-sgt-current-at-splunk-com.
That accepts Splunk's license and general terms for the throwaway test container. The admin password is generated at run time into a
file with mode 0600 outside the repository and shredded when the container is removed.
Apache-2.0. See LICENSE; a copy ships inside the app at TA-bunkerweb/LICENSES/LICENSE.