Network alert first, then a login

Which sources triggered a Suricata alert and later logged in to the honeypot?

T1595 T1110 T1078

Two sensors, one question. Suricata sees packets on the whole host; Cowrie sees what happens inside a shell session. Joining them on the source address answers whether the noisy scanner from the IDS is the same actor that later got in, which is the difference between "background scanning" and "an intrusion that started with reconnaissance".

Order matters: require the first alert to come before the first login. Time zones matter too: both sources are stored in UTC here; mixing a local-time IDS log with a UTC application log is a classic way to get this join wrong.

Queries

Splunk and Microsoft Sentinel returned 10 rows; Wazuh returned 9 rows. Why they differ is explained below.

Splunk SPL

verified 2026-11-11 data · 10 rows · Splunk 10.4.5
index=honeypot_demo (sourcetype=suricata:alert) OR (sourcetype=cowrie:json eventid="cowrie.login.success")
| stats count(eval(sourcetype="suricata:alert")) AS alerts
        count(eval(sourcetype="cowrie:json")) AS logins
        min(eval(if(sourcetype="suricata:alert", _time, null()))) AS first_alert
        min(eval(if(sourcetype="cowrie:json", _time, null()))) AS first_login
        by src_ip
| where alerts > 0 AND logins > 0 AND first_alert < first_login
| eval first_alert=strftime(first_alert, "%F %T"), first_login=strftime(first_login, "%F %T")
| sort - alerts

Microsoft Sentinel KQL

verified 2026-11-11 data · 10 rows · Microsoft Sentinel (Log Analytics)
let alerts = HP_Suricata
    | summarize alerts = count(), first_alert = min(TimeGenerated) by src_ip;
HP_Cowrie
| where eventid == "cowrie.login.success"
| summarize logins = count(), first_login = min(TimeGenerated) by src_ip
| join kind=inner alerts on src_ip
| where first_alert < first_login
| project src_ip, alerts, logins, first_alert, first_login
| sort by alerts desc

Wazuh DQL + OpenSearch query

verified 2026-11-11 data · 9 rows · Wazuh 4.14.9

Discover filter (DQL)

rule.groups:suricata or rule.id:100101

Aggregation (OpenSearch query DSL, Dev Tools or API)

{
  "query": { "bool": { "should": [ { "term": { "rule.groups": "suricata" } },
                                   { "term": { "rule.id": "100101" } } ], "minimum_should_match": 1 } },
  "aggs": {
    "src_ip": { "terms": { "field": "data.src_ip", "size": 10000 },
      "aggs": {
        "alerts": { "filter": { "term": { "rule.groups": "suricata" } },
                    "aggs": { "first": { "min": { "field": "timestamp" } } } },
        "logins": { "filter": { "term": { "rule.id": "100101" } },
                    "aggs": { "first": { "min": { "field": "timestamp" } } } },
        "alert_before_login": { "bucket_selector": {
          "buckets_path": { "a": "alerts._count", "l": "logins._count", "fa": "alerts>first", "fl": "logins>first" },
          "script": "params.a > 0 && params.l > 0 && params.fa < params.fl" } }
      } }
  }
}

Why Wazuh can return one row fewer. Wazuh stores an alert's time in milliseconds; Cowrie, Suricata, Splunk and Sentinel keep microseconds. On 2026-11-11 one source raised its Suricata alert 0.7 ms before it logged in. Splunk and Sentinel see "alert, then login"; in Wazuh both events carry the same millisecond, first_alert < first_login is false, and the source drops out. Changing < to <= would bring it back, but would also count sources whose login really came first within the same millisecond. Neither is wrong; it is a precision limit worth knowing before you build a sequence detection on Wazuh timestamps.