Bulk closing alerts with Python and the Google Security Operations API

Dan Dye's Dwell Time
Google Cloud - Community
7 min readAug 2, 2024

In the postscript of my last blog post, I scripted 99 calls to the Ingestion API to create 99 USER_LOGIN Unified Data Model (UDM) events where the principal.ip was one of the known-malicious IPs from an Abuse IPDB blocklist (stored as a reference list in Google Security Operations). Since I had a detection rule that triggered on $event.principal.ip in %AbuseIPDB_Blocklist and that detection rule was both active and alerting, I now have 99 alerts associated with that rule. I got 99 problems but bulk closing ’em ain’t one!

99 Alerts from my detection rule named “ip_in_abuseipdb_blocklist”

Closing each of these alerts with the web UI would be tedious. It can be done programmatically with a SecOps SOAR playbook, but I’m going to demonstrate how to bulk close them using the Google Security Operations REST API. I will call the API from a Python script that is shared in the api-samples-python GitHub repo. I hope that you, the reader, will see that the Python code is modular, extensible, and supports refactoring to suit your own automation needs.

Searching Detections

My first task is to get the detection IDs for the alerts that were raised by a specific rule using the new REST API for Google Security Operations (currently in v1alpha). I’ve previously introduced Python samples for that REST API in Update Reference Lists with Python and the new Chronicle REST API. Once again, I need to provide this notice, since the REST API is pre-general availability (Pre-GA):

Note: This feature is covered by Pre-GA Offerings Terms of the SecOps Service Specific Terms. Pre-GA features might have limited support, and changes to pre-GA features might not be compatible with other pre-GA versions. For more information, see the SecOps Technical Support Service guidelines and the SecOps Service Specific Terms.

The SearchDetections method on the REST API and the associated Python sample utility (list_detections.py) support a page size up to 1k results ( --page_size=1000) and pagination (via --page_token) should you need to get more than 1k results. There are also options to more precisely target the detections/alerts by creation/detection time and by alert state (an ALERTING detection is an “alert”). Here is the full usage for the Python utility (reformatted for readability):

$ python -m detect.v1alpha.list_detections --help
usage: list_detections.py
[-h]
[-c CREDENTIALS_FILE]
[-r {asia-northeast1,...,us}]
-i PROJECT_INSTANCE
-p PROJECT_ID
-rid RULE_ID
[--alert_state {UNSPECIFIED,NOT_ALERTING,ALERTING}]
[--page_size PAGE_SIZE]
[--page_token PAGE_TOKEN]
-h, --help show this help message and exit
-c CREDENTIALS_FILE, --credentials_file CREDENTIALS_FILE
credentials file path (default: '${HOME}/.chronicle_credentials.json')
-r {asia-northeast1,...,us}, --region {asia-northeast1,..,us}
the region where the customer is located (default: us)
-i PROJECT_INSTANCE, --project_instance PROJECT_INSTANCE
Customer ID for Chronicle instance
-p PROJECT_ID, --project_id PROJECT_ID
Your BYOP, project id
-rid RULE_ID, --rule_id RULE_ID
rule id to list detections for.
Options are
(1) rule_id
(2) rule_id@v_<seconds>_<nanoseconds>
(3) rule_id@- which matches on all versions.
--alert_state {UNSPECIFIED,NOT_ALERTING,ALERTING}
--page_size PAGE_SIZE
--page_token PAGE_TOKEN

The output from list_detections.py is in JSON format and, in the usage below, I’ve redirected it to a file on disk (ip_in_abuseipdb_out.json).

Sorry for the screenshot—cloudflare hates this snippet!

JSON -> TXT

In the resulting JSON file, every item within the detections array has an identifier id, with a value that starts with “de_”. In the screenshot below, the alertState value for the shown detection in the array is ALERT. This is commonly just referred to as an “alert”, but technically it’s an “alerting detection”, which means it’s a detection with alert attributes.

For the 99 alerting detections in my JSON file, I‘ll next extract the detection identifiers (IDs) and emit them into a plain text file with one ID per line using the command-line JSON processor, jq.

# convert the json to a flat file with one ID per line
cat ip_in_abuseipdb_out.json | jq -r '.detections[].id' \
> ip_in_abuseipdb_out.txt

The snippet below shows the top two lines of the resulting text file:

$ head -n2 ip_in_abuseipdb_out.txt
de_ad9d2771-a567-49ee-6452-1b2db13c1d33
de_3c2e2556-aba1-a253-7518-b4ddb666cc32

Retrieving Alerts

Next, I want to get the details for a single alerting detection. This isn’t strictly necessary for my use case, but would come in handy if I wanted to verify the alert wasn’t already closed or for before/after comparison to verify that the intended change was made. Also, I want to show off the get_alert.py module. 🙂

The get_alert.py Python module is calling the method GetAlert, again using the new REST API for Google Security Operations.

That Python module gets the details for a single provided detection or alerting detection ID (both start with de_*).

