The Okta Identity Defense team recently announced the launch of their Customer Detection Catalog, a repository of detection queries designed to help their customers proactively identify and respond to potential security threats. If you haven’t done so already, I encourage you to explore this valuable resource and think about how you can apply it to your own detection and threat hunting scenarios to defend your organization.
As someone who has spent a considerable amount of time doing threat hunting and detection engineering based on Okta’s system log events, one detection in particular piqued my interest, as it would have come in useful during an incident I worked on at a previous company. I was inspired to figure out how to implement this detection in Google Security Operations (SecOps).
In today’s installment of this blog series, we’re going to walk through a practical example of how to leverage data tables when building detections in Google SecOps. Specifically, we’re going to look at how to manage the contents of a data table via Google SecOps’ automation capabilities and utilize that data in a detection.
The detection I mentioned earlier triggers when the same device ID is registered to multiple Okta users. This behavior may indicate that an adversary has compromised multiple user accounts and is registering their device as an MFA factor to maintain access. To implement this detection use case in Google SecOps, I came up with the following design:
Step one is to create a new data table in Google SecOps that will store the metadata about registered devices and associated user accounts. I name the data table, “okta_user_device_ids” and specify the columns, device_id, user_id, and username. The number of columns can certainly be expanded to include more information, but for today’s example, this is enough to get us where we need to be.
To implement this detection use case, I’m going to retrieve device and user information via Okta’s API and write the relevant values to individual rows in the data table. I’m going to use some code from Content Manager to manage the data table via Google SecOps’ REST API within a SOAR job.
If you’re not familiar with Content Manager, it’s a tool to manage artifacts such as rules, data tables, and rule exclusions via Google SecOps’ API and makes it easy to manage your content in a CI/CD pipeline. We’ll go over how to use Content Manager to create & update data tables in a future blog.
The code for my SOAR job can be found on GitHub and does the following:
The code handles API rate limits and supports pagination for folks who have an Okta organization with thousands of registered devices and users.
Creating a new SOAR job in Google SecOps
After testing the SOAR job, I schedule it to run every 10 minutes via the Jobs Scheduler page.
Scheduling the SOAR job to run every 10 minutes
Reviewing the logs for my job reveals that three rows were written to the data table, okta_user_device_ids. Let’s navigate to the data table in Google SecOps and review its contents.
[2025-05-28,11:08:48,000 INFO] Okta Domain: [DOMAIN].okta.com
[2025-05-28,11:08:48,000 INFO] Data Table Name: okta_user_device_ids
[2025-05-28,11:08:48,000 INFO] Google Cloud Project ID: [GOOGLE_CLOUD_PROJECT_ID]
[2025-05-28,11:08:48,000 INFO] Google Cloud Project Region: us
[2025-05-28,11:08:48,000 INFO] Google SecOps Customer ID: [GOOGLE_SECOPS_CUSTOMER_ID]
[2025-05-28,11:08:48,000 INFO] Attempting to retrieve all device IDs for Okta organization [DOMAIN].okta.com
[2025-05-28,11:08:48,000 INFO] Retrieved 3 devices
[2025-05-28,11:08:48,000 INFO] Retrieved a total of 3 devices
[2025-05-28,11:08:48,000 INFO] Attempting to list users for each device ID
[2025-05-28,11:08:48,000 INFO] Retrieved 1 users for device ID [DEVICE_ID]
[2025-05-28,11:08:49,000 INFO] Retrieved 1 users for device ID [DEVICE_ID]
[2025-05-28,11:08:49,000 INFO] Retrieved 1 users for device ID [DEVICE_ID]
[2025-05-28,11:08:49,000 INFO] Attempting to write device and user metadata (3 rows) to data table okta_user_device_ids
[2025-05-28,11:08:49,000 INFO] Successfully wrote device and user metadata (3 rows) to data table okta_user_device_ids
As you can see, my data table contains three rows with values for the device_id, user_id, and username. I don’t have a ton of users in my Okta developer organization, but these are enough objects for us to build the detection that we’re focusing on today.
Reviewing the contents of a data table in Google SecOps
Below is the YARA-L logic for the rule that I converted from Okta’s Customer Detection Catalogue. I’ll step through each of the important sections of this rule and explain what is happening in detail.
rule okta_device_registered_to_multiple_users {
meta:
author = "Okta"
description = "Identifies when the same device ID is registered to multiple Okta users. This behavior may indicate that an adversary has compromised multiple Okta user accounts and has registered their device as an MFA factor."
reference = "https://github.com/okta/customer-detections/blob/master/detections/device_registered_to_multiple_users.yml"
license = "https://github.com/okta/customer-detections/blob/master/LICENSE"
false_positives = "Legitimate use of shared phones by customer identity accounts. Service accounts that legitimately use a shared device."
events:
$device_reg.metadata.product_name = "Okta"
$device_reg.metadata.product_event_type = "device.user.add"
$device_id = $device_reg.extracted.fields["target[0].detailEntry.oktaDeviceId"]
$user_id = $device_reg.extracted.fields["actor.id"]
$device_id = %okta_user_device_ids.device_id
$user_id != %okta_user_device_ids.user_id
match:
$device_id over 15m
outcome:
$username = array_distinct($device_reg.extracted.fields["actor.alternateId"])
condition:
$device_reg
}
In the events section of the rule, we’re filtering for the presence of the “device.user.add” event type from Okta’s system log. This event is logged when an Okta user registers a new device for MFA, for example, Okta Verify. The unique ID for the device is assigned to the placeholder variable, “$device_id” and the user ID is assigned to the placeholder variable, “$user_id”.
The device ID value ($device_id) from the UDM event is being joined with the device_id column in the okta_user_device_ids data table.
Finally, we’re filtering events where the user ID ($user_id) in the UDM event does not match the value found in the user_id column in the same row: $user_id != %okta_user_device_ids.user_id
In the match section of the rule, we’re grouping the events by the value stored in $device_id over a 15 minute period. The email address of the user associated with the device is stored in the $username variable in the outcome section. Finally, for the rule to generate detections/alerts, the device registration event ($device_reg) has to have occurred.
To trigger the rule, I registered a device under my Okta user account using Okta Verify. This device was already registered to another user in my Okta organization. I ran the rule over today’s events and a detection was returned. I can see that user frenchee@[DOMAIN].com registered a device with the unique identifier ending with “Yo5d7”.
Reviewing a detection generated by the new rule
A logical first step to triage alerts from this rule is to determine which users are associated with the device ID. This can be accomplished through any of the following methods:
We’ve built a rule that detects when a device ID is associated with more than one Okta user account. What if we want to carry out a threat hunt to search for this behavior over historical events that are already ingested in Google SecOps. This is where a statistical search comes in handy.
The following search looks for device registration events, counts the distinct number of users associated with each device ID, and displays the results in a table.
metadata.product_name = "Okta"
metadata.product_event_type = "device.user.add"
$device_id = extracted.fields["target[0].detailEntry.oktaDeviceId"]
match:
$device_id
outcome:
$user_distinct_count = count_distinct(extracted.fields["actor.id"])
$user_ids = array_distinct(extracted.fields["actor.id"])
order:
$user_distinct_count desc
Reviewing the results of a statistical search ran over the last 30 days of events
In today’s post, we learned how to do the following:
Thanks for reading. Please feel free to reach out on the Google Cloud Security Community with any questions or if you’d like to share any cool detections you’ve built in Google SecOps.
Thanks to the Okta Identity Defense team for publishing their Customer Detection Catalog.
Thank you to the following people for providing me with valuable feedback: Drew Pilarski (Tempus AI) John Stoner, Mike Wilusz, and Jose Marin.