Revision 1.1 — September 22, 2026
This hunt determines whether the Keycloak reset-credentials authentication bypass (CVE-2026-18963, CVSS v3.1 9.1) was used against the organization, either through its ABB dynovaPRO energy management accounts before ABB's cloud fix on 2026-08-27 12:00 (CEST), or through any self-hosted Keycloak or Red Hat build of Keycloak server the organization operates. The objective is to identify accounts whose passwords were reset without the owner's involvement, sessions established with those credentials, and any follow-on configuration change or control command issued to EV chargers, battery energy storage, or photovoltaic assets supervised by dynovaPRO.
Environment in scope: internet-facing and internal Keycloak 26.x servers (bare metal, VM, container, Kubernetes) and their reverse proxies; Active Directory where Keycloak federates users over LDAP with password writeback; corporate email that receives dynovaPRO notification mail; endpoints used by facility, fleet and energy managers to reach dynovaPRO; and site OT networks hosting chargers, storage and PV inverters with their site controllers or gateways. Affected upstream releases are 26.0.0–26.4.14, 26.5.0–26.6.5 and 26.7.0–26.7.1; releases before 26.0.0 are outside the published affected range.
Time window: 2026-08-17 (earliest public disclosure date cited; the CVE record lists 2026-08-18) to present for self-hosted Keycloak; 2026-08-17 to 2026-08-27 12:00 CEST for dynovaPRO account resets, extended to present for sessions and control activity attributable to any account reset in that window. Where logging retention allows, extend back 30 days before disclosure to establish a password-reset baseline.
Exploit mechanics that drive the detections (from the public proof of concept and the upstream fix): the attacker opens the reset-credentials flow, posts tryAnotherWay=on to corrupt the flow's session state, submits the victim's username (this sends the victim a genuine reset email), re-enters the flow, and is advanced to the UPDATE_PASSWORD required action without ever presenting the emailed action token. The observable difference from a legitimate reset is therefore a password update with no preceding EXECUTE_ACTION_TOKEN event or /login-actions/action-token request in the same authentication session.
External attack-surface hypothesis H-1 is included because Keycloak is a web-facing identity service that is routinely published to the internet; execute it first, as its output scopes H-2. CVE-2026-18963 was not in the CISA KEV catalog as of the 2026-09-21 catalog release.
Hypothesis 1: an external actor can reach an internet-facing instance of Keycloak or Red Hat build of Keycloak operated by the organization, observable as an exposed Keycloak login or admin service in passive internet-scan indices. Execute FIRST; its output scopes the internal hypotheses.
MITRE ATT&CK: Reconnaissance | T1595 — Active Scanning | defender-side passive discovery of the Keycloak endpoints an actor would enumerate before sending crafted reset-credentials requests. Initial Access | T1190 — Exploit Public-Facing Application | CVE-2026-18963 is exploitable by any unauthenticated party that can reach the realm login endpoints. ICS | T0883 — Internet Accessible Device | applies where the Keycloak realm fronts an OT or DER management application.
Collection Queries — passive external indices (STRICTLY passive; never probe target infrastructure; API keys by env-var name only: SHODAN_API_KEY, CENSYS_API_SECRET, CENSYS_ORG_ID, NETLAS_API_KEY):
// Shodan — unscoped first, to prove each filter matches the Keycloak population
http.title:"Keycloak"
http.title:"Welcome to Keycloak"
http.html:"/realms/" http.html:"login-actions"
// Shodan — scoped; run each ownership anchor as its own query (never AND two anchors together)
http.html:"/realms/" net:<your_public_CIDR>
http.html:"/realms/" org:"<Your Org Name>"
http.title:"Keycloak" asn:AS<your_ASN>
// Censys CenQL (current form — host.services.*)
host.services.endpoints.http.html_title: "Keycloak"
host.services.software.product: "keycloak"
host.services.software.product: "keycloak" and (host.autonomous_system.asn = <your_ASN> or host.ip: "<your_public_CIDR>")
// Netlas (tie-breaker)
http.title:"Keycloak"
host:<your_public_CIDR> AND http.title:"Keycloak"
→ Cross-reference every confirmed-owned instance against the CISA Known Exploited Vulnerabilities catalog (https://www.cisa.gov/known-exploited-vulnerabilities-catalog) for CVE-2026-18963 and record its advertised version, if any, against the fixed releases (Keycloak 26.4.15, 26.6.6, 26.7.2, or the fixed Red Hat build of Keycloak errata).
Analysis Queries:
Hypothesis 2: an unauthenticated actor has reset user passwords on a self-hosted Keycloak server through the reset-credentials flow without the email verification step, observable as SEND_RESET_PASSWORD followed by UPDATE_PASSWORD/UPDATE_CREDENTIAL for the same user with no intervening EXECUTE_ACTION_TOKEN event, and as reset-credentials POSTs carrying tryAnotherWay=on, in Keycloak event logs, reverse-proxy access logs and LDAP writeback events in Active Directory.
MITRE ATT&CK: Initial Access | T1190 — Exploit Public-Facing Application | crafted requests to /realms/<realm>/login-actions/reset-credentials bypass email verification. Persistence | T1098 — Account Manipulation | the attacker sets a new password on the victim account. Defense Evasion | T1078 — Valid Accounts | subsequent sign-ins use the attacker-set credential and appear legitimate.
Collection Queries:
CrowdStrike Falcon LogScale (CQL) — inventory every host running a Keycloak server and extract the distribution version where the install path carries it (the official container image installs to an unversioned /opt/keycloak, so an empty kcVersion means "check the image tag", not "unaffected"):
#event_simpleName=ProcessRollup2
| CommandLine=/keycloak|kc\.sh|io\.quarkus\.bootstrap\.runner/i
| regex("keycloak-(?<kcVersion>\d+\.\d+\.\d+)", field=CommandLine, strict=false)
| groupBy([ComputerName, ImageFileName, kcVersion], function=[count(as=cnt), max(@timestamp, as=lastSeen)], limit=max)
| sort(lastSeen, order=desc, limit=max)
CrowdStrike Falcon LogScale (CQL) — inbound connections from public addresses to the default Keycloak HTTP/HTTPS listener ports:
#event_simpleName=NetworkReceiveAcceptIP4
| in(LocalPort, values=["8080", "8443"])
| !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"])
| groupBy([ComputerName, LocalPort, RemoteAddressIP4], function=[count(as=cnt), min(@timestamp, as=firstSeen)], limit=max)
| sort(cnt, order=desc, limit=max)
Keycloak server-side logging prerequisites (enable before the hunt if not already on): Realm settings → Events → User events settings → Save events ON with SEND_RESET_PASSWORD, RESET_PASSWORD, EXECUTE_ACTION_TOKEN, UPDATE_PASSWORD, UPDATE_CREDENTIAL, LOGIN and LOGIN_ERROR selected; Admin events ON with representation. The jboss-logging listener writes SUCCESS events at DEBUG by default, so raise it to INFO or they never reach stdout; for the Datadog Keycloak integration, also emit JSON over syslog. Keycloak 26 logs both the deprecated UPDATE_PASSWORD and the new UPDATE_CREDENTIAL (credential_type=password) for the same change; hunt on both. Saved events in the database are retained only per the realm's expiration setting — export them before they age out.
bin/kc.sh start --http-access-log-enabled=true --spi-events-listener-jboss-logging-success-level=info --spi-events-listener-jboss-logging-error-level=warn
bin/kc.sh start --log=console,syslog --log-syslog-output=json --log-syslog-endpoint=<datadog_agent_host>:514
BPF / tcpdump — rolling capture on the reverse-proxy-to-Keycloak backend segment where TLS is terminated at the proxy (plaintext HTTP on the backend):
tcpdump -i eth0 -s 0 -w /var/captures/kc_backend_%Y%m%d_%H%M%S.pcap -G 3600 -C 500 'tcp port 8080 and host 10.20.30.40'
tcpdump -i eth0 -s 0 -A 'tcp port 8080 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354)' | grep -i "tryAnotherWay"
Datadog Log Search (Keycloak integration; source:keycloak is the integration's fixed source value — attribute names below depend on your pipeline, confirm them in the Log Explorer facet panel):
// time range: 2026-08-17T00:00Z to current
source:keycloak ("SEND_RESET_PASSWORD" OR "EXECUTE_ACTION_TOKEN" OR "UPDATE_PASSWORD" OR "UPDATE_CREDENTIAL")
// time range: 2026-08-17T00:00Z to current — Kubernetes fallback where no keycloak source pipeline exists
source:kubernetes @kubernetes.container_name:keycloak message:"reset-credentials"
// time range: 2026-08-17T00:00Z to current — Windows-hosted Keycloak fallback
source:windows service:keycloak "SEND_RESET_PASSWORD"
Datadog Live Process Monitoring (Infrastructure > Processes — NOT a log source):
command:java user:keycloak
// free text fallback: kc.sh
Data source gap note: if Keycloak event logging was not enabled at INFO before the hunt, reset events for the exposure window may exist only in the realm's saved-events store (Realm settings → Events → User events) or not at all; fall back to reverse-proxy access logs (source:nginx or source:haproxy) and Active Directory LDAP writeback events below, and record the gap in the report.
Windows Event IDs to collect (domain controllers, where Keycloak LDAP user federation has password writeback):
$start = [datetime]"2026-08-17"
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4724,4723,4738; StartTime=$start} |
Select-Object TimeCreated, Id, @{n='Subject';e={$_.Properties[4].Value}}, @{n='Target';e={$_.Properties[0].Value}} |
Export-Csv -NoTypeInformation .\kc_ldap_pwd_resets.csv
YARA file-system scan — staged exploit scripts on Keycloak hosts, jump hosts and analyst workstations:
yara -r rules/keycloak_resetcreds.yar /home /tmp /var/tmp /opt >> keycloak_resetcreds_hits.txt
Analysis Queries:
CrowdStrike Falcon LogScale (CQL) — where Keycloak event logs are ingested into Falcon Next-Gen SIEM, flag users whose password changed after a reset request with no EXECUTE_ACTION_TOKEN event in between (the emailed link was never used):
/type="?(SEND_RESET_PASSWORD|EXECUTE_ACTION_TOKEN|UPDATE_PASSWORD|UPDATE_CREDENTIAL)"?/i
| regex("type=\"?(?<kcType>[A-Z_]+)\"?", strict=true)
| regex("userId=\"?(?<kcUser>[0-9a-f-]{36})", strict=false)
| regex("ipAddress=\"?(?<kcIp>[0-9a-fA-F.:]+)", strict=false)
| case { kcType="SEND_RESET_PASSWORD" | sendTs := @timestamp; kcType="EXECUTE_ACTION_TOKEN" | tokenTs := @timestamp; in(kcType, values=["UPDATE_PASSWORD", "UPDATE_CREDENTIAL"]) | chgTs := @timestamp; * }
| groupBy([kcUser], function=[min(sendTs, as=firstSend), max(tokenTs, as=lastToken), max(chgTs, as=lastChange), collect([kcIp])], limit=max)
| test(lastChange > firstSend)
| case { lastToken!=* | tokenUsed := "no"; test(lastToken < firstSend) | tokenUsed := "no"; * | tokenUsed := "yes" }
| tokenUsed="no"
| deltaSec := (lastChange - firstSend) / 1000
| sort(deltaSec, order=asc, limit=max)
CrowdStrike Falcon LogScale (CQL) — rarest source IPs completing password changes (one IP resetting many users is the campaign signature):
/type="?(UPDATE_PASSWORD|UPDATE_CREDENTIAL)"?/i
| regex("userId=\"?(?<kcUser>[0-9a-f-]{36})", strict=false)
| regex("ipAddress=\"?(?<kcIp>[0-9a-fA-F.:]+)", strict=false)
| groupBy([kcIp], function=[count(kcUser, distinct=true, as=usersReset), count(as=events)], limit=max)
| sort(usersReset, order=desc, limit=max)
Wireshark display filters (backend PCAP):
http.request.method == "POST" && http.request.uri contains "/login-actions/reset-credentials" && http.file_data contains "tryAnotherWay=on"
http.request.uri contains "/login-actions/required-action" && http.request.uri contains "execution=UPDATE_PASSWORD"
http.request.uri contains "/login-actions/action-token"
http.user_agent contains "cve-2026-18963"
tshark -r kc_backend.pcap -Y 'http.request.uri contains "login-actions"' -T fields -e frame.time -e ip.src -e http.x_forwarded_for -e http.user_agent -e http.request.method -e http.request.uri
Datadog Log Analytics:
// time range: 2026-08-17T00:00Z to current
source:keycloak ("UPDATE_PASSWORD" OR "UPDATE_CREDENTIAL")
// Use Table view; group by @userId, @ipAddress; time range 2026-08-17 to current
// Use Timeseries view; group by @ipAddress; 1-minute rollup to surface reset bursts
// time range: 2026-08-17T00:00Z to current — proxy-side: password set via required-action with no action-token GET from the same client
source:nginx @http.url_details.path:*required-action* @http.method:POST
// Use Top List view; group by @network.client.ip; compare against source:nginx @http.url_details.path:*action-token*
Datadog Audit Trail (Datadog itself federated to Keycloak via SAML — a hijacked Keycloak account signs straight into Datadog):
// time range: 2026-08-17T00:00Z to current
source:datadog @evt.name:Authentication
// Use Table view; group by @usr.email, @network.client.ip; flag first-seen IPs for reset users
Datadog CloudTrail (AWS IAM federated to Keycloak via SAML or OIDC):
// time range: 2026-08-17T00:00Z to current
source:cloudtrail @evt.name:(AssumeRoleWithSAML OR AssumeRoleWithWebIdentity) -@network.client.ip:10.* -@network.client.ip:172.16.* -@network.client.ip:192.168.*
// Use Table view; group by @network.client.ip, @userIdentity.arn
Datadog Monitor:
Type: Log Alert
Query: source:keycloak "UPDATE_CREDENTIAL"
Evaluation window: last 5 minutes
Alert condition: count > 3 by @ipAddress
Message: "ALERT: Keycloak password updates burst from one IP — possible CVE-2026-18963 exploitation @slack-soc-alerts"
Prerequisites: Keycloak user events enabled; jboss-logging success level raised to INFO; Keycloak logs sent as JSON over syslog to the Datadog Agent (source:keycloak) with the client IP parsed
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors
Windows Event Log PowerShell analysis — LDAP writeback resets performed by the Keycloak bind account, followed by a logon within one hour:
$kcBind = "svc-keycloak-ldap"
$resets = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4724; StartTime=[datetime]"2026-08-17"} |
Where-Object { $_.Properties[4].Value -like "*$kcBind*" }
foreach ($r in $resets) {
$target = $r.Properties[0].Value
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624; StartTime=$r.TimeCreated; EndTime=$r.TimeCreated.AddHours(1)} |
Where-Object { $_.Properties[5].Value -eq $target } |
Select-Object @{n='ResetTime';e={$r.TimeCreated}}, TimeCreated, @{n='User';e={$target}}, @{n='SrcIP';e={$_.Properties[18].Value}}
} | Export-Csv -NoTypeInformation .\kc_reset_then_logon.csv
YARA memory scan — scripting interpreters holding a crafted reset-credentials request (Falcon RTR can run the same command remotely via runscript on an isolated host):
Get-Process python*,node*,ruby*,perl* -ErrorAction SilentlyContinue | ForEach-Object { yara rules/keycloak_resetcreds_mem.yar $_.Id } >> keycloak_mem_hits.txt
pgrep -f 'python|node|ruby|perl' | xargs -I{} yara rules/keycloak_resetcreds_mem.yar {} >> keycloak_mem_hits.txt
Hypothesis 3: an unauthenticated actor has taken over ABB dynovaPRO user accounts belonging to the organization before the 2026-08-27 cloud fix, observable as unsolicited dynovaPRO password reset emails in mail logs followed by user-reported lockouts, ABB account notifications, or new sign-in sources in dynovaPRO user activity.
MITRE ATT&CK: Initial Access | T1078.004 — Valid Accounts: Cloud Accounts | the attacker signs in to the dynovaPRO SaaS tenant with a credential it set itself. Persistence | T1098 — Account Manipulation | the victim's password is replaced without their knowledge.
Collection Queries:
Exchange Online message trace — identify the dynovaPRO notification sender from one known-good reset mail first. Get-MessageTraceV2 returns at most 10 days per query and 5000 rows, and reaches back 90 days, so the window is split in two and should be run before 2026-11-15:
Connect-ExchangeOnline
$sender = "<dynovaPRO_notification_sender>"
$w1 = Get-MessageTraceV2 -SenderAddress $sender -StartDate "2026-08-17" -EndDate "2026-08-24" -ResultSize 5000
$w2 = Get-MessageTraceV2 -SenderAddress $sender -StartDate "2026-08-24" -EndDate "2026-08-28" -ResultSize 5000
$w1 + $w2 | Select-Object Received, RecipientAddress, Subject, Status, MessageId |
Export-Csv -NoTypeInformation .\dynovapro_reset_mails.csv
Datadog Log Search (email gateway or Microsoft 365 logs forwarded to Datadog; adjust source to your integration's tag):
// time range: 2026-08-17T00:00Z to 2026-08-28T00:00Z
source:microsoft-365 "dynovaPRO" "password"
CrowdStrike Falcon LogScale (CQL) — endpoints that resolve dynovaPRO hosts, to build the population of legitimate users (ABB does not publish the dynovaPRO FQDN; confirm it from a user's browser and tighten the pattern):
#event_simpleName=DnsRequest
| DomainName=/dynovapro/i
| groupBy([ComputerName, DomainName], function=[count(as=cnt), min(@timestamp, as=firstSeen), max(@timestamp, as=lastSeen)], limit=max)
| sort(firstSeen, order=desc, limit=max)
BPF / tcpdump — site-gateway uplink capture to preserve cloud-to-site sessions for H-4 (metadata only; payload is TLS):
tcpdump -i eth1 -s 128 -w /var/captures/der_uplink_%Y%m%d_%H.pcap -G 3600 'tcp port 443 and host 192.168.50.10'
Windows Event IDs to collect: none are generated for a SaaS password reset; collect 4624/4625 on the manager workstations only if a reset user also reused the same password for a domain account (credential reuse check).
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624,4625; StartTime=[datetime]"2026-08-17"} | Export-Csv -NoTypeInformation .\mgr_logons.csv
Analysis Queries:
CrowdStrike Falcon LogScale (CQL) — hosts with a first-ever dynovaPRO resolution in the window (a new machine using a manager's account); 1786924800000 is 2026-08-17T00:00Z:
#event_simpleName=DnsRequest
| DomainName=/dynovapro/i
| groupBy([ComputerName], function=min(@timestamp, as=firstSeen), limit=max)
| test(firstSeen > 1786924800000)
Wireshark display filters (site uplink PCAP — TLS metadata):
tls.handshake.extensions_server_name contains "dynovapro"
tshark -r der_uplink.pcap -Y 'tls.handshake.type == 1' -T fields -e frame.time -e ip.dst -e tls.handshake.extensions_server_name
Datadog Log Analytics:
// time range: 2026-08-17T00:00Z to 2026-08-28T00:00Z
source:microsoft-365 "dynovaPRO" "password"
// Use Table view; group by @recipient; time range 2026-08-17 to 2026-08-28
Datadog Audit Trail: not applicable to the dynovaPRO SaaS itself; use the Hypothesis 2 Audit Trail query where Keycloak also backs Datadog SSO.
Datadog Monitor:
Type: Log Alert
Query: source:microsoft-365 "dynovaPRO" "password"
Evaluation window: last 15 minutes
Alert condition: count > 0
Message: "ALERT: dynovaPRO password reset email delivered — confirm the user requested it @slack-soc-alerts"
Prerequisites: Microsoft 365 or email gateway message logs forwarded to Datadog with subject and recipient parsed
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors
Windows Event Log PowerShell analysis — failed-then-successful domain logons for reset users (password reuse by the attacker):
$users = Import-Csv .\dynovapro_reset_mails.csv | ForEach-Object { ($_.RecipientAddress -split '@')[0] }
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625,4624; StartTime=[datetime]"2026-08-17"} |
Where-Object { $users -contains $_.Properties[5].Value } |
Select-Object TimeCreated, Id, @{n='User';e={$_.Properties[5].Value}}, @{n='SrcIP';e={$_.Properties[18].Value}} |
Export-Csv -NoTypeInformation .\reset_users_domain_logons.csv
Hypothesis 4: an actor holding a hijacked dynovaPRO account has issued unauthorized configuration changes or control commands to EV chargers, battery energy storage or PV inverters, observable as setpoint, schedule or mode changes originating from the cloud-connected site controller in OT monitoring platforms, site controller logs and SNMP data.
MITRE ATT&CK: ICS Initial Access | T0859 — Valid Accounts | control arrives through an authenticated cloud session. ICS Impair Process Control | T0836 — Modify Parameter | charging limits, dispatch and inverter setpoints altered. ICS Impair Process Control | T0855 — Unauthorized Command Message | start/stop or mode commands relayed from cloud to device.
Collection Queries:
OT Data Collection: Claroty xDome — `Network → Communication → Communication Analysis` → Side A: the DER site controller/gateway asset · Communication: `Protocol = Modbus`, `Port = 502` · Side B: `Purdue Level = 1` · Time Frame `Past Month`; then `Alerts & Threats → OT Activity` filtered to write and mode-change events for the same assets, exported CSV. API: `POST /api/v1/ot_activity_events/`.
OT Data Collection: Claroty CTD — on-premises: Assets filtered to the DER site controller and inverter/BMS assets → Events export for the window; retrieve PCAP from the CTD appliance while it is still within local retention.
OT Data Collection: Dragos Platform — Query the Industrial Protocols QFD for Modbus writes from the site controller to battery and inverter assets between 2026-08-17 and present; in the `Communications Hub`, filter source = site controller, protocol = Modbus, and export PCAP for the window from SiteStore.
OT Data Collection: Nozomi Guardian — run the N2QL queries below via `GET /api/open/query/do?query=<N2QL>` and review the `Variable view` for setpoint excursions.
links | where protocol == "modbus" | where last_activity_time > days_ago(40)
variables | where name ~= "setpoint" and last_change > days_ago(40)
OT Data Collection: Armis Centrix — ASQ for cloud sessions from site assets to dynovaPRO and for Modbus sessions into storage and inverter devices:
in:ipConnections
serverPort:443
endpointB:(networkLocation:External)
decisionData:(host:(matches(".*dynovapro.*")))
in:ipConnections
serverPort:502
endpointB:(role:Server)
OT Data Collection: Tenable OT — `Events`: filter `Event Category` = Configuration Events with event types `Change in state` across the window for the site controller and inverter assets; API `POST /graphql` `eventAggregations(...)`.
OT Data Collection: Forescout eyeInspect — `Command Center → Alerts` for the window with `l7_proto` = Modbus and `src_ip` = the site controller; retrieve the PCAP for each `alert_id`.
curl -sS -u "$EYEINSPECT_USER:$EYEINSPECT_PASS" "https://<cc_host>/api/v1/alerts?start_timestamp=<epoch_ms>&end_timestamp=<epoch_ms>&l7_proto=modbus&limit=500"
SNMP polling — site switch port facing the DER site controller and the controller itself:
snmpwalk -v2c -c <community> <switch_ip> IF-MIB::ifTable
snmpget -v2c -c <community> <switch_ip> IF-MIB::ifHCInOctets.<ifIndex> IF-MIB::ifHCOutOctets.<ifIndex> IF-MIB::ifInErrors.<ifIndex> IF-MIB::ifOutErrors.<ifIndex>
snmpwalk -v2c -c <community> <device_ip> system
snmpwalk -v3 -l authPriv -u <user> -a SHA-256 -A <authpass> -x AES-256 -X <privpass> <device_ip> SNMPv2-MIB::sysUpTime.0
Repeat the ifHC counter poll every 60 seconds and diff; any v1/v2c community access to DER assets is itself a finding. From the trap receiver, pull coldStart (1.3.6.1.6.3.1.1.5.1), warmStart (1.3.6.1.6.3.1.1.5.2), linkDown/linkUp (1.3.6.1.6.3.1.1.5.3 / .4) and authenticationFailure (1.3.6.1.6.3.1.1.5.5) traps from DER device ranges for the window.
BPF / tcpdump — Modbus TCP writes on the site OT segment:
tcpdump -i eth2 -s 0 -w /var/captures/der_modbus_%Y%m%d_%H.pcap -G 3600 'tcp port 502'
Historian / SCADA correlation: export battery state-of-charge, charge/discharge power, charger session and inverter active/reactive power setpoints for the window; mark every step change with no matching operator action, schedule or ABB service ticket.
CrowdStrike Falcon LogScale (CQL) — Windows or Linux site controllers and HMIs with a Falcon sensor: outbound sessions to dynovaPRO hosts, by process:
#event_simpleName=ProcessRollup2
| join(