ALERT_ID=de_ad9d2771-a567-49ee-6452-1b2db13c1d33
python -m detect.v1alpha.get_alert \
--project_id=$PROJECT_ID \
--project_instance=$PROJECT_INSTANCE \
--credentials_file=$CREDENTIALS_FILE \
--alert_id=$ALERT_ID
{
"alert": {
"type": "RULE_DETECTION",
"detection": [
{
"ruleName": "ip_in_abuseipdb_blocklist",
"description": "IP matches AbuseIPDB blocklist",
"ruleId": "ru_4a0173fd-ad41-4925-90ba-5b87a81a48c1",
"ruleVersion": "ru_4a0173fd-ad41-4925-90ba-5b87a81a48c1@v_1711118794_340383000",
"alertState": "ALERT",
"ruleType": "SINGLE_EVENT",
...

Reviewing Alert Feedback and Status Objects

When alerts are closed, feedBackSummary and feedbackHistory objects (collectively, “feedback”) are added to the alerting detection’s JSON representation. As shown in the screenshot below, there is only one feedbackSummary (the latest) but feedbackHistory is a JSON Array with the first item being the most recent. To close the alert, we set the feedback status to “CLOSED”.

feedbackSummary and feedbackHistory Objects in the JSON representation of a Detection/Alert.

Bulk Closing Alerts

We use the UpdateAlert API method to create or update the feedback and set the status to “CLOSED”. For a single alert, it can be called with the Python module update_alert.py. The bulk_update_alerts.py script simply imports functionality from the update_alert.py module in order to call the UpdateAlert API method once for each of the detection IDs contained in the text file that is provided.

Before using this example script, please note that I’ve hard-coded the feedback payload at the top of the bulk_update_alerts.py Python file as follows:

DEFAULT_FEEDBACK = {
"reason": "REASON_MAINTENANCE",
"reputation": "REPUTATION_UNSPECIFIED",
"status": "CLOSED",
"verdict": "VERDICT_UNSPECIFIED",
"comment": "automated cleanup",
"rootCause": "Other",
}

You may want to edit those feedback values to suit your needs by directly editing the Python file. Alternatively, you can override individual values (and add some non-default ones) with CLI params (e.g. --verdict="FALSE_POSITIVE").

Note that the values in DEFAULT_FEEDBACK that are in ALL CAPS are enumerated vocabularies (“enum”). In the Reference for JSON representation of FeedBack shown in the image below, the vocabularies’ allowed values are linked on the enum noun (Verdict, Reputation, etc.).

Reference documentation for JSON representation of FeedBack

Those enum values are also specified and enforced in the update_alert.py Python file (see image below) and imported and used in bulk_update_alerts.py as well.

GitHub view of update_alert.py file’s enumerated vocabulary values.

Here is my own usage for closing my alerts with that default feedback:

# close each alert in the file, ip_in_abuseipdb_out.txt
python -m detect.v1alpha.bulk_update_alerts \
--project_id=$PROJECT_ID \
--project_instance=$PROJECT_INSTANCE \
--credentials_file=$CREDENTIALS_FILE \
--alert_ids_file="$(pwd)/ip_in_abuseipdb_out.txt"

In the screenshot below, you can see that I’ve now got 112 Closed alerts for that rule. Where did the extra 13 alerts come from? That Abuse IPDB rule is so general that while I was writing this, it generated real (script-kiddie scanner) alerts in addition to my synthetic USER_LOGIN events.

Get Alert again to verify

For programmatic verification that an update was applied, I can optionally use the GetAlert method on the API again. The response confirms that the feedbackSummary and feedbackHistory values have been added to the alert and its status is now CLOSED:

ALERT_ID=de_ad9d2771-a567-49ee-6452-1b2db13c1d33
python -m detect.v1alpha.get_alert \
--project_id=$PROJECT_ID \
--project_instance=$PROJECT_INSTANCE \
--credentials_file=$CREDENTIALS_FILE \
--alert_id=$ALERT_ID
...
"detectionTime": "2024-07-01T22:28:24Z",
"feedbackSummary": {
"idpUserId": "secops-api-admin@dandye-0324-chronicle.iam.gserviceaccount.com",
"createdTime": "2024-07-01T22:41:42.747024063Z",
"verdict": "VERDICT_UNSPECIFIED",
"reputation": "REPUTATION_UNSPECIFIED",
"riskScore": 40,
"comment": "automated cleanup",
"status": "CLOSED",
"priority": "PRIORITY_UNSPECIFIED",
"rootCause": "Other",
"reason": "REASON_MAINTENANCE",
"severityDisplay": "Medium",
"priorityDisplay": "Unspecified"
},
"feedbackHistory": [
{
"idpUserId": "secops-api-admin@dandye-0324-chronicle.iam.gserviceaccount.com",
"createdTime": "2024-07-01T22:41:42.747024063Z",
"verdict": "VERDICT_UNSPECIFIED",
"reputation": "REPUTATION_UNSPECIFIED",
"comment": "automated cleanup",
"status": "CLOSED",
"rootCause": "Other",
"reason": "REASON_MAINTENANCE"
}
]
}
}

To recap, here is the workflow for batch closing alerts associated with a detection:

  1. Call the SearchDetections method on the REST API to get JSON representations of the detections associated with a detection rule.
  2. Use jq to extract the detection IDs from a JSON file and emit them into a plain text file.
  3. Optionally, use the GetAlert method to retrieve detection details for a single alert.
  4. Use the UpdateAlert method to close each alert.
  5. Optionally, use the GetAlert method to verify the alert status and other metadata was updated.

The provided Python api sample modules can be used for this as-is, or you can extend and repurpose them for your own automation needs.

Future Improvements

I would like to hear your thoughts on how to improve this workflow. My current running list of caveats and possible solutions are:

  1. Hard-coded feedback payload
    — Maybe add an option to supply a CSV with feedback keys in columns
    --alert_ids_txt_file=<PATH_TO_FILE> Expect one alert ID per line
    --alert_ids_csv_file=<PATH_TO_FILE> Respect the headers in the first line
  2. List detections pagination
    — Maybe provide a wrapper script to handle the pagination
  3. Duplication of feedback
    — Maybe before each update, check the status first to ensure that we aren’t duplicating updates
  4. …?

--

--