Elasticsearch ingest pipeline for ASUS ROG router syslog in Security Onion 3.1.
Correctly parses and classifies every log shape produced by ASUS ROG routers into structured ECS fields, covering firewall (netfilter), wireless association, DHCP, SSH authentication, AiMesh roaming, and system events across all daemons the firmware generates.
| Component | Details |
|---|---|
| Security Onion | 3.1 |
| Elasticsearch | 9.3.3 |
| Hardware | ASUS ROG Rapture GT-BE98 Pro + GT-AXE16000 (AiMesh) |
Should work with any ASUS router running stock firmware that sends syslog over
UDP 514. The pipeline recognizes daemon names (kernel, hostapd, wlceventd,
dnsmasq, dropbear, roamast, bsd, avahi-daemon, miniupnpd, acsd)
rather than specific router models, so it is not hardware-specific.
- Architecture
- Datasets produced
- ECS fields reference
- Configuration
- Router syslog setup
- Installation
- Updating pipelines
- SOUP updates
- Searching in Hunt
- Running the tests
- Known limitations
- Troubleshooting
- Contributing
Six Elasticsearch ingest pipelines work together:
SO syslog pipeline (platform)
└─► asus (entry/dispatcher — this repo)
├─► asus.firewall kernel netfilter ACCEPT/DROP/REJECT
├─► asus.wireless hostapd, wlceventd, acsd, roamast, bsd,
│ kernel wireless driver errors (SBF/WLC_SCB_/CFG80211)
├─► asus.dhcp dnsmasq DHCP and DNS
├─► asus.auth dropbear SSH
└─► asus.system avahi-daemon, miniupnpd, remaining kernel, catch-all
└─► common (platform — GeoIP, ASN, finalize)
The asus entry pipeline keeps that exact name so the routing rule you add to
the platform's ingest/syslog file routes correctly without any further changes
after initial setup.
event.dataset |
Source daemons / message types |
|---|---|
asus.firewall |
kernel netfilter ACCEPT/DROP/REJECT |
asus.wireless |
hostapd, wlceventd, acsd, kernel SBF/WLC_SCB_/CFG80211 |
asus.roaming |
roamast, bsd (AiMesh BSS transitions) |
asus.dhcp |
dnsmasq-dhcp lease events |
asus.dns |
dnsmasq DNS warnings (rebind, etc.) |
asus.auth |
dropbear SSH login/failure |
asus.mdns |
avahi-daemon |
asus.upnp |
miniupnpd |
asus.network |
kernel bridge port state changes |
asus.usb |
kernel USB device events |
asus.kernel |
kernel crash dumps / fatal signals |
asus.system |
catch-all for unrecognized daemons (e.g. cfg_server) |
The value of event.dataset changes as the event moves through the pipeline
chain. This affects where you look depending on whether you are using
_simulate in Dev Tools or querying indexed events in Hunt:
| Stage | event.module |
event.dataset |
Where you see this |
|---|---|---|---|
Inside sub-pipeline (e.g. asus.wireless) |
asus |
wireless |
_simulate against asus or sub-pipeline directly |
After SO common pipeline / indexed in Elasticsearch |
asus |
asus.wireless |
Hunt queries, dashboards, detections |
The SO common pipeline concatenates event.module + . + event.dataset
when event.dataset contains no dot. All Hunt queries in this README use the
post-common fully qualified values (e.g. asus.firewall, asus.wireless).
Simulate output against the asus entry pipeline alone will show bare suffixes.
Key fields populated per dataset. All datasets also receive event.module,
event.kind, event.provider, observer.*, syslog.*, @timestamp,
and tags: [asus].
| Field | Example | Notes |
|---|---|---|
event.action |
accept / drop / reject |
|
event.outcome |
success / failure |
|
event.type |
[connection, allowed] |
|
source.ip |
||
source.port |
||
source.mac |
AA-BB-CC-DD-EE-FF |
normalized to dash-separated uppercase |
destination.ip |
||
destination.port |
||
destination.mac |
||
network.transport |
tcp / udp / icmp |
|
network.direction |
inbound / outbound |
|
network.community_id |
Elasticsearch community_id processor | |
observer.type |
firewall |
overridden here only |
observer.ingress.interface.name |
eth0 |
|
observer.egress.interface.name |
br0 |
|
asus.firewall.action |
raw netfilter action | |
asus.firewall.ttl |
||
asus.firewall.tcp_flags |
[syn, ack] |
|
related.ip |
source + destination IPs |
| Field | Example | Notes |
|---|---|---|
event.action |
wireless-associated |
|
event.outcome |
success / failure |
|
source.mac |
AA-BB-CC-DD-EE-FF |
station MAC |
asus.wireless.interface |
wl1.2 |
radio interface |
asus.wireless.status |
Successful (0) |
wlceventd |
asus.wireless.rssi |
-47 |
wlceventd, dBm |
asus.wireless.reason |
Unspecified reason (1) |
deauth reason |
asus.wireless.driver |
dhd0 |
kernel SBF messages |
asus.roaming.event_id |
bsd BSS transition | |
asus.roaming.status_code |
| Field | Example | Notes |
|---|---|---|
event.action |
dhcpack / dhcprequest |
normalized lowercase |
source.ip |
assigned IP | |
source.mac |
client MAC | |
source.domain |
laptop-01 |
hostname if provided |
asus.dhcp.interface |
br0 |
| Field | Example | Notes |
|---|---|---|
event.action |
ssh-login |
|
event.outcome |
success / failure |
|
user.name |
admin |
|
source.ip |
connecting client | |
source.port |
||
related.user |
Two places require your environment-specific values before deploying.
See Installation step 2. Replace the example IP addresses with your actual router IPs.
The observer script maps source IP to router name and model. Open
pipelines/asus and find the Painless script under the "Identify which physical router" description. Replace the placeholder IPs, hostnames, and
model names with your own:
if ('192.0.2.1'.equals(ip)) { // ← your primary router IP
ctx.observer.put('name', 'my-router-1'); // ← your router hostname
ctx.observer.put('product', 'ASUS ROG Rapture XYZ'); // ← your router model
} else if ('192.0.2.2'.equals(ip)) { // ← your secondary router IP
ctx.observer.put('name', 'my-router-2'); // ← your router hostname
ctx.observer.put('product', 'ASUS ROG Rapture XYZ'); // ← your router model
}Add or remove else if blocks for however many routers you have.
The date processor in pipelines/asus is set to America/New_York. If your
routers are in a different timezone, update this value before deploying:
"timezone": "America/New_York"ASUS routers log in local time with no timezone in the BSD syslog timestamp.
Getting this wrong will cause @timestamp to be off by the difference between
your timezone and Eastern Time.
All IP addresses in the pipeline files and test harness use RFC 5737 reserved documentation addresses (
192.0.2.x,198.51.100.x). They are not real addresses and must be replaced with your actual values before use.
Your ASUS router must be configured to send syslog to your Security Onion manager before this pipeline does anything useful. Steps apply to stock ASUS firmware (the UI may vary slightly by model and firmware version):
- Log into your router admin panel (typically
http://192.168.x.1) - Navigate to Administration → System
- Scroll to the System Log section
- Set Enable System Log to Yes
- Set Log server IP address to your Security Onion manager's IP
- Set Log server port to
514 - Set Protocol to
UDP - Click Apply
Repeat for each router. In an AiMesh setup, configure syslog on each node individually — each node sends its own syslog stream independently.
To verify logs are arriving before deploying this parser, SSH into your SO manager and run:
sudo tcpdump -i any -n udp port 514 -c 20You should see packets from your router IPs within a few seconds.
ASUS stock firmware sends syslog over unauthenticated UDP (port 514). UDP is
connectionless and spoofable — a host on the same network segment could inject
fake kernel, dropbear, dnsmasq, or wireless events that appear to
originate from a trusted router IP. This is a protocol limitation of syslog
itself, not a defect in this parser.
Mitigations:
- Place routers and the Security Onion manager on a dedicated management or logging VLAN, isolated from general-purpose network segments
- Apply switch-level or firewall ACLs so only your specific router IPs can send UDP 514 to the SO manager
- Restrict the Security Onion
sysloghostgroup to your router IPs only - Monitor for unexpected source addresses:
event.module:"asus" AND NOT log.source.address:("192.0.2.1" OR "192.0.2.2" OR "192.0.2.1:514" OR "192.0.2.2:514")
Replace the example IPs with your actual router IPs. Any event matching this query came from an unexpected source and warrants investigation.
If your network enforces strict segmentation and ACLs, the residual spoofing risk is low. If routers and workstations share the same flat network, treat router syslog as advisory data rather than authoritative audit evidence.
- Security Onion 3.1 standalone or distributed
- Routers sending syslog (UDP 514) to the SO manager (see Router syslog setup)
- Python 3 (for running the test harness locally — not required on the SO server)
git clone https://github.com/scweeks/securityonion-asus-router.git
cd securityonion-asus-routerBefore copying any files to your server, make the two configuration edits
described in the Configuration section — your router IPs,
hostnames/model names in pipelines/asus, and your timezone.
Confirm the pipeline logic is intact before deploying:
python3 tests/run_tests.pyAll 30 tests must pass.
sudo cp pipelines/asus \
pipelines/asus.firewall \
pipelines/asus.wireless \
pipelines/asus.dhcp \
pipelines/asus.auth \
pipelines/asus.system \
/opt/so/saltstack/local/salt/elasticsearch/files/ingest/Filenames must match exactly — no extensions.
Create a local copy of the platform syslog pipeline if one doesn't exist:
sudo cp /opt/so/saltstack/default/salt/elasticsearch/files/ingest/syslog \
/opt/so/saltstack/local/salt/elasticsearch/files/ingest/syslogOpen the file and add this block before the common pipeline processor,
replacing 192.0.2.1 and 192.0.2.2 with your actual router IPs:
{
"pipeline": {
"name": "asus",
"ignore_failure": true,
"if": "ctx.log?.source?.address != null && (ctx.log.source.address == '192.0.2.1' || ctx.log.source.address == '192.0.2.2' || ctx.log.source.address.toString().startsWith('192.0.2.1:') || ctx.log.source.address.toString().startsWith('192.0.2.2:'))"
}
}The first time, restart Elasticsearch to let salt register all six files:
sudo so-elasticsearch-restartNote: This restart can take several minutes on home-lab hardware. See Troubleshooting if it hangs.
In Kibana Dev Tools, confirm all six pipelines registered:
GET _ingest/pipeline/asus*
Then test with a simulated document, replacing 192.0.2.2 with one of your
actual router IPs:
POST _ingest/pipeline/asus/_simulate
{
"docs": [
{
"_source": {
"log": { "source": { "address": "192.0.2.2:514" } },
"message": "Jun 10 12:06:18 hostapd: wl1.2: STA aa:bb:cc:dd:ee:ff IEEE 802.11: associated"
}
}
]
}
Expected result: process.name: "hostapd", event.dataset: "wireless",
event.action: "wireless-associated", source.mac: "AA-BB-CC-DD-EE-FF".
Once Elasticsearch is running, pipeline changes take effect immediately via
the REST API — no so-elasticsearch-restart required:
In Kibana Dev Tools:
PUT _ingest/pipeline/asus.wireless
{ ...updated file contents... }
Also update the file on disk so salt doesn't overwrite your change on the next restart:
sudo cp pipelines/asus.wireless \
/opt/so/saltstack/local/salt/elasticsearch/files/ingest/asus.wirelessso-elasticsearch-restart is only needed for container-level changes — heap
size, TLS config, network bind address — not for ingest pipeline updates.
Security Onion updates (soup) do not overwrite files in the local salt
tree. Your pipeline files are safe across SOUP updates as long as they live
in /opt/so/saltstack/local/salt/elasticsearch/files/ingest/, not in the
default path.
After a major SOUP update, check the platform's own syslog pipeline for
changes:
diff /opt/so/saltstack/default/salt/elasticsearch/files/ingest/syslog \
/opt/so/saltstack/local/salt/elasticsearch/files/ingest/syslogIf the default changed in a way that affects the dispatch order or upstream field names, you may need to update your local copy accordingly.
Show all router traffic grouped by dataset:
event.module:"asus" | groupby event.dataset
event.module: "asus" is set as the first step of the entry pipeline on
every event regardless of dataset, making it the most reliable anchor for
finding all router traffic.
Useful follow-on queries:
event.dataset:"asus.firewall" AND event.action:"drop"
event.dataset:"asus.auth" AND event.outcome:"failure"
event.dataset:"asus.wireless" AND event.action:"wireless-deauthenticated"
event.dataset:"asus.dhcp" | groupby source.mac
Requires Python 3 only — no external dependencies.
Synthetic test suite (runs in CI and locally):
python3 tests/run_tests.pyWith bulk validation against a real syslog capture (not included in this repo — see Known limitations):
python3 tests/run_tests.py /path/to/syslog.txtThe bulk pass confirms zero lines produce no event.dataset and zero lines
end up with a wireless interface name in process.name (the original bug).
Timezone is hardcoded. The date processor in pipelines/asus is set to
America/New_York. Users in other timezones must change this before deploying
or @timestamp will be incorrect. See Configuration.
No retroactive reclassification. Events indexed before this pipeline was deployed (or before a pipeline update) are not reclassified. Only new events flowing through after deployment are parsed correctly. Correcting historical data requires a reindex operation.
Bulk test requires your own capture. The test harness's bulk validation mode expects a real syslog capture passed as a CLI argument. No capture file is included in this repo — real log data can contain sensitive network information. See .gitignore.
community_id not simulated in the test harness. The Python simulator
does not compute network.community_id (it requires native libraries).
This field is correctly populated by Elasticsearch's built-in community_id
processor in production.
AiMesh node traffic. In AiMesh configurations, wireless events
(association, roaming) may be logged by the node the client is currently
connected to, not the primary router. The observer.name field will reflect
whichever device's source IP the log arrived from.
The asus entry pipeline tags every event with asus and any parse failure
with asus_parse_failure. Use these Hunt queries to monitor pipeline health
and catch silent classification problems:
Active parse failures — investigate immediately:
event.module:"asus" AND tags:"asus_parse_failure"
Events that landed in the catch-all but shouldn't have — indicates a new daemon shape not yet handled by the parser:
event.dataset:"asus.system" AND process.name:(kernel OR hostapd OR wlceventd OR dnsmasq* OR dropbear OR roamast OR bsd OR acsd)
Unexpected source addresses — possible log injection or misconfiguration:
event.module:"asus" AND NOT log.source.address:("192.0.2.1" OR "192.0.2.2" OR "192.0.2.1:514" OR "192.0.2.2:514")
(Replace example IPs with your actual router IPs)
Dataset distribution — run periodically as a sanity check:
event.module:"asus" | groupby event.dataset
A healthy deployment should show the vast majority of events in asus.firewall
with smaller counts in asus.wireless, and zero or near-zero asus_parse_failure
tagged events. A sudden shift in the distribution or a spike in failures after
a router firmware update is the signal to check whether new log shapes need
parser coverage.
This is a known pattern on home-lab hardware, unrelated to the pipeline files. Open a second terminal while the command is running:
# Is Elasticsearch up?
sudo so-elasticsearch-query _cluster/health?pretty
# What is the container doing?
sudo docker logs so-elasticsearch --tail 100 -f
# Is it just slow shard recovery? (check for active log output)
# If logs show shard recovery in progress — just wait, can take 15+ min.
# If logs are completely silent for several minutes:
sudo docker ps -a | grep elasticsearch # container should appearIf the restart is genuinely stuck with no container present:
sudo pkill -f so-elasticsearch-restart
sudo rm -f /var/cache/salt/minion/proc/*
sudo salt-call state.apply elasticsearchSalt applies files alphabetically and stops if Elasticsearch doesn't come up in time. Check which are missing:
GET _ingest/pipeline/asus*
For any missing pipelines, register them directly via Dev Tools — no restart needed:
PUT _ingest/pipeline/asus.wireless
{ ...file contents... }
The upstream SO syslog pipeline sets event.dataset: "syslog" before
calling the asus pipeline. The asus entry pipeline removes this value
immediately so dispatch works correctly. If you see asus.syslog for all
events, the version of pipelines/asus on disk is likely stale — confirm the
file contains a remove processor for event.dataset as the third processor:
python3 -c "
import json
with open('/opt/so/saltstack/local/salt/elasticsearch/files/ingest/asus') as f:
d = json.load(f)
print(list(d['processors'][2].keys())[0]) # should print: remove
"If GET _ingest/pipeline/asus* shows all six pipelines but simulate output is
missing fields like event.dataset, run the simulate with "verbose": true
to see exactly where the chain stops:
POST _ingest/pipeline/asus/_simulate
{
"verbose": true,
"docs": [{ "_source": { ... } }]
}
See CONTRIBUTING.md.
See DESIGN.md for the root-cause analysis of the v1–v8
process.name bug, why the header grok is written the way it is, and why
event.dataset is explicitly removed at the start of the entry pipeline.