Custom Wazuh SIEM integration for Ubiquiti UniFi (Network controller — UDM Pro Max, UDM SE or any UniFi OS console — plus APs, switches, UPS), MikroTik RouterOS 7.x, Fortinet FortiGate, Stormshield SNS (SN-series NG firewalls), Jamf Pro (on-prem MDM), Authentik IdP (goauthentik), Synology DSM (NAS), Microsoft 365 (Office 365 audit), Docker containers, and the full Proxmox stack — Proxmox VE (journald + task trail + pveproxy Web/API access) and Proxmox Backup Server (task/verify integrity) — providing comprehensive log decoding, field extraction, noise suppression, and alerting rules.
It also ships an in-situ log anonymiser (tools/sns-anonymize.py) so you can build and share decoder sample corpora without ever exposing production personal data — see Log anonymiser.
The design philosophy is decode everything, then triage by level: every event type a device can emit gets decoded and traced (level 1 = archived without alerting), and only meaningful events alert (level 3+). Nothing is silently dropped — noise reduction happens at the source or at the rule level, never by leaving events undecoded.
The second commitment is compliance-first tagging: every security-relevant rule carries the framework groups (PCI DSS, GDPR, HIPAA, NIST 800-53, TSC, GPG13 where applicable) and MITRE ATT&CK technique ids — so the network gear, MDM and IdP this ruleset covers show up in Wazuh's compliance dashboards exactly like the stock OS rules do. See Compliance & MITRE ATT&CK tagging.
Developed and tested on Wazuh 4.14.3 by Astier Consulting — Apple/IT consulting with 30 years of datacenter management experience.
This repository is public. Decoders and rules are built from real syslog captured on live networks, so every example must be scrubbed before it is committed. This is a founding principle of the project, not a preference.
Never commit:
- MAC addresses of real equipment — use the RFC 7042 documentation range
00-00-5E-00-53-xx(e.g.00005e005301) - IP addresses of real hosts — use RFC 5737 (
192.0.2.0/24) or RFC 3849 (2001:db8::/32) - hostnames, serial numbers, account names, delegated IPv6 prefixes, or site identifiers
Equipment model names (USW-Pro-Max-48, U7-Pro-Wall) are fine: they
identify a product, not a unit. Truncating a real value is not anonymisation —
replace it.
Suggested pre-commit sweep:
grep -rnoE '\b[0-9a-f]{12}\b' --include='*.xml' --include='*.md' .
grep -rnoE '\b([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\b' --include='*.xml' --include='*.md' .
grep -rnoE '\b(192\.168|10\.|172\.(1[6-9]|2[0-9]|3[01]))\.' --include='*.xml' --include='*.md' .When we set out to integrate our network infrastructure into Wazuh, we found that:
- The BNC community rules for UniFi (
0999-bnc-unifi-rules.xml) are outdated and reference decoder names that no longer exist - There were no working decoders for MikroTik RouterOS 7.x with BSD syslog format
- UniFi CEF events (WiFi client tracking, admin access) were not decoded at all
- Fortinet built-in decoders work well, but without tuning, mDNS/Bonjour noise drowns out real alerts — especially in Apple-heavy environments
- Stormshield SNS firewalls emit a rich
key="value"syslog — traffic decisions, IPS alarms, IPsec/SSL VPN, admin audit, DHCP — that no community Wazuh decoder handles, so every event landed undecoded - Synology DSM can forward its Log Center to a syslog server, but there are no community decoders for what it sends — sign-ins, shared-folder access and the per-file SMB transfer log all landed as unclassified noise, when a NAS is precisely where your backups (a ransomware's first target) live
- Nothing measured decode coverage. A decoder that matches but extracts nothing looks healthy — the event is "decoded" — yet its rule renders as
UniFi switch []:and the message is lost. On a reference deployment that silent failure affected 99.9% of switch events, hiding loop-protection and 802.1X messages for months - Documentation on Wazuh PCRE2 limitations was scattered and incomplete
This repository provides production-tested decoders and rules that actually work, along with noise suppression tuning for real-world mixed environments.
Firewalls, switches, APs, MDM and IdP logs are exactly what auditors ask about — yet community rules rarely tag them, so they stay invisible in the compliance views. Here, every security-relevant rule is tagged with PCI DSS, GDPR, HIPAA, NIST 800-53 and TSC group tokens (plus GPG13 where the stock equivalent carries it) and MITRE ATT&CK technique ids, aimed at making a CISO's life easier:
- The Wazuh compliance dashboards (PCI DSS, GDPR, HIPAA, NIST 800-53, TSC tabs) and the MITRE ATT&CK module light up out of the box for this ruleset's sources — evidence for logging/monitoring controls (e.g. PCI DSS 10.2.x authentication trails, 10.6.1 log review, 11.4 intrusion detection, 10.5.2 audit-trail protection) without extra work
- Combos are copied verbatim from the stock ruleset per category (authentication success/failed/brute force,
firewall_drop/multiple_drops,config_changed,account_changed,attack/recon,service_availability, admin actions, sensitive-object reads), so the semantics match what analysts already know from the OS rules - Pure telemetry (level 0/1 noise, presence tracking, radio chatter) is deliberately left untagged — the dashboards show signal, not volume
If you find a rule that deserves a tag it does not have, open an issue — pushing the decoders as far as they can go for compliance is a goal of this project, not an afterthought.
Custom decoders and rules — nothing usable existed for RouterOS 7.x BSD syslog.
- Firewall: DROP IPv4/IPv6, invalid forward, INPUT protection
- DHCP: Server operations (discover, offer, request, ack, lease, removal)
- DHCP bindings:
assigned/deassignedwith IP↔MAC↔hostname extraction (live inventory),offering lease ... without success(client not completing) - DHCP debug option dump: one-option-per-line debug output decoded with a whitelisted option list (
mikrotik_dhcp_extra.xml) - Interface links: SFP+/ether link up/down with speed and duplex, plus a flapping correlation rule (6+ transitions in 5 min → level 8)
- Authentication: Login/logout with external IP detection
- Correlation rules: Port scan detection, brute force alerts
- MITRE ATT&CK: T1078 (Valid Accounts), T1133 (External Remote Services), T1110 (Brute Force)
Custom decoders and rules — the existing BNC community rules are broken. The CEF/syslog these parse is emitted by the UniFi Network controller itself, not by a specific box, so this applies to any UniFi OS console — validated on UDM Pro Max and UDM SE, and should hold on UDR, Cloud Key, UXG and self-hosted controllers running comparable Network/Protect versions.
- Firewall: iptables rules with full field extraction (src/dst IP, MAC, ports, flags)
- WiFi tracking: Client connected/disconnected/roamed with enriched fields (device alias, AP name, SSID, band, RSSI, duration)
- Protect: Smart detection (person/vehicle/animal/license plate), smart audio detection (alarms/glass-break), camera motion, sensor motion, camera tamper, loiter, device connect/disconnect, door sensor open/close, admin activity, intrusion correlation
- IoT sensors: Door opened/closed events from Protect sensors (category=iot), motion detection from Protect-managed IoT devices
- OS: Console access, application update notifications and tracking — covers the UniFi OS layer independently from Network and Protect
- Audit trail: Configuration changes (created/modified/removed), software updates, console access — essential for DORA/compliance
- DHCP: Pool exhaustion alerts, lease tracking
- Admin access: Management interface access with MITRE mapping
- Wired tracking: Client connected/disconnected on switches with port and link speed details
- Device management: Firmware update alerts with version tracking
- UPS monitoring: Battery power and AC restore events with battery percentage extraction (level 10 critical alert)
- Infrastructure alerts: Poor AP link speed detection
- Noise suppression: ubios-udapi-server, DPI stats, system events filtered at level 0
- UDM system syslog in debug mode (
unifi_udm_syslog.xml): in debug mode the UDM doubles its hostname in every system line, which silently breaksprogram_namepre-decoding — sudo, systemd, earlyoom,ips-update.shall go undecoded. Dedicated decoders restore: sudo audit trail (who ran what as root), IPS list update failures (tor.list.gz,alien.list.gzchecksum errors — level 5, repeated failures level 10), memory pressure (earlyoom telemetry with a <10% level-8 alert), systemd unit failures - AP kernel events (
unifi_ap_kernel.xml):kernel:lines carry no PID and escape the hostapd decoders. Covers 802.11 auth/assoc frames (station MAC, RSSI, algorithm — real security value), per-client DHCP state machine traces, DNS timeout tracking per station, VLAN assignment failures, and generic wlan errors/warnings - USW-Flex 2.5G & UPS Tower (
unifi_mca.xml): these adopt-managed devices log without a syslog timestamp (the line starts with the hostname), so the pre-decoder extracts nothing and every event was undecoded. Covers MCA informs, controller inform failures (HTTP 4xx/5xx with a persistent-failure correlation → level 7), port link up/down, STP transitions
UniFi's syslog config has two independent knobs (Settings → System → Logging): the per-device Logging Levels (Info/Verbose) and a separate Debug Logs toggle. This matters when you tune noise — cutting them does not cost you the security-relevant events, which ride the controller's own activity log:
| Always available (controller CEF activity log) | Needs Verbose / per-device syslog | Needs Debug Logs |
|---|---|---|
| WiFi client connect / disconnect / roam, wired connect/disconnect | Per-AP syslog daemon messages (unifi-ap) |
UDM system syslog: sudo trail, IPS list update failures, earlyoom, systemd unit failures (unifi_udm_syslog.xml, doubled-hostname mode) |
| Admin / console access, config created/modified/removed (audit) | AP kernel 802.11 auth/assoc, DNS timeouts, VLAN/DHCP-SM traces (unifi_ap_kernel.xml) |
Some CEF event types only surface at debug (e.g. Config Modified ID 546 — see learning #13) |
| IDS/IPS threats, firewall, DHCP, device/software updates, UPS | USW-Flex / UPS Tower MCA informs, port links (unifi_mca.xml) |
Takeaway: the station lifecycle (connect/disconnect/roam), admin audit, IDS and firewall are on the always-available controller log — safe to run with Debug off. Turn Debug Logs on only when you specifically want the UDM host's own admin audit (sudo/IPS-update/systemd), and accept its verbosity. Verbose device logging is worth it for the AP-kernel security signals (802.11 auth) but is the bulk of the volume.
The goal of this ruleset is that every event reaching the manager is claimed by a decoder. Two failure modes stand in the way, and only the first is obvious:
- No decoder at all. The event keeps its pre-decoded fields and nothing more.
It can also fall through to generic rules such as
1002("Unknown problem somewhere in the system") purely because its text contains error or failed. - A decoder that matches but extracts nothing. Far more insidious: coverage
metrics count the event as decoded, while the rule renders
[]where a field should be. This is what a stale regex assumption produces.
ac_housekeeping.xml closes the first gap for the long tail of infrastructure
daemons — package management, filesystem maintenance, Proxmox VE/PBS/PDM
auxiliaries, macOS ManagedClient helpers, Pi-hole FTL — using the repo's
program-only decoder pattern: a <program_name> selector and nothing else.
It marks the event decoded without inventing fields nobody consumes, and it
cannot steal events from a more specific decoder.
Two long-standing typos surfaced while doing this, both of which had silently disabled a decoder since it was written:
| Declared | Actual binary |
|---|---|
fwupd |
fwupdmgr |
pvepw-logger |
pve**fw**-logger (pve FireWall logger) |
Watch for duplicate rule IDs. Wazuh does not error on them — it keeps one definition and silently drops the other, which is how a whole message family can disappear. Before allocating a range:
grep -rhoE 'rule id="[0-9]+"' /var/ossec/etc/rules/*.xml | sort | uniq -dRestrict the glob to *.xml: Wazuh ignores other extensions, so a .bak left in
the rules directory is inert — but a naive grep -r will report it as a
duplicate and send you chasing a problem that does not exist.
Supplementary rules on top of Wazuh's built-in FortiGate decoders (which work well). The focus here is noise suppression and VPN monitoring — in environments with Apple devices, Bonjour/mDNS generates thousands of deny logs per minute that bury real security events.
- mDNS/LLMNR suppression: Filters out the massive volume of UDP/5353 and UDP/5355 deny logs typical in Apple/HomeKit/Bonjour environments
- UniFi discovery suppression: Filters UDP/10001 broadcast noise
- VPN IPsec monitoring: Alerts on denied traffic through site-to-site tunnels (routing issues, unauthorized access attempts). Filters on
action="deny"to avoid false positives from legitimate ZTNA traffic - System events: Performance stats, AV database updates, disk log rotation
- Correlation: Repeated VPN denies trigger higher-level alerts for investigation
Custom decoders and rules for Stormshield Network Security appliances — no community Wazuh decoders exist for the SNS key="value" syslog format. Every event begins with id=firewall ... logtype="<type>"; one parent decoder on the id=firewall signature dispatches to a child per logtype, with a catch-all so unknown logtypes still decode (stormshield_sns.xml).
- Traffic (
filter/connection): allowed/denied decisions with the 5-tuple (src/dst IP, ports, protocol), source user (SSL-VPN), interfaces. Allowed traffic is telemetry (level 1); blocks alert (level 3) - SSL/TLS inspection (
ssl): decipher verdict + message (e.g. "Connection not deciphered", "OpenVPN connection detected") — blocked SSL raised to level 4 - IPS / protections (
alarm): alarm id, class, target and risk — level 6, high-risk (risk ≥ 7) escalated to level 10 - Authentication (
auth): events such as "deprecated hash method" (weak password hashing) — level 5 - SSL VPN (
xvpn): nominative remote-access sessions (tunnel created, user authenticated) — level 4 - IPsec VPN (
vpn): IKE tunnel events (SA established/deleted), negotiation failures escalated to level 8 - Firewall admin audit (
server): who administers the appliance (webadmin login + serverd CLI/config commands) — level 5, configuration changes raised to level 8 - DHCP (
system,service=dhcp):DHCPACK= who is connected — IP/MAC/hostname extracted intodhcp.*fields for an asset inventory (level 3); other DHCP verbs stay telemetry - Periodic statistics (
ipsecstat/filterstat/authstat): IKE SA count, accepted/blocked totals, auth counters — traced at level 1 - Field mapping: 5-tuple to Wazuh static fields; the firewall verdict is a dynamic field
fw.action(the staticactionfield is rejected in rule matching on some builds — learning #23)
Decoders and rules for common programs collected by Wazuh agents through journald and left undecoded by the stock ruleset (journald_extra.xml):
- ZFS zed:
eid/class/poolextraction — io/data/checksum errors at level 9, error burst (10+ in 1 min on the same pool) at level 12, scrubs informational. Catches real incidents: ENOSPC/ENXIO bursts show up here before anything else complains - Proxmox pvescheduler: backup job start/finish per VM (level 3), backup errors (level 8)
- Debian/Ubuntu CRON via journald: the journald
program_nameisCRON, which the stockcronddecoder does not match — decoded with user + command extraction - qemu-ga: guest fsfreeze/fsthaw calls during backups
- LibreNMS service: component/level/message extraction — polling INFO reclassified to level 1, ERROR/CRITICAL/WARNING at level 5
- Program-only decoders for chronyd, snapd, dbus-daemon, pmxcfs, corosync, canonical-livepatch, fwupd, smokeping, opensearch-dashboards, proxmox-backup-proxy, and more — every journald event ends up decoded and traceable
- Proxmox VE task trail (
pve_tasks.xml): every qm/pct/API action logs a task with actor and result — VM/CT lifecycle completions (start/stop/reboot/migrate) at level 3 with who did it (qmreboot VM 110 by user@realm — OK), vzdump backups finished OK (L3) / FAILED (L8), snapshots/rollbacks L3, any task error L7, task starts and apt housekeeping as level-1 telemetry - Proxmox VE auth for ANY realm (
pve_extra.xml): the stock decoder only extracts the user for the@pam/@pverealms, so logins through custom realms (OIDC/SSO — e.g. an authentik realm) are not decoded at all. This full replacement (stock0440must be excluded — see learning #25) matches every realm and keeps the full principal (user@realm) indstuser, which is what an audit trail needs - Readable stock descriptions (
stock_description_overrides.xml): description-onlyoverwrite="yes"of PAM 5501/5502, sshd 5715, sudo 5402/5403 and Proxmox VE 87201/87203 — the alert list shows which user (and source IP where available) without opening each event; conditions, levels, MITRE and groups stay verbatim from stock
Malformed nginx/Apache access-log entries that the stock web-accesslog decoder ignores (web_accesslog_malformed.xml): empty requests ("" 400) and binary payloads (TLS handshakes sent to a plaintext port). These are exactly what Internet scanners generate against public endpoints. Each probe alerts at level 5 with srcip extracted; 6+ probes from the same source in 5 minutes escalate to level 8.
Custom decoders and rules for Jamf Pro's application logs — no community Wazuh decoders exist for these file-based logs. Jamf Pro does not write to syslog; logs are collected from the host with the Wazuh agent.
- Authentication (
JSSAccess.log): console & API logins (success/failure), logout, API token creation, password changes — with user, source IP and entry point (JSS console / Universal API / OAuth) - Audit trail (
JAMFChangeManagement.log): full CRUD on every Jamf object (accounts, groups, policies, configuration profiles, scripts, MDM commands, settings…) with actor, action and object type - Full audit visibility: every event is decoded and surfaced — including READ access at a low level (3) with the object name extracted — so nothing is hidden by default; sensitive-object reads (level 5) and FileVault key access (level 12) are escalated. Reads are ~96% of the audit volume, so set rule
100511to level 0 to suppress them once you have decided what you do not want to see. - Sensitive object escalation: admin Account changes (level 10), API client / client secret changes (level 12), MDM commands (level 9) with destructive commands (erase / wipe / unmanage) at level 12, security settings (Jamf Protect, Conditional Access, Change Management, Push Certificate, SMTP) (level 8)
- FileVault recovery key (PRK) access (level 12): detects an admin viewing a computer's FileVault Personal Recovery Key — Jamf logs this as an empty-object
READwhose body carriesFile Vault 2 ID(requires multi-line collection) - Object name extraction (
jamf_object_name): pulls the changed object's name from the multi-line body (e.g. which script/policy/profile) - Brute force correlation: repeated failed logins from the same source IP
- Instance install/upgrade tracking (
jamf-pro-installer.log): Jamf Pro upgrades, upgrade steps, disk-space warnings (level 5) and install failures (level 10) — the noisyJAMFSoftwareServer.logis intentionally not collected (99% INFO + a single recurring internal ERROR) - MITRE ATT&CK: T1110 (Brute Force), T1098 (Account Manipulation), T1555 (Credentials from Password Stores), T1562.001 / T1070 (Impair Defenses / Indicator Removal)
Decoders and rules for authentik's structlog JSON, collected through the Docker journald logging driver — no syslog listener, no sidecar, no log files to rotate. The audit trail (logger=authentik.events.models) is the compliance signal; HTTP access and worker chatter stay decoded at level 0, searchable in the archives.
- Audit trail: login / logout with user, client IP and auth method; failed login (level 5) with brute-force correlation (6+ failures from the same IP in 2 min → level 10); application authorization; account/credential changes (
user_write,password_set,password_reset); admin object changes (providers, flows, policies, applications); impersonation andsuspicious_request(level 8); policy/configuration exceptions - HTTP access (
authentik.asgi): every request decoded; 401/403 access denied (level 5), 404s with scanner correlation (10+ from the same source in 2 min → level 8 — seen live:GET /.envsweeps), 5xx server errors - Framework security (
django.security.*) plus a generic warn/error/critical mapping, so no logger escapes unclassified - Compliance tagging on the audit rules: PCI DSS, GDPR, HIPAA, NIST 800-53, TSC
- MITRE ATT&CK: T1078 (Valid Accounts), T1110 / T1110.001 (Brute Force), T1098 (Account Manipulation), T1595 (Active Scanning)
Native macos log format collection with a security-focused ULS query
predicate pushed centrally through a dedicated agent group (no per-Mac
configuration): the stock predicate (sudo, sshd, tccd, screen sharing,
securityd sessions) extended with MDM/ManagedClient (Jamf-managed fleet),
Gatekeeper/syspolicy (restricted to assessment/denied/XProtect/quarantine/
malware messages — the default category logs every lookup), XProtect,
opendirectoryd authentication, application firewall and package installs
(macos_uls.xml + macos_rules.xml):
- MDM destructive command (EraseDevice/DeviceLock/RemoteWipe) → level 10 (T1561); configuration profile install/remove → level 5; other MDM activity traced at level 1
- Jamf Protect (EDR) families decoded (JamfProtect/JamfDaemon/security-extension): activity traced L1, threat signals → level 9; Jamf Pro agent
/var/log/jamf.log(non-standard day-of-week timestamp → raw prematch decoder): policy execution L3, errors L5 - XProtect false-positive trap: every ~6h XProtect Remediator enumerates its own plugins ("Found XProtect plugin", "Discovered remediator") — the binary names contain "Remediator", so a loose
remediatmatcher turns the enumeration into a fake L12 storm (1200/day). Enumeration is pinned at L1; L12 requires real detection words - XProtect malware detection/remediation → level 12; Gatekeeper assessment denied → level 8 (T1204.002)
- opendirectoryd authentication failures → level 5 + local brute-force correlation (8+ in 4 min → level 10)
- Decoder pattern note: ULS-predecoded events only match
<program_name>-keyed decoders (same as stock0580-macos_decoders.xml) — a free-standing<prematch>decoder is never tried on them - Rule pattern note: when a stock generic rule (e.g. 2501 auth failure) wins the event, refine it with a child rule scoped by a custom decoder field instead of fighting the evaluation order
Rules for the PBS task archive (/var/log/proxmox-backup/tasks/archive, one raw UPID:…: <end> <status> line per completed task — pbs_tasks.xml + pbs_task_rules.xml). Backup integrity is the point: completions are traced (OK at level 1), and the signal is failure:
- Verify FAILED → level 10 (MITRE T1490): a verification task failing means a stored backup is corrupt — the single most important backup-integrity alert
- Backup FAILED → level 8, any other task failure (GC / prune / sync) → level 7, warnings /
unknown→ level 4 - Covers backup, verify, garbage_collection, prune, logrotate, reader tasks; the worker id keeps the datastore + target (
atlas-pbs:vm-110). Base at level 3 so failure lines aren't stolen by stock rule 1002 - Note the transient
read fs info … ENODEV/400 Bad Requeston a datastore-status API poll is the automount warming up (anx-systemd.automountNFS datastore) — not a task failure, and correctly not matched here
Rules for the Proxmox pveproxy access log (/var/log/pveproxy/access.log, collected by the agent on each PVE node — pve_access_rules.xml). The log is Apache-ish but uses an all-numeric date (DD/MM/YYYY), so the stock web-accesslog decoder wins the prematch and already extracts srcip/url/status/method — the custom rules build on it (keyed on the /api2/… paths nginx never serves) rather than fighting a shared decoder:
- Web/API login (
POST …/access/ticket200) → level 3; failed login (401/403) → level 6 with a brute-force correlation (6+ from one IP in 2 min → level 10) - Mutating API call (
POST/PUT/DELETEon/api2/…, excluding the auth ticket) → level 4 — an admin action on the hypervisor (VM/CT lifecycle, config) - UI polling (
GET /api2/… 200, the bulk) stays on stock rule 31108 at level 0 — traced, no alert - The authenticated
user@realmis not in this log's stock decode, but the same login is already decoded from journald (pvedaemon, rules 87201/87203 viapve_extra.xml) with the full principal — cross-reference by IP + time
Supplementary rules on top of Wazuh's built-in Office 365 module (office365_extra_rules.xml). The stock ruleset ingests the Management Activity API fine but leaves the security signal flat — routine mailbox auditing floods the alert view while real threats sit at level 3:
- Noise cut:
MailItemsAccessed(stock 91578) is overwritten from level 5 → level 3 — thousands/week of routine mailbox auditing no longer bury everything, but stay archived/searchable. The real signal is a spike by one user (compromise/exfil), caught by a mass-access correlation (60+ in 2 min → level 10) - Authentication:
UserLoginFailedsurfaced at level 5 with a brute-force correlation (8+ from the same account in 10 min → level 10) - Admin security changes (level 8): conditional-access policy, transport config/rules, anti-phish policy, org config — the settings an attacker weakens
- BEC / persistence (level 10): mailbox forwarding rules, inbox rules, mailbox delegation (
Add-MailboxPermission) — the classic Business Email Compromise footprint (MITRE T1114.003 / T1098.002) - Privileged role assignment (level 8), bulk HardDelete correlation (50+ in 5 min → level 8, possible evidence removal)
- Phishing / malware — severity by OUTCOME (stock 91556 fires L12 on every ThreatIntelligence event, even mails already blocked): overwritten to L5 when EOP/Defender handled it (blocked/quarantined — the defence worked, informational) and escalated to L12 only when the threat was actually DELIVERED to a mailbox (101116). Severity mapping is a vendor default, not a framework requirement — frameworks mandate logging and review; outcome-based severity is the sane operational reading
- Compliance tagging throughout (PCI DSS, GDPR, HIPAA, NIST 800-53, TSC); MITRE T1110/T1114/T1098/T1562/T1566/T1070
Per-container resource and health monitoring through two agent command localfiles (docker stats / docker ps, adapted from the Wazuh documentation PoC) — no API socket exposure, no sidecar:
- Resources (
docker stats): CPU, memory usage/limit/percent, network I/O per container — periodic samples are telemetry (level 1, archived); CPU or memory above 80% escalates to level 12 - Health (
docker ps): image, uptime and health status per container —unhealthyescalates to level 12 - Pairs naturally with the Authentik section (or any compose stack): the containers' stdout goes through the journald driver, their resource envelope through these commands
Decoders and rules for DSM 7.x Log Center syslog forwarding (BSD format) — no agent on the NAS:
- Authentication: sign-in success (level 3, with user, source IP and method — password/SSO), failed sign-in (level 5) with brute-force correlation (6+ failures from the same IP in 2 min → level 10)
- Share access: who mounted/accessed which shared folder, from which host, over which protocol (CIFS/SMB3, AFP...) — level 3
- SMB transfer log (
WinFileService): per-file operations decoded as level-1 telemetry (Time Machine and clone jobs legitimately move thousands of files — per-file alerts would be pure noise), topped by a mass-deletion correlation (100+ deletes from one source in 1 min → level 10, MITRE T1485) — a ransomware/destructive-job tripwire on the NAS - MITRE ATT&CK: T1078 (Valid Accounts), T1110 / T1110.001 (Brute Force), T1485 (Data Destruction)
MikroTik router (RouterOS) ──┐
UniFi Network controller ────┤
Fortinet FortiGate ──────────┼──► Syslog UDP/514 ──► Wazuh Server (rsyslog → analysisd)
Stormshield SNS ─────────────┤
Synology DSM (Log Center) ───┘
MikroTik and UniFi send BSD syslog; FortiGate and Stormshield SNS send their native key=value syslog. Wazuh's pre-decoder extracts timestamp and hostname before the custom decoders process the message payload. Jamf Pro, Authentik and Linux/Proxmox hosts are collected by the Wazuh agent (<localfile> / journald), not syslog.
Jamf Pro is collected differently: it does not emit syslog, so its JSSAccess.log and JAMFChangeManagement.log (and jamf-pro-installer.log) are read directly on the Jamf Pro host by the Wazuh agent via <localfile> — there is no syslog pre-decoder, so the decoders match the raw file lines. See Device configuration.
These are hard-won lessons from building these integrations:
-
Wazuh PCRE2: one child decoder per parent — When multiple
<decoder>children withtype="pcre2"share the same<parent>, only the first one matches. Solution: create separate parent/child pairs for each log type. -
[^\]]+fails in Wazuh PCRE2 — Use explicit character classes like[a-f0-9:]+for IPv6 addresses instead. -
BSD syslog pre-decoder consumes the hostname — Your decoder
<prematch>must NOT include the device hostname (e.g.,MikroTik), as it's already extracted by the pre-decoder. -
UniFi CEF
program_nameextraction — When UDM hostname contains hyphens (UDM-Pro-Max-AC), the pre-decoder extractsprogram_name: CEF. Decoders must use<program_name>^CEF</program_name>to match. -
<match>vs<field>in rules —actionis a static decoded field. Use<match>to search the raw log, or<decoded_as>with specific decoder names for targeting. -
Rule hierarchy matters — Use
<if_sid>for child rules. Without it, catch-all rules at the same level may match before specific ones. -
Anchor all prematches with
^— Generic prematches likeleasecan match unexpected content (e.g., "Please" contains "lease"). Always anchor with^to match only at the start of the decoded message. -
Fortinet noise in Apple environments — A single Apple TV or HomePod can generate 2000+ mDNS deny logs per minute on a FortiGate. Use level 0 rules in Wazuh to suppress noise while preserving archives for forensics.
-
Catch-all rules must be parents, not siblings — When multiple rules share the same
<decoded_as>without<match>, the catch-all (no<match>) wins unpredictably over specific rules. Solution: make the catch-all the parent rule, and specific rules its children via<if_sid>. This pattern is used for both UniFi Protect (100312 as parent) and Network CEF (100344 as parent). -
Avoid spaces in UDM hostname — The BSD syslog pre-decoder splits on spaces. A hostname like
UDM Pro Max ACgets truncated toUDM, breaking decoder matching for Protect events. Use hyphens instead:UDM-Pro-Max-AC. -
Decoder order matters across files — Wazuh loads decoder files alphabetically. A catch-all decoder in
ubiquiti.xmlwill match before specific decoders inunifi.xml. Place specific decoders (UPS, WiFi) in the same file as the catch-all, or ensure they load first. -
FortiGate VPN rule: always filter on action — A rule matching
vpntype="ipsecvpn"withoutaction="deny"will flag all ZTNA/IPsec traffic (accept, close, client-rst) as "denied". Always combine VPN type with action filter. -
UniFi debug mode reveals hidden CEF events — Some CEF event types (like Config Modified, ID 546) only appear when syslog is set to debug level. Enable debug temporarily to discover all available event types, then revert to normal.
-
UniFi Protect needs its own CEF parent decoder — A hyphenated UDM hostname makes the BSD pre-decoder set
program_name=CEF(learning #4); once that happens, the bareubiquitiprematch decoder no longer matches, so Protect events silently stop decoding (decoder: {}, no alert) while Network/OS keep working because they have dedicated*-cef-parentdecoders with<program_name>^CEF</program_name>. Fix: add aunifi-protect-cef-parent(program_name CEF + prematchUbiquiti\|UniFi Protect) and point the Protect rule (100312) at it instead ofdecoded_as ubiquiti. Symptom to watch for: UniFi Network events alert but Protect events (smart detect, tamper, motion) never do. -
UniFi AP (hostapd) events collide with the built-in
symantec-avdecoder — Access points/switches send syslog as<12-hex-MAC>,<model>: hostapd[..]: ... STA <mac> IEEE 802.11: associated|disassociated. That leading<12 hex>,matches the built-insymantec-avprematch (^\w{12},); its own regex then fails (our prefix is hex, not the digits-CSV it expects). On some Wazuh builds the engine falls through to the next decoder sounifi-apmatches, but on others (observed on 4.14.5) it locks ontosymantec-avon the prematch alone — AP client connect/disconnect events get mis-labelled "symantec-av" (fires rule 7300) andunifi-apnever runs. Two managers on the same version can behave differently here (decoder_dir ordering does NOT override it). Fix — upgrade-safe, no base-file edits — exclude the symantec decoder inossec.conf:<decoder_exclude>0330-symantec_decoders.xml</decoder_exclude>plus the matching<rule_exclude>lines. Symptom to watch for: "who connects to which AP/switch" events appear under Symantec AV instead of UniFi. -
A parent decoder's
<regex>/<order>is IGNORED once it has children — analysisd never applies the parent's own field extraction when<parent>children exist, whether a child matches or not. Pattern that works: parent =<prematch>only; every child captures all the fields it needs (including "parent-level" identity like program/pid or device MAC); finish with one or two catch-all children that capture identity + message for lines no specific child handles. -
A child whose prematch matches but whose regex fails STOPS the sibling chain — the remaining children are never tried and the event stays field-less. A trailing period is enough to trigger this (
Succeeded.vs a regex ending in(Succeeded|Failed.*)$). Keep child regex endings tolerant:[^.]*,\.?$. -
Root rules are evaluated by descending level — stock rule 1002 steals your events — a level-1 base rule on
decoded_asloses every log containingfailed/errorto the built-in "Unknown problem" rule 1002 (level 2). Put basedecoded_asrules at level 3 and reclassify noise down to level 1 through child rules; never anchor a rule tree on a level-1/2 root. -
<field name="status">is rejected in rules —status(likeaction,id,srcip...) is a static field; analysisd fails to load the rule with "Field 'status' is static". Match static fields with their dedicated element (<status>^down$</status>, OS_Match syntax — no\dclasses) or fall back to<match>on the raw log. -
<program_name>uses OS_Regex where\d*does not match —^python\d*$silently matches nothing. Use literal alternations:^python$|^python2$|^python3$. -
UDM debug syslog doubles the hostname and kills
program_namepre-decoding — in debug mode every UDM system line readsUDM-Pro-Max-AC UDM-Pro-Max-AC prog[pid]: msg; the BSD pre-decoder consumes the first hostname, chokes on the second, and never setsprogram_name— so sudo/systemd/earlyoom events silently stop matching the stock decoders.unifi_udm_syslog.xmlre-extracts program/pid from the message body (anchored^UDM-so it also covers UDM SE). -
Some UniFi devices log with NO syslog timestamp — USW-Flex 2.5G switches and UPS Tower units send
HOSTNAME <mac>,<model>-<fw>: SUBSYS: msgwith no timestamp at all; the pre-decoder extracts nothing and prematches must match from the raw line start (^\S+ [0-9a-fA-F]{12},...). -
Stormshield SNS
key="value": dispatch on the trailinglogtype=, keep the verdict dynamic — every SNS event ends withlogtype="filter|ssl|alarm|vpn|server|...". One parent on^id=firewall+ one child per logtype (each discriminating on its ownlogtype=) + a catch-all last decodes every type cleanly. Decode the firewall verdict into a dynamic field (fw.action), not the staticaction— matching a static field with<field name="action">fails on some builds (learnings #5/#19). Space-bearing values (msg="...") need a quoted capture, andsrc=/dst=must be space-anchored so they don't matchmodsrc=/origdst=. -
Wazuh
<field>matching is substring, not exact — anchor it —<field name="logtype">auth</field>also matchesauthstat(andvpnmatchesxvpn), so the wrong rule fires and the specific one never runs. Anchor every discriminating field match:<field name="logtype" type="pcre2">^auth$</field>. Same trap as prematches (learning #7), one layer up in the rules. -
The first matching prematch wins even if its regex then fails — a sibling decoder can never fix a stock decoder — when several children share a parent, analysisd picks the first child whose
<prematch>matches and stops there; if that child's<regex>then extracts nothing, the event stays undecoded and your sibling with the better regex is never tried. To fix a stock child decoder you must<decoder_exclude>the whole stock file inossec.confand ship a full replacement (keeping the stock parent decoder names so stock rules keep matching) — that is exactly whatpve_extra.xmldoes for Proxmox VE non-@pam/@pverealms.
cp decoders/mikrotik_custom.xml /var/ossec/etc/decoders/
cp decoders/mikrotik_dhcp_extra.xml /var/ossec/etc/decoders/ # DHCP bindings + debug dump + iface links
cp decoders/ubiquiti.xml /var/ossec/etc/decoders/
cp decoders/unifi_ap.xml /var/ossec/etc/decoders/ # UniFi AP hostapd (client assoc/disassoc) - see learning #15
cp decoders/unifi_ap_kernel.xml /var/ossec/etc/decoders/ # UniFi AP kernel: 802.11 auth/assoc, DHCP-SM, DNS timeouts
cp decoders/unifi_udm_syslog.xml /var/ossec/etc/decoders/ # UDM system syslog in debug mode (doubled hostname) - see learning #21
cp decoders/unifi_mca.xml /var/ossec/etc/decoders/ # USW-Flex 2.5G + UPS Tower (no syslog timestamp) - see learning #22
cp decoders/journald_extra.xml /var/ossec/etc/decoders/ # zed, pvescheduler, CRON, qemu-ga, LibreNMS...
cp decoders/web_accesslog_malformed.xml /var/ossec/etc/decoders/ # scanner probes on public endpoints
cp decoders/authentik.xml /var/ossec/etc/decoders/ # Authentik IdP (structlog JSON via journald)
cp decoders/pve_extra.xml /var/ossec/etc/decoders/ # Proxmox VE auth, any realm (requires excluding stock 0440 - step 4)
cp decoders/docker_decoders.xml /var/ossec/etc/decoders/ # Docker container resources/health (agent command localfiles)
cp decoders/synology.xml /var/ossec/etc/decoders/ # Synology DSM (Log Center syslog: auth, share access, SMB transfer log)
cp decoders/pve_tasks.xml /var/ossec/etc/decoders/ # Proxmox VE task trail (actor+result on qm/pct/vzdump/... tasks)
# Only if you have a Stormshield SNS firewall:
cp decoders/stormshield_sns.xml /var/ossec/etc/decoders/ # Stormshield SNS key=value syslog (all logtypes)
# Only if you use UniFi Protect (CEF format):
cp decoders/unifi.xml /var/ossec/etc/decoders/cp rules/mikrotik_rules.xml /var/ossec/etc/rules/
cp rules/mikrotik_extra_rules.xml /var/ossec/etc/rules/ # DHCP bindings, link flapping (100800-100806)
cp rules/unifi_rules.xml /var/ossec/etc/rules/
cp rules/unifi_ap_rules.xml /var/ossec/etc/rules/ # UniFi AP hostapd rules (100368-100370)
cp rules/unifi_ap_kernel_rules.xml /var/ossec/etc/rules/ # AP kernel events (100750-100758)
cp rules/unifi_udm_sys_rules.xml /var/ossec/etc/rules/ # UDM system: sudo, IPS updates, earlyoom (100700-100708)
cp rules/unifi_mca_rules.xml /var/ossec/etc/rules/ # USW-Flex + UPS Tower (100770-100778)
cp rules/journald_extra_rules.xml /var/ossec/etc/rules/ # zed, pvescheduler, CRON... (100840-100858)
cp rules/web_malformed_rules.xml /var/ossec/etc/rules/ # scanner detection (100820-100821)
cp rules/stormshield_sns_rules.xml /var/ossec/etc/rules/ # Stormshield SNS (100900-100919)
cp rules/authentik_rules.xml /var/ossec/etc/rules/ # Authentik IdP (101000-101042)
cp rules/stock_description_overrides.xml /var/ossec/etc/rules/ # readable user/IP in stock PAM/sshd/sudo/PVE alerts
cp rules/docker_rules.xml /var/ossec/etc/rules/ # Docker container resources/health (100100-100106)
cp rules/synology_rules.xml /var/ossec/etc/rules/ # Synology DSM + Hyper Backup (100950-100966)
cp rules/office365_extra_rules.xml /var/ossec/etc/rules/ # Office 365: noise cut + auth/admin/BEC/phishing (101100-101125)
cp rules/pve_access_rules.xml /var/ossec/etc/rules/ # Proxmox pveproxy Web/API access (101131-101135, on stock web-accesslog)
cp rules/pve_task_rules.xml /var/ossec/etc/rules/ # Proxmox VE task trail (100860-100867)
cp rules/pbs_task_rules.xml /var/ossec/etc/rules/ # Proxmox Backup Server task archive (100870-100875)
cp decoders/macos_uls.xml /var/ossec/etc/decoders/ # macOS Unified Logging System (program_name-keyed)
cp rules/macos_rules.xml /var/ossec/etc/rules/ # macOS MDM/Gatekeeper/XProtect/auth (101150-101179)
# Only if you have a FortiGate:
cp rules/fortigate_rules.xml /var/ossec/etc/rules/chown wazuh:wazuh /var/ossec/etc/decoders/mikrotik_custom.xml
chown wazuh:wazuh /var/ossec/etc/decoders/ubiquiti.xml
chown wazuh:wazuh /var/ossec/etc/rules/*.xml
chmod 660 /var/ossec/etc/decoders/*.xml /var/ossec/etc/rules/*.xmlAdd to /var/ossec/etc/ossec.conf inside <ruleset>:
<rule_exclude>0999-bnc-unifi-rules.xml</rule_exclude>
<!-- Required for pve_extra.xml (see learning #25): the stock PVE decoder
only extracts users for the @pam/@pve realms and shadows any sibling. -->
<decoder_exclude>0440-proxmox-ve_decoders.xml</decoder_exclude>
<!-- Required for UniFi AP hostapd events (see learning #15): the built-in
symantec-av decoder false-matches the "<12-hex>," AP syslog prefix. -->
<decoder_exclude>0330-symantec_decoders.xml</decoder_exclude>
<rule_exclude>0120-symantec-av_rules.xml</rule_exclude>
<rule_exclude>0125-symantec-ws_rules.xml</rule_exclude>/var/ossec/bin/wazuh-analysisd -t
systemctl restart wazuh-manager/system logging action add name=wazuh target=remote remote=<WAZUH_IP> remote-port=514 src-address=<ROUTER_IP> bsd-syslog=yes
/system logging add action=wazuh topics=firewall
/system logging add action=wazuh topics=dhcp
/system logging add action=wazuh topics=system
On the UniFi Network controller (UDM Pro Max/SE, UDR, Cloud Key, UXG, self-hosted…), navigate to Settings → System → Activity Logging (Syslog):
- Select SIEM Server
- Server Address:
<WAZUH_IP> - Port:
514 - Contents: Gateway, Access Points, Switches, Admin Activity, Clients, Security Detections, Triggers, Devices, Updates, VPN, Firewall Default Policy
Tip: Avoid spaces in your UDM hostname (e.g., use
UDM-Pro-Max-ACinstead ofUDM Pro Max AC). Spaces break the BSD syslog pre-decoder and cause Protect events to be misclassified.
The FortiGate should be configured to send syslog to your Wazuh server. No special format is needed — Wazuh's built-in decoders handle FortiGate's native key=value format. Our custom rules layer on top for noise suppression and enhanced alerting.
config log syslogd setting
set status enable
set server "<WAZUH_IP>"
set port 514
set facility local7
set source-ip "<FORTIGATE_IP>"
end
On the appliance, add a syslog profile under Configuration → Notifications → Logs - Syslog - IPFIX → Syslog: destination = your Wazuh server, UDP port 514, and enable the log families you want (filter, connection, alarm, system, ssl, vpn, xvpn, server, authentication…). Then declare a syslog listener on the Wazuh manager in /var/ossec/etc/ossec.conf and restart:
<remote>
<connection>syslog</connection>
<port>514</port>
<protocol>udp</protocol>
<allowed-ips>198.51.100.1/32</allowed-ips> <!-- the SN's real source IP -->
</remote>Firewall self-traffic through IPsec: if the SN reaches Wazuh over a policy-based IPsec tunnel, its self-originated syslog must be sourced from an IP inside the tunnel's local network, or it never enters the tunnel. Confirm the received source with
sudo tcpdump -ni any udp port 514on the Wazuh host, then pin<allowed-ips>to that exact/32— otherwisewazuh-remotedsilently drops packets whose source isn't whitelisted.
Jamf Pro writes log4j files under /usr/local/jss/logs/ — it does not send syslog. Install the Wazuh agent on the Jamf Pro host and add to /var/ossec/etc/ossec.conf:
<localfile>
<log_format>syslog</log_format>
<location>/usr/local/jss/logs/JSSAccess.log</location>
</localfile>
<localfile>
<log_format>multi-line-regex</log_format>
<location>/usr/local/jss/logs/JAMFChangeManagement.log</location>
<multiline_regex match="start" replace="no-replace">^\[</multiline_regex>
</localfile>
<localfile>
<log_format>syslog</log_format>
<location>/usr/local/jss/logs/jamf-pro-installer.log</location>
</localfile>JAMFChangeManagement.log is collected as multi-line (match="start" on the ^[ header) so each change is a single event carrying the header plus the detail body — i.e. the object name and the changed fields, not just the object type. Note that Jamf logs mostly metadata, but some changes (Configuration Profile payloads, script-content edits) include larger/sensitive blobs — tune at the agent or rule level if needed. Then restart the agent. In Jamf Pro, enable the audit trail under Settings → System → Change Management → Use Log File (directory /usr/local/jss/logs). The agent runs as root, so it can read both files regardless of their jamftomcat ownership.
Why file collection and not syslog? Jamf Pro's Change Management can alternatively forward to a syslog server (Settings → System → Change Management → Use Syslog Server). We deliberately chose not to (for now): syslog only covers Change Management — not
JSSAccess.logor the installer log — it can drop events over UDP, and being line-oriented it would lose the multi-line detail body that carries the object name and the FileVault recovery-key signature (File Vault 2 ID). File collection via the agent keeps everything, reliably. (Use syslog instead if you only need a condensed change feed forwarded to a central collector.)
Authentik containers write structlog JSON to stdout. Route it through the host's journald with the Docker logging driver — the tag becomes the journald program_name the decoder matches on:
# docker-compose.yml — same block on the worker service with tag authentik-worker
services:
server:
logging:
driver: journald
options:
tag: authentik-serverAgent side (/var/ossec/etc/ossec.conf), the standard journald collection is all it takes:
<localfile>
<location>journald</location>
<log_format>journald</log_format>
</localfile>Recreate the containers (docker compose up -d) for the driver change to apply. The decoder carries no payload regex: the JSON plugin decoder extracts every key (logger, event, action, client_ip, nested user.username, context.*, method, status, remote), which keeps it robust across authentik releases. Non-JSON payloads (tracebacks) still match the parent decoder, so the severity-mapping rules see them.
On the Docker host's agent (or centrally via a shared group agent.conf, as shown), two command localfiles sample the containers; the out_format prefix is what the decoders match on:
<agent_config>
<!-- Command to extract container resource information. -->
<localfile>
<log_format>command</log_format>
<command>docker stats --format "{{.Container}} {{.Name}} {{.CPUPerc}} {{.MemUsage}} {{.MemPerc}} {{.NetIO}}" --no-stream</command>
<alias>docker container stats</alias>
<frequency>120</frequency>
<out_format>$(timestamp) $(hostname) docker-container-resource: $(log)</out_format>
</localfile>
<!-- Command to extract container health information. -->
<localfile>
<log_format>command</log_format>
<command>docker ps --format "{{.Image}} {{.Names}} {{.Status}}"</command>
<alias>docker container ps</alias>
<frequency>120</frequency>
<out_format>$(timestamp) $(hostname) docker-container-health: $(log)</out_format>
</localfile>
</agent_config>Remote commands pushed from the manager require logcollector.remote_commands=1 in the agent's local_internal_options.conf.
In DSM: Log Center → Log Sending — enable sending to a syslog server: your Wazuh manager, port 514, UDP, format BSD (RFC 3164) (the decoders are built against the BSD shape; Wazuh's remoted does not terminate TLS). For the per-file SMB transfer log, also enable Control Panel → File Services → SMB → Advanced → Transfer log.
If your manager listens on IPv6, the syslog <remote> block needs <ipv6>yes</ipv6> alongside the v6 allowed-ips, or remoted fails to bind (CRITICAL 1206):
<remote>
<connection>syslog</connection>
<port>514</port>
<protocol>udp</protocol>
<ipv6>yes</ipv6>
<allowed-ips>2001:db8:aaaa::/64</allowed-ips>
<local_ip>2001:db8:aaaa::69</local_ip>
</remote>Create /etc/rsyslog.d/10-mikrotik.conf:
$template MikroTikFormat,"/var/ossec/logs/MikroTik/%HOSTNAME%/messages-%$YEAR%-%$MONTH%-%$DAY%.log"
if $fromhost-ip == '<MIKROTIK_IP>' then ?MikroTikFormat
& stop
| Range | Device | Category |
|---|---|---|
| 100100-100106 | Docker | Container resources & health (samples L1; CPU/mem >80% and unhealthy at L12) |
| 100200-100211 | MikroTik | Firewall (DROP, invalid, scan detection) |
| 100220-100223 | MikroTik | Authentication (login, brute force) |
| 100230-100233 | MikroTik | DHCP operations |
| 100240-100243 | MikroTik | System (catch-all L3; netwatch probes L1; RouterOS upgrade trail: package installed L5, router rebooted — incl. power-loss "without proper shutdown" — L8) |
| 100300-100302 | UniFi | Firewall (base, DROP, Allow suppressed) |
| 100310-100316 | UniFi | Protect (smart detect, camera/sensor motion, door sensor, admin activity, intrusion correlation) |
| 100320-100325 | UniFi | DHCP (DHCPACK = who-is-connected L3, IP/MAC/hostname extracted; negotiation + inotify chatter L1; pool exhaustion L6 with an expected-interface exception L1) |
| 100330-100331 | UniFi | Noise suppression (services, DPI) |
| 100340-100349 | UniFi | WiFi, Wired, Network CEF, Device Updates, UPS power |
| 100350 | UniFi | System suppression |
| 100351-100357 | UniFi | Network audit & infra (admin console access w/ IP, config created/modified incl. security-setting escalation, removed, software updates, AP link speed) |
| 100358-100359 | UniFi | IDS/IPS threat detected & blocked (readable signature + src/dst; high-risk escalated to L10) |
| 100360-100363 | UniFi | OS events (console access, application update available/completed) |
| 100364-100367 | UniFi | Protect (camera tamper, smart audio detect, device connect/disconnect) |
| 100368-100372 | UniFi | AP per-AP syslog (readable daemon+message, Wi-Fi client assoc/disassoc, fan/thermal telemetry + high-temp escalation) |
| 100380 | UniFi | Switch per-switch syslog (readable daemon+message) |
| 100381-100382 | UniFi | AP mesh topology (meshed / lost mesh uplink) |
| 100383-100384 | UniFi | IDS/IPS outbound threat (internal client reached a flagged destination; high-risk escalated) |
| 100400-100401 | FortiGate | Noise suppression (mDNS, UniFi discovery) |
| 100410-100411 | FortiGate | VPN IPsec (denied traffic, VPN events) |
| 100420-100424 | FortiGate | System (perf stats L1, disk rotation L4, AV signature refresh L1, FortiGuard update succeeded L3 / FAILED L5) |
| 100430 | FortiGate | Correlation (repeated VPN denies) |
| 100440-100445 | FortiGate | IPsec tunnel state (tunnel down L8, up L3, phase-2 L5/L3, flapping L10, stats/negotiation traced L1) |
| 100446-100447 | FortiGate | Edge local-in deny from WAN (telemetry L1, srcintfrole="wan" — no site-specific prefix list) + same-source probe campaign L8 |
| 100500-100507 | Jamf Pro | Authentication (login, logout, token, failed login, password change, brute force) |
| 100510-100516 | Jamf Pro | Change management (READ surfaced at low level + object name, sensitive objects + FileVault PRK escalated, create/update/delete) |
| 100520-100525 | Jamf Pro | Sensitive object changes (API client/secret, account, MDM commands incl. destructive erase/wipe, security settings, audit log retention) |
| 100530-100534 | Jamf Pro | Instance install/upgrade (upgrade steps, disk warning, install failure) |
| 100700-100708 | UniFi | UDM system syslog (sudo trail, IPS list update failures + repeat escalation L10, earlyoom memory <10% L8, systemd unit failures) |
| 100750-100758 | UniFi | AP kernel (802.11 auth/assoc, DNS timeouts, VLAN failures, DHCP-SM traces, wlan errors) |
| 100770-100778 | UniFi | USW-Flex 2.5G + UPS Tower (port links, inform failures + persistence L7, heartbeats) |
| 100800-100806 | MikroTik | DHCP bindings (assigned/deassigned/offering), debug option dump, iface links + flapping L8 |
| 100820-100821 | Web | Malformed requests / scanner probes (single L5, repeated L8) |
| 100840-100858 | journald | ZFS zed (errors L9, burst L12), Proxmox backups (finished OK L3 — 100848, error L8), CRON, qemu-ga, LibreNMS, chronyd |
| 100860-100867 | Proxmox VE | Task trail with actor+result (vzdump OK L3 / FAILED L8, VM/CT lifecycle by user L3, snapshots L3, task errors L7, starts/housekeeping L1) |
| 100870-100873, 100874-100875 | Proxmox Backup Server | Task archive: OK L1, warnings/unknown L4, task FAILED L7, backup FAILED L8, VERIFY FAILED = stored backup corrupt L10 (T1490) |
| 101150-101179 | macOS (ULS) | MDM activity L1 / profile change L5 / destructive command L10 (T1561), Gatekeeper denied L8, XProtect scan chatter L1 / real malware detection L12, opendirectoryd auth fail L5 + brute force L10, app firewall L3, installs L3, Jamf Protect activity L1 / threat L9, Jamf Pro agent (jamf.log) trail L1 / policy L3 / error L5 |
| 100900-100910 | Stormshield | Traffic (allowed L1 / blocked L3), SSL (blocked L4), IPS alarm L6 / high-risk L10, auth L5, SSL-VPN L4, system, plugin |
| 100911-100912 | Stormshield | DHCP (all verbs L1; DHCPACK = who-is-connected L3, IP/MAC/hostname extracted) |
| 100913-100919 | Stormshield | Admin audit L5 / config change L8, IPsec VPN L4 / failure L8, ipsec/filter/auth stats L1 |
| 100950-100959 | Synology | DSM sign-in L3 / failed L5 + brute force L10, share access L3, SMB file ops L1 + mass-deletion (ransomware tripwire) L10 (Time Machine paths excluded, 100966) |
| 100960-100966 | Synology | Hyper Backup off-site: started L1, finished successfully L3, failed L8, version rotation L1 |
| 91578, 91556 (overwrite) | Office 365 | MailItemsAccessed L5→L3 (noise), phishing/malware by outcome: EOP-blocked L5, delivered to mailbox L12 (101116) |
| 101100-101125 | Office 365 | Failed login L5 + brute force L10, mass mailbox access L10, admin security changes L8, BEC forwarding/delegation L10, role assignment L8, bulk HardDelete L8 |
| 101131-101135 | Proxmox pveproxy | Web/API login L3, failed login L6 + brute force L10, mutating API call L4 (built on the stock web-accesslog decoder; UI polling stays stock 31108 L0) |
| 101000-101003 | Authentik | Base + periodic chatter pinned to L0 (health checks, worker scheduling, router refresh) |
| 101010-101022 | Authentik | Audit trail (login/logout L3, failed login L5 + brute force L10, app authorization L3, account/credential change L5, config change L5, impersonation L8, suspicious request L8, exceptions L6) |
| 101030-101034 | Authentik | HTTP access (all decoded L0, 401/403 L5, 404 L3 + scanner correlation L8, 5xx L5) |
| 101035-101042 | Authentik | Django security L4 + generic warn/error/critical mapping (L4/L5/L7) |
| Field | Content | Example |
|---|---|---|
srcip |
Source IP | 192.0.2.100 |
dstip |
Destination IP | 8.8.8.8 |
srcport |
Source port | 54321 |
dstport |
Destination port | 443 |
protocol |
Protocol | TCP / UDP |
action |
Firewall chain | DROP |
| Field | Content | Example |
|---|---|---|
srcip |
Client IP | 192.0.2.100 |
srcmac |
Client MAC | aa:bb:cc:dd:ee:01 |
dstuser |
Device alias | iPhone de Jean |
extra_data |
Access Point | AP-Bureau-RDC |
system_name |
SSID | MonSSID |
protocol |
WiFi band | 6e |
data |
RSSI or duration | -41 / 2m |
action |
Network name | LAN |
| Field | Content | Example |
|---|---|---|
ap_daemon |
Daemon that emitted the line | syswrapper / wpa_supplicant |
ap_message |
Readable message body | Trigger rrm scan(1): ... |
client_mac |
Wi-Fi client MAC (assoc/disassoc) | aa:bb:cc:dd:ee:01 |
ap_radio |
Radio interface | wifi1ap2 |
wifi_action |
Association state | associated / disassociated |
ap_model |
AP model + firmware | U6-IW-6.8.2+15592 |
| Field | Content | Example |
|---|---|---|
ap_temp |
Sensor temperature (°C) | 76 |
sensor_zone |
Sensor zone | zone3 |
fan_speed |
Fan speed (%) | 0 |
fan_rpm |
Fan RPM | 0 |
pwm_set / pwm_actual |
Fan PWM set / actual | 63 / 55 |
| Field | Content | Example |
|---|---|---|
dstuser |
Client alias | Living-Room-TV |
srcip |
Client IP | 192.0.2.37 |
srcmac |
Client MAC | aa:bb:cc:dd:ee:02 |
switch_name |
Switch/AP it connected to | USW-Flex-Office |
switch_port |
Port number | 2 |
network / vlan |
Network name / VLAN id | VLAN_IoT / 200 |
data |
Session duration (on disconnect) | 1h 52m |
| Field | Content | Example |
|---|---|---|
sw_daemon |
Daemon that emitted the line | mcad |
sw_message |
Readable message body | ui-ubus-utils...: Failed to find ubus object |
sw_model |
Switch model + firmware | USW-Pro-Max-48-7.5.4+17029 |
| Field | Content | Example |
|---|---|---|
signature |
IPS signature / threat name | ET MALWARE ... RAT C2 Domain in DNS Lookup |
risk |
Risk level | high |
policy |
Policy name | Malware and Trojans |
srcip / dstip |
Source / destination IP | 192.0.2.10 / 198.51.100.5 |
signature_id |
IPS signature ID | 2068515 |
srcuser / srcmac |
Internal client (outbound threat) | LAPTOP-1 / aa:bb:cc:dd:ee:ff |
| Field | Content | Example |
|---|---|---|
dstuser |
Admin who performed the action | jdoe |
srcip |
Source IP of the admin | 203.0.113.10 |
access_method |
Access method (console access) | web |
setting |
Setting key changed (config modified) | ip_filtering.countries |
| Field | Content | Example |
|---|---|---|
ap_name |
AP whose mesh state changed | AP-Garden |
mesh_parent |
Mesh uplink AP (new or prior) | AP-House |
| Field | Content | Example |
|---|---|---|
extra_data |
UPS device name | UPS Bureau Cave |
srcip |
UPS IP address | 192.0.2.40 |
data |
Battery remaining | 93.0% |
action |
Full message | UPS Bureau Cave has lost AC power... |
| Field | Content | Example |
|---|---|---|
program / pid |
Program re-extracted from the doubled-hostname line | sudo / 21901 |
srcuser / dstuser |
sudo: invoking user / target user | uid / root |
command |
sudo: command executed | /usr/lib/uid-agent/scripts/apps_info.sh |
ips.file |
IPS list whose update failed | /run/ips/rules/tor.list.gz |
mem.avail_pct |
earlyoom: available memory % | 35.05 |
systemd.unit / status |
systemd unit and outcome | wifiman-proxy-cert.service / Succeeded |
| Field | Content | Example |
|---|---|---|
station.mac |
Client station MAC | aa:bb:cc:dd:ee:01 |
wifi.iface / wifi.vap |
Radio interface / VAP index | wifi2ap8 / 4 |
wifi.auth_alg / wifi.rssi |
802.11 auth algorithm / RSSI | 2 / 52 |
dns.timeouts |
DNS timeouts counted for the station | 4 |
vlan.tag |
VLAN that failed to assign | 1 |
dhcp.trace |
DHCP state machine trace | d-10-o-30-r-10-a |
wlan.level / wlan.facility |
wlan message severity / facility | E / ANY |
| Field | Content | Example |
|---|---|---|
device.name |
Device hostname | USWFlex25GOffice |
device.mac / device.model / device.fw |
Identity | 942a6f... / USW-Flex-2.5G-5 / 7.4.2.1039 |
port / status |
Switch port and link state | 2 / down |
status (UPS) |
Inform HTTP status | 503 |
mca.rc / mca.reason |
Inform failure code / reason | 7 / Server Busy |
url |
Controller inform URL | http://192.0.2.2:8080/inform |
| Field | Content | Example |
|---|---|---|
dhcp.server |
DHCP server instance | dhcp1 |
action |
Binding operation | assigned / deassigned |
srcip / srcmac / srchost |
Bound IP / MAC / hostname | 192.0.2.33 / 00:24:E4:... / laptop |
dhcp.option / dhcp.value |
Debug dump option | Host-Name / "Mac" |
iface / status / speed |
Link transitions | sfp-sfpplus11 / down / 10G |
| Field | Content | Example |
|---|---|---|
srcip |
Scanner source IP | 203.0.113.66 |
id |
HTTP status | 400 |
http.request_raw |
Raw binary payload (TLS handshake bytes) | \x16\x03\x01... |
| Field | Content | Example |
|---|---|---|
zfs.class |
Event class | io / data / io_failure / scrub_finish |
zfs.pool |
Pool name | rpool |
zfs.eid / zfs.detail |
Event id / raw detail (err=6 ENXIO, err=28 ENOSPC...) | 6743 / err=28 ... |
Wazuh's built-in FortiGate decoder extracts all native fields: srcip, dstip, srcport, dstport, action, policyid, service, srcintf, dstintf, devname, logid, level, and more. Our custom rules add context through enhanced descriptions that surface VPN tunnel names and traffic patterns.
| Field | Content | Example |
|---|---|---|
srcip / dstip / srcport / dstport / protocol |
Connection 5-tuple | 198.51.100.24 / 203.0.113.67 / 56594 / 443 / https |
fw.action |
Firewall verdict (dynamic field) | pass / block |
srcuser |
Authenticated user (SSL-VPN / admin) | jdoe |
logtype |
SNS log family | filter / alarm / vpn / server |
sns.msg |
Event message | OpenVPN connection detected |
alarm.id / alarm.class / alarm.risk |
IPS alarm | 118 / protocol / 10 |
dhcp.event / dhcp.ip / dhcp.mac / dhcp.hostname |
DHCP lease (asset inventory) | DHCPACK / 198.51.100.69 / 00:11:22:33:44:bb / Laptop-01 |
http.method / http.status |
Protocol plugin (HTTP) | GET / 200 |
srcintf / dstintf |
Interfaces | lan / wan |
| Field | Content | Example |
|---|---|---|
logger |
structlog logger — the routing key | authentik.events.models / authentik.asgi |
action |
Audit action (Wazuh static field — learning #19) | login / login_failed / model_updated |
client_ip |
Client IP of the audited action | 203.0.113.67 |
user.username |
Acting user (nested JSON) | jdoe |
context.username |
Attempted username on failed logins | admin |
context.auth_method |
How the user authenticated | password / auth_webauthn_pwl |
method / event / remote |
HTTP request: verb, path, client | GET / /api/v3/... / 203.0.113.67 |
level |
structlog severity | info / warning / error |
| Field | Content | Example |
|---|---|---|
dstuser |
Account involved (sign-in, share access, file op) | jdoe |
srcip |
Client IP — for share access, may be HOST(ip) as sent by DSM |
203.0.113.67 / LAPTOP-01(203.0.113.67) |
synology.service |
DSM service signed in to | DSM |
synology.method |
Authentication method | password / sso |
synology.protocol |
Share access protocol | CIFS(SMB3) / AFP |
synology.share |
Shared folder accessed | Projects |
synology.op |
SMB transfer-log operation | read / write / delete / rename |
synology.path |
File path (old -> new for renames) |
/Projects/report.xlsx |
synology.objtype |
Object type | File / Folder |
These are the UniFi Network, Protect, and OS CEF event IDs we have identified and mapped:
| ID | Event | Source | Note |
|---|---|---|---|
| 215 | UPS Battery Power In Use | Network | |
| 216 | UPS AC Power Restored | Network | |
| 400 | WiFi Client Connected | Network | |
| 401 | WiFi Client Disconnected | Network | |
| 402 | WiFi Client Roamed | Network | |
| 403 | Wired Client Connected | Network | |
| 404 | Wired Client Disconnected | Network | |
| 510 | Device Updated | Network | |
| 544 | Admin Accessed / Network Accessed | Network | |
| 545 | Config Created | Network | |
| 546 | Config Modified | Network | Requires debug syslog level |
| 549 | Config Removed | Network | |
| 563 | Poor AP Link Speed | Network | |
| 578 | Network Updated (software) | Network | |
| 1000 | Admin Accessed UniFi OS | OS | |
| 1100 | Application Update Available | OS | |
| 1102 | Application Updated | OS | |
| 2008 | Access | Protect | |
| 2103 | Device Connected | Protect | |
| 2108 | Update | Protect | |
| 2150 | Device Disconnected | Protect | |
| 2159 | Motion (camera) | Protect | |
| 2161 | Smart Detect: Zone / Loiter / Tamper / Audio | Protect | Tamper & audio have dedicated rules |
| 2201 | Sensor Motion | Protect | Also appears with category=iot |
| 2202 | Sensor Opened | Protect | Also appears with category=iot |
| 2203 | Sensor Closed | Protect | |
| 2308 | Admin Activity | Protect |
Note: Some Protect sensor events (2201, 2202) can appear with
UNIFIcategory=iotinstead ofUNIFIcategory=detection. Both are handled by the same rules.
Contributions welcome if you discover additional event IDs!
Use wazuh-logtest to validate decoders and rules:
# MikroTik DROP
echo 'Feb 25 10:30:00 MikroTik DROP : IPv4 FORWARD IN:ether1 OUT:bridge1 SRC:203.0.113.1 DST:192.0.2.100 PROTO:TCP SPT:12345 DPT:443 (ACK PSH)' | /var/ossec/bin/wazuh-logtest
# UniFi WiFi Connected
echo 'Feb 25 11:04:19 UDM-Pro-Max-AC CEF:0|Ubiquiti|UniFi Network|10.1.85|400|WiFi Client Connected|1|UNIFIcategory=Client Devices UNIFIconnectedToDeviceName=AP-Bureau UNIFIclientAlias=iPhone UNIFIclientIp=192.0.2.100 UNIFIclientMac=aa:bb:cc:dd:ee:01 UNIFIwifiName=MySSID UNIFIwifiBand=6e UNIFIWiFiRssi=-45 UNIFInetworkName=LAN' | /var/ossec/bin/wazuh-logtest
# UniFi OS Admin Accessed
echo 'Mar 13 16:00:00 UDM-Pro-Max-AC CEF:0|Ubiquiti|UniFi OS|5.0.16|1000|Admin Accessed UniFi OS|1|UNIFIhost=Host UNIFIadmin=Admin msg=Admin accessed the UniFi OS' | /var/ossec/bin/wazuh-logtest
# UniFi Config Modified (audit)
echo 'Mar 14 08:16:22 UDM-Pro-Max-AC CEF:0|Ubiquiti|UniFi Network|10.2.93|546|Config Modified|5|src=192.0.2.39 UNIFIcategory=Audit UNIFIsettingsChanges=debug: true UNIFIsettingsSection=System UNIFIadmin=Admin msg=Admin made a change in System settings' | /var/ossec/bin/wazuh-logtest
# FortiGate mDNS (should be suppressed - level 0)
echo 'date=2026-02-25 time=13:00:00 devname="fortigate" devid="FGT123" logid="0001000014" type="traffic" subtype="local" level="notice" srcip=fe80::1 dstip=ff02::fb action="deny" service="udp/5353"' | /var/ossec/bin/wazuh-logtest
# Jamf Pro: failed login (level 8)
echo '2026-06-29 08:28:01,037: username=jdoe, status=Failed Login, ipAddress=192.0.2.10, entryPoint=JSS' | /var/ossec/bin/wazuh-logtest
# Jamf Pro: admin account created (level 10)
echo '[jdoe (ID: 5)] [CREATE] [Account] [2026-06-29T10:02:00.791+0200]' | /var/ossec/bin/wazuh-logtest
# Jamf Pro: API client secret deleted (level 12, critical)
echo '[jdoe (ID: 5)] [DELETE] [API Client - Client Secret] [2026-06-29T10:02:00.772+0200]' | /var/ossec/bin/wazuh-logtest
# UDM system (debug mode, doubled hostname): IPS list update failure (level 5)
echo 'Jul 1 00:51:16 UDM-Pro-Max-AC UDM-Pro-Max-AC ips-update.sh[2238648]: File checksum failed for /run/ips/rules/tor.list.gz.' | /var/ossec/bin/wazuh-logtest
# UDM system: sudo command trail (level 3)
echo 'Jul 1 18:55:15 UDM-Pro-Max-AC UDM-Pro-Max-AC sudo[21901]: uid : PWD=/data ; USER=root ; COMMAND=/usr/bin/id' | /var/ossec/bin/wazuh-logtest
# UniFi AP kernel: 802.11 authentication frame (level 3)
echo 'Jul 1 10:06:47 AP-Office 9c05d6000001,U7-Pro-Wall-8.7.9+19401: kernel: wlan: [0:I:ANY] [UNSPECIFIED] vap-4(wifi2ap8): [aa:bb:cc:dd:ee:01]recv auth frame with algorithm 2 seq 1 rssi:52 minrssi:0' | /var/ossec/bin/wazuh-logtest
# UPS Tower (no syslog timestamp): controller inform failure (level 4)
echo 'UPS-Office 847848000001,UPS TOWER-1.5.0.378: MCA: HTTP Status = 503, content_length = 0' | /var/ossec/bin/wazuh-logtest
# MikroTik DHCP binding (level 3)
echo 'Jul 1 00:33:20 router dhcp1 assigned 192.0.2.33 for 00:00:5E:00:53:05 laptop' | /var/ossec/bin/wazuh-logtest
# Scanner probe on a public endpoint (level 5)
echo '203.0.113.66 - - [01/Jul/2026:14:33:39 +0200] "" 400 0 "-" "-"' | /var/ossec/bin/wazuh-logtest
# ZFS zed error (level 9)
echo 'Jun 30 14:50:31 pve zed[851820]: eid=6743 class=io pool='"'"'rpool'"'"' size=8192 offset=839904612352 priority=0 err=6 flags=0x200080 bookmark=259:128:0:11611' | /var/ossec/bin/wazuh-logtest
# Stormshield SNS: blocked traffic (level 3)
echo 'id=firewall time="2026-07-06 13:44:52" fw="SN000TESTFW00000" tz=+0200 startime="2026-07-06 13:44:52" pri=5 confid=01 slotlevel=2 ruleid=29 rulename="rule_2" srcif="Ethernet1" srcifname="lan" ipproto=udp dstif="Ethernet0" dstifname="wan" proto=dns_udp src=198.51.100.61 srcport=40638 srcportname=ephemeral_fw_udp srcmac=00:11:22:33:44:66 dst=198.51.100.1 dstport=53 dstportname=dns_udp dstname=resolver.example ipv=4 sent=0 rcvd=0 duration=0.00 action=block logtype="filter"' | /var/ossec/bin/wazuh-logtest
# Stormshield SNS: high-risk IPS alarm (level 10)
echo 'id=firewall time="2026-07-06 13:45:10" fw="SN000TESTFW00000" tz=+0200 startime="2026-07-06 13:45:09" pri=1 confid=01 slotlevel=2 ruleid=93 rulename="rule_1d" srcif="Ethernet1" srcifname="lan" ipproto=tcp proto=ssl src=198.51.100.20 srcport=64127 dst=203.0.113.61 dstport=443 ipv=4 action=block msg="Invalid SSL packet (Unknown SSL protocol)" class=protocol classification=0 alarmid=118 target=dst risk=10 logtype="alarm"' | /var/ossec/bin/wazuh-logtest
# Stormshield SNS: DHCPACK — who is connected (level 3, IP/MAC/hostname extracted)
echo 'id=firewall time="2026-07-06 13:45:51" fw="SN000TESTFW00000" tz=+0200 startime="2026-07-06 13:45:51" pri=6 service=dhcp msg="DHCPACK on 198.51.100.69 to 00:11:22:33:44:bb (Laptop-01) via igc6" logtype="system"' | /var/ossec/bin/wazuh-logtest- Wazuh 4.14.5 (3-VM cluster: server, dashboard, indexer)
- MikroTik CCR2004-1G-12S+2XS running RouterOS 7.x (plus CRS317/CRS305 switches)
- Ubiquiti UDM Pro Max / UDM SE running UniFi Network 10.4–10.5 and UniFi Protect 7.x — plus access points (U6-IW, U7-Pro, U7-Pro-Wall, U7-Pro-Max, U7-Mesh, UDB), switches (USW-Pro-Max-48, USW-Enterprise-24-PoE, USW-Flex 2.5G) and UPS Tower units on firmware 6.x–8.x
- Fortinet FortiGate 60E running FortiOS 7.x
- Proxmox VE + Proxmox Backup Server (journald collection through Wazuh agents)
- Syslog transport: UDP/514 via rsyslog
- Decoder/rule validation: a survey of 10.7M archived events over 48h with device syslog set to debug — coverage measured at 100% decoded (0 events without a decoder) after this ruleset
Versions move fast; these decoders are kept working across newer Wazuh, RouterOS and UniFi releases, so the exact versions above are a snapshot, not a hard requirement.
tools/sns-anonymize.py builds shareable Stormshield sample corpora from production logs without leaking personal data. Firewall logs contain named users, client IPs and MACs, hostnames, certificate DNs and browsing destinations — so the tool anonymises in situ (on the box where the logs live, read-only) and lets only pseudonymised output leave the environment. This is how the Stormshield samples in this repo were derived.
- Consistent, non-reversible pseudonyms — same real value → same placeholder within a run (structure and correlations preserved), but the mapping is order-based and never written to disk.
- IPs →
10.x(private) / TEST-NET (public), plus MACs, users →user0001, hostnames, rulenames, domains, interface zones and appliance serial → placeholders. - Hard-redacts the high-risk free text:
arg="...", certificate DNs anywhere (incl.remoteid=), and destination intelligence (geo / IP reputation / URL category) on bothsrc*anddst*sides. - Diversity sampling keeps a few examples per
(logtype, action, field-shape), so the corpus stays compact and representative. - Refuses to emit (exit 2) if any site marker survives — a belt-and-braces leak scan. Site-specific markers (org names, serials, internal subnets) live in an external
--deny-file, never in the script, so the tool itself is safe to publish (seetools/leak_markers.example.txt).
# build an anonymised corpus from the manager's archive (read-only)
sudo tail -n 500000 /var/ossec/logs/archives/archives.json \
| tools/sns-anonymize.py --deny-file leak_markers.txt > corpus_anon.txt
# re-scan an existing corpus without anonymising
tools/sns-anonymize.py --self-check --deny-file leak_markers.txt < corpus_anon.txtTwo tools ship in tools/:
sns-anonymize.py— Stormshield-tuned (knows the SNS field names); used to derive the Stormshield samples in this repo.log-anonymize.py— generic, works on any device/format: universal IPv4 / IPv6 / MAC / e-mail scrubbing, certificate-DN redaction,--redact-key NAMEto blank your format's user/host fields, optional--scrub-fqdn, and--fieldto pull a value out of JSON lines (e.g.full_log). This is the one to run before requesting a decoder for a device we don't cover yet.
- JAMF Protect & Security Cloud integration
- Fortinet VPN tunnel state monitoring — tunnel/phase-2 up/down with per-tunnel flapping correlation (rules 100440-100445)
- UniFi threat/IDS event decoding (rules 100358-100359, 100383-100384)
- UniFi AP direct logs — readable per-AP syslog (daemon+message), Wi-Fi client assoc/disassoc, fan/thermal telemetry (
unifi_ap.xml, rules 100368-100372); needs symantecdecoder_exclude(learning #15) - UniFi switch direct logs — readable per-switch syslog (
unifi_switch.xml, rule 100380; exclude BNC decoders if present) - UniFi debug-mode full coverage — UDM system syslog (doubled hostname), AP kernel events, USW-Flex/UPS Tower (no timestamp); measured 100% decode rate over a 48h/10.7M-event survey
- More UniFi hardware coverage — we are limited by the devices we own; log samples from other UniFi models (UXG, UCG, Protect NVRs, other switch/AP families) are very welcome
- Stormshield SNS decoding — all logtypes (traffic, SSL, IPS alarms, auth, SSL-VPN, IPsec VPN, admin audit, DHCP asset inventory, periodic stats), rules 100900-100919
- In-situ log anonymiser for building shareable sample corpora without leaking personal data (
tools/sns-anonymize.py) - Generic, format-agnostic log anonymiser for any platform (
tools/log-anonymize.py) — IPv4/IPv6/MAC/e-mail/DN scrubbing,--redact-key,--scrub-fqdn, deny-file - Community decoder-request workflow — submit anonymised logs via a GitHub issue template (
CONTRIBUTING.md) - Authentik IdP — structlog JSON via the Docker journald driver; audit trail with compliance tagging, brute-force + scanner correlations (rules 101000-101042)
- macOS Content Caching (Caching Service / AssetCache) monitoring — activity, cache efficiency and client-serving events from the built-in Caching Service, targeting the surface exposed from macOS 27; on hold until Apple's documentation settles
- CoreFilter (corefilter.app, by Octiplex) log integration — per-application firewall for macOS built on the native Network Extension / Endpoint Security frameworks, with identity-bound policies (Entra ID / Okta) and real-time audit. SIEM forwarding is a native product feature, and we are working in direct collaboration with CoreFilter's author so the decoder (per-app, per-user verdict events) is ready alongside the launch (Q2 2026)
- Propose the UniFi and Stormshield decoders upstream (Wazuh ruleset repository)
- Dashboard templates for OpenSearch/Kibana
PRs welcome — and you don't have to write a decoder yourself. Open a decoder request with a few anonymised log samples (run them through tools/log-anonymize.py first — raw logs contain personal data) and we'll build one. See CONTRIBUTING.md for both paths (requesting vs. contributing) and the anonymisation requirement.
MIT License — Use freely, attribution appreciated.
Built by Astier Consulting with assistance from Claude (Anthropic).