Example Scenario: A user logs in at 03:00 from a foreign IP address, deviating from their normal location and login schedule. Another user who typically logs in from Luxembourg starts showing login activity from multiple countries within a short time frame. These behaviors deviate from the user’s normal pattern and may indicate account compromise, credential theft, or malicious automation.
The objective is to detect such deviations in login behavior by modeling typical login patterns (time, location, frequency) per user and identifying outliers indicative of suspicious or unauthorized access.
Categorical Features: To improve detection accuracy, categorize anomalies by username or user ID. This ensures the model builds a separate baseline per user. Optional dimensions include user department, user role, or device ID if such metadata exists in the log source. Data should be sliced per user to model unique behavioral patterns.
Hybrid detection (AD + rules): Alongside anomaly detection, this scenario also enables rule-based (signature) detection in Wazuh. Rules cover high-confidence patterns such as failed-login bursts, impossible travel, and a correlation of both. This hybrid approach gives fast, deterministic alerts (rules) while Anomaly Detector could handle subtle, behavior-based deviations.
This scenario implements a signature-based detection pipeline for suspicious SSH logins. It combines local log enrichment with Wazuh decoders, rules, and active responses.
The primary log source for this scenario is the Linux authentication log: /var/log/auth.log. This file contains raw SSH authentication events produced by sshd (both successful and failed login attempts).
The enrichment step is performed by the radar-helper/radar-helper.py script located in the radar directory. The helper does:
/var/log/auth.log for new SSH authentication events./var/log/suspicious_login.logThis file is the Wazuh-monitored log source for the signature-based detection.
The enriched entries in /var/log/suspicious_login.log are parsed by Wazuh using decoders defined in /radar/scenarios/decoders/suspicious_login/0310-ssh.xml. At the end of this file, specific decoders are defined to handle enriched SSH events:
sshd-success-with-radarssh-failed-with-radarssh-invfailed-with-radarRole of these decoders: Parse the enriched log format produced by radar-helper.py. These extract both standard SSH fields (e.g., user, IP, port, result) and enrichment fields (ASN, country, geo-velocity, etc.).
The parsed events are then evaluated by rules defined in /radar/scenarios/rules/suspicious_login/, namely rules starting with 210XXX. These rules implement the signature-based detection logic, including:
Failed-login bursts
Rules that detect repeated failed SSH authentication attempts within a defined time window (e.g., multiple failures for the same user in a short period).
Impossible travel / abnormal geo-velocity
Rules that use the geo-velocity and country change information produced by radar-helper.py to detect login patterns that are not physically plausible (e.g., logins from distant countries within an unrealistically short time frame).
When rule conditions are satisfied, Wazuh generates an alert for the corresponding suspicious login behavior.
Once a rule from /radar/scenarios/rules/suspicious_login fires, Wazuh triggers the configured active response defined in /radar/scenarios/ossec/radar-suspicious-login-ossec-snippet.xml. This snippet binds the rule IDs starting with 210XXX to email active-response command.
The email active response uses /radar/scenarios/active_responses/radar_ar.py to:
/radar/.env file.sshd and written to /var/log/auth.log.radar-helper.py tails /var/log/auth.log, enriches SSH events with GeoIP data (ASN, country, geo-velocity), and writes them to /var/log/suspicious_login.log./var/log/suspicious_login.log.0310-ssh.xml (sshd-success-with-radar, ssh-failed-with-radar, ssh-invfailed-with-radar) parse enriched SSH events into structured fields./radar/scenarios/rules/suspicious_login/ apply signature-based logic to detect failed-login bursts and impossible travel patterns./radar/scenarios/ossec/radar-suspicious-login-ossec-snippet.xml are executed when rules fire, triggering email notifications via radar_ar.py script.This completes the signature-based suspicious login detection path for the suspicious_login scenario.
This section describes how to manually deploy the signature-based suspicious login scenario on Wazuh.
We distinguish between:
/var/log/auth.log)radar-helper/radar-helper.py, the databases can be found in /radar/geoip directory./radar/radar-helper.py to the target host into /opt/radar/radar-helper.py:
mkdir -p /opt/radar
mkdir -p /opt/radar/venv
chown user:user /opt/radar -R
chmod 755 /opt/radar
apt-get update
apt-get install -y \
python3 \
python3-venv \
python3-pip
python3 -m venv /opt/radar/venv
/opt/radar/venv/bin/pip install --upgrade pip
/opt/radar/venv/bin/pip install maxminddb
mkdir -p /usr/share/GeoIP
chown user:user /usr/share/GeoIP
chmod 755 /usr/share/GeoIP
cp ../geoip/GeoLite2-City.mmdb /usr/share/GeoIP/GeoLite2-City.mmdb
cp GeoLite2-ASN.mmdb /usr/share/GeoIP/GeoLite2-ASN.mmdb
radar-helper/radar-helper.service service configurations and run it as a service so it continuously:
cp ../radar-helper/radar-helper.service /etc/systemd/system/radar-helper.service
systemctl daemon-reload
systemctl enable radar-helper.service
systemctl start radar-helper.service
/var/log/suspicious_login.log:
nano /var/ossec/etc/ossec.conf
And paste the content of /radar/scenarios/ossec/radar-suspicious-login-ossec-snippet.xml into the end of file before the tag </ossec_config>
systemctl restart wazuh-agent
/radar/scenarios/decoders/suspicious_login/0310-ssh.xml to the manager and ensure that it has the needed permissions root:wazuh:
cp /radar/scenarios/decoders/suspicious_login/0310-ssh.xml /var/ossec/etc/decoders/0310-ssh.xml
chmod 640 /var/ossec/etc/decoders/0310-ssh.xml
chown root:wazuh /var/ossec/etc/decoders/0310-ssh.xml
0310 SSH decoders are excluded from the configurations of manager:
nano /var/ossec/etc/ossec.conf
And add this line into the ruleset tag:
```
3. Copy the content of `/radar/scenarios/rules/suspicious_login` into the `/var/ossec/etc/rules/`
4. Copy the `/radar/scenarios/active_responses/radar_ar.py` script into `/var/ossec/active-response/bin/` and ensure that it has proper permissions:
cp /radar/scenarios/active_responses/radar_ar.py /var/ossec/active-response/bin/radar_ar.py chmod 750 /var/ossec/active-response/bin/.py chown root:wazuh /var/ossec/active-response/bin/.py
5. Add the content of `/radar/scenarios/ossec/radar-suspicious-login-ossec-snippet.xml` inside `<ossec_config>` in `/var/ossec/etc/ossec.conf`.
6. Restart Wazuh manager:
/var/ossec/bin/wazuh-control restart
## Behavior-based Approach
> This approach should be tested in experimental environment. Improvements are needed for production environment usage.
### Data Preparation & Ingestion
#### Dataset Ingestion
To simulate “today” data and feed Wazuh’s AD plugin, we shift each file’s dates into the last three days. The script is located in suspicious_login/wazuh_ingest.py. Run it from suspicious_login:
```bash
python3 /radar/archives/suspicious_login/wazuh_ingest.py
today + offset@timestamp (ISO), event_hourwazuh-ad-suspicious-login-2025.06.07wazuh-ad-suspicious-login-*.@timestamp (date)User ID (string)Country, IP Address, event_hour, etc.docker run -d --name keycloak -p 8080:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=secret \
quay.io/keycloak/keycloak:24.0.1 start-dev
demowazuh (enable Service Accounts)realm-management: view-users, manage-usersexport KC_BASE_URL=http://127.0.0.1:8080
export KC_REALM=demo
export KC_ADMIN_USER=admin
export KC_ADMIN_PASSWORD=secret
Run the script to bulk create users:
python3 bulk_create_keycloak_users.py dataset/rba-dataset+0.csv
suspicious-login-detectorwazuh-ad-suspicious-login-*@timestamp5m (with 1m window delay)| Feature name | Method | Field | Notes |
|---|---|---|---|
login_count |
value_count |
User ID.keyword |
Counts total login events per user |
distinct_geo_country |
Custom expression | — | See “Workaround for Cardinality” below |
login_hour_cardinality |
Custom expression | — | See “Workaround for Cardinality” below |
OpenSearch’s anomaly-detection UI doesn’t directly expose a cardinality() aggregation in the simple “Field value” mode, so we inject our two cardinality features via custom JSON expressions:
{
"distinct_geo_country": {
"cardinality": {
"field": "Country.keyword"
}
}
}
{
"login_hour_cardinality": {
"cardinality": {
"field": "event_hour"
}
}
}
Each of these goes into the “Custom expression” section when you add a feature.
Under Categorical field, select the user identifier User ID.keyword.
This ensures each user gets its own statistical model, preventing Alice’s behavior from obscuring Bob’s anomalies.
Click Next to Review.
In suspicious-login-detector Anomaly overview, set up alert:
Suspicious-Login-DetectedWhen choosing thresholds for firing alerts, you must balance sensitivity (catching real threats) against precision (avoiding false positives). A balanced strategy is to require:
Starting here helps minimize alerts on spikes. Particularly important in high-cardinality, per-user detectors where data volume per user can vary widely. Tuning can then adjust these up or down based on observed false-positive rates during the analysis.
RADARMessage (must be JSON)
{
"monitor": {
"name": ""
},
"trigger": {
"name": ""
},
"entity": "",
"periodStart": "",
"periodEnd": ""
}
When the condition is met, this monitor will send structured JSON to the webhook.
ad_alerts_webhook.py)This webhook is a simple Flask application that receives the monitor’s payload and appends a single line to /var/log/ad_alerts.log. To deploy the webhook in the Wazuh manager:
Copy the ad_alerts_webhook.py file from this repository into the Wazuh manager to a custom wazuh_webhook directory.
Ensure execution permissions: chmod +x
python3 ad_alerts_webhook.py
/var/ossec/etc/ossec.conf needs to be configured:<localfile>
<log_format>syslog</log_format>
<location>/var/log/ad_alerts.log</location>
</localfile>
Add the content of the file /radar/archives/suspicious_login/local_decoder.xml in this repository into the file /var/ossec/etc/decoders/local_decoder.xml at the Wazuh manager.
Add the content of the file /radar/archives/suspicious_login/local_rules.xml in this repository into the file /var/ossec/etc/rules/local_rules.xml at the Wazuh manager.
var/ossec/bin/wazuh-control restart in Docker or systemctl restart wazuh-manager)./var/log/ad_alerts.log.ossec.conf on the manager, register and bind only the ad_context_susplog_active_response.py script. Script can be found in Active Response directory.<ossec_config>
<!-- 1) Command declaration -->
<command>
<name>ad_enrich_suspicious_login</name>
<executable>ad_context_susplog_active_response.py</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
<!-- 2) Active-response binding -->
<active-response>
<disabled>no</disabled>
<command>ad_enrich_suspicious_login</command>
<location>server</location>
<rules_id>100302</rules_id>
</active-response>
</ossec_config>
100302 fires, it will run ad_context_susplog_active_response.py.chmod 750 /var/ossec/active-response/bin/ad_context_susplog_active_response.py
chown root:wazuh /var/ossec/active-response/bin/ad_context_susplog_active_response.py
python3 -m pip install requests
jq for JSON parsing:sudo apt update
sudo apt install -y jq
sudo touch /var/ossec/logs/ad_pc_enriched.log
sudo chown root:wazuh /var/ossec/logs/ad_pc_enriched.log
sudo chmod 664 /var/ossec/logs/ad_pc_enriched.log
sudo cp write_contextual_logs_susplog_active_response.sh /var/ossec/active-response/bin/
sudo chown root:wazuh /var/ossec/active-response/bin/write_contextual_logs_susplog_active_response.sh
sudo chmod 750 /var/ossec/active-response/bin/write_contextual_logs_susplog_active_response.sh
ossec.conf:<command>
<name>write_contextual_logs_susplog_active_response.sh</name>
<executable>write_contextual_logs_susplog_active_response.sh</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
sudo cp disable_sso_user.py /var/ossec/active-response/bin/
sudo chmod 750 /var/ossec/active-response/bin/disable_sso_user.py
sudo chown root:wazuh /var/ossec/active-response/bin/disable_sso_user.py
export KC_BASE_URL=http://127.0.0.1:8080
export KC_REALM=demo
export KC_CLIENT_ID=wazuh
export KC_CLIENT_SECRET=REPLACE_ME
disable-sso-user command in ossec.conf:<command>
<name>disable_sso_user.py</name>
<executable>disable_sso_user.py</executable>
<timeout_allowed>no</timeout_allowed>
</command>
Edit /var/ossec/etc/local_internal_options.conf:
wazuh_command.remote_commands=1
sudo systemctl restart wazuh-agent
In a production environment, we recommend a two-tier response strategy for login anomalies:
anomaly_grade ≥ 0.7 and confidence ≥ 0.8write_contextual_logs_susplog_active_response.sh to store user behavior snapshots in /var/ossec/logs/ad_pc_enriched.logThis tier gives visibility to analysts while avoiding premature blocking.
anomaly_grade ≥ 0.9 and confidence ≥ 0.9disable-sso-user to lock the user in KeycloakUse case:
Impact: Prevents any future login attempts using the user’s SSO identity across all systems federated with Keycloak.
Recovery: Admins can re-enable accounts manually after validation.
anomaly_grade ≥ 0.9 and confidence ≥ 0.9firewall-drop via Wazuh Active ResponseUse case:
Impact: Temporarily drops packets from the attacker’s IP using IPTables (default expiration ~10 mins).
Recovery: IP block expires automatically unless re-enforced.
whitelist logic in scripts to skip known corporate VPN IPs or admin users./var/ossec/logs/active-responses.log for auditability.This two-tier model balances visibility with rapid containment, reserving automated blocks for the highest-confidence scenarios and minimizing collateral disruption.
Below is the end-to-end sequence when a suspicious login anomaly triggers Tier 2 containment:
detector_name: "Suspicious-Login-Detected"user_keyword: (e.g., "test_openbas")User ID: the SSO identity to be disabled (e.g., 4324475583306591935)period_start / period_end: ISO timestamps bounding the anomaly windowexecd
execd daemon triggers the relevant Active Response scripts using the wrapper JSON.write_contextual_logs_susplog_active_response.sh and disable-sso-user receive the alert data via stdin with "command":"add"./var/log/suspicious_login_enriched.log for forensic and audit purposes.IP Address.For each distinct source IP, the script triggers Wazuh’s firewall-drop Active Response:
{
"command": "firewall-drop",
"arguments": ["1.2.3.4"],
"alert": { "data": { "srcip": "1.2.3.4" } }
}
disable-sso-user script loads environment variables from /var/ossec/.kc_env.User ID.enabled=false to halt further SSO authentication./var/ossec/logs/active-responses.log on the manager, capturing success or failure per IP.disable-sso-user script logs user disablement actions to /var/ossec/logs/active-responses/disable_sso_user.log.For Contextual Enrichment and Threat Intelligence, corresponding Active Response can be triggered on every Anomaly detection. The instruction is in Automated OpenCTI Enrichment.
The dataset originates from Kaggle - RBA-dataset.
While this setup uses Keycloak as the default SSO provider for demonstration purposes, the detection logic is fully generalizable to other authentication systems such as:
/var/log/auth.log)The anomaly detection system is designed to be identity provider–agnostic, relying only on normalized login event data.
| Component | Adaptation Notes |
|---|---|
| Log Source | Replace or augment Keycloak logs with logs from SSH, Azure AD, Okta, etc. |
| Ingest Format | Normalize logs to include fields like User ID, timestamp, Country, IP. |
| Anomaly Features | Maintain behavior-based indicators (geo changes, login hours, frequency). |
| Categorical Field | Always slice data per user (e.g., User ID.keyword, username.keyword). |
| Common Field | SSH | Azure AD / Okta |
|---|---|---|
| User ID | username |
userPrincipalName |
| Timestamp | timestamp |
createdDateTime |
| IP Address | src_ip |
ipAddress |
| Country | Derived from src_ip |
Derived from ipAddress |
| Login Time | Derived from timestamp | Derived from timestamp |
Use Logstash, Filebeat modules, or ingestion scripts to transform and map fields before indexing to OpenSearch.
In the case of suspicious login activity,such as a user accessing the system at 03:00 from a foreign IP or from multiple countries in a short timeframe, the associated risk is again modelled using:
R = C × I
Here, C represents the confidence score output by the anomaly detection system, reflecting the likelihood that the login behavior deviates from established user-specific baselines. This use of model confidence as a proxy for likelihood is standard in behavior-based intrusion detection systems. The impact score I is derived from CVSS, adapted to represent behavioral anomalies such as unauthorized or suspicious access events.
In this context, potential consequences include moderate confidentiality loss (e.g., exposure of personal or customer data), but typically no direct integrity or availability compromise, assuming the attacker has not escalated privileges or performed destructive actions.
According to our tiered thresholding automated response mechanism, we set:
This framework ensures that anomalous login behavior is escalated only when both the confidence is high and the potential business impact is non-trivial. It also allows for consistent application of response policies across users and login patterns, making the model robust for account compromise detection.