Skip to main content
Question

How to distinguish between precise and broad curated detections at the alert level in composite detections

  • June 16, 2026
  • 3 replies
  • 32 views

greon

Hi everyone. I am working on enabling curated detections via composite detections. One issue that I have not been able to figure out yet so far is, is there a UDM value that defines whether a curated detection rule is broad or precise? I looked at the meta fields of the curated detections and used the network inspection tool as well to look at the actual json format. So far I have not been able to find such a field.

My usecase is to write separate composite detections for precise curated detections and broad curated detections. I have a rule that consumes detections from curated detections but, as far as I know, it is not possible to distinguish whether these detections come from precise or broad rules.

 

Thanks in advance.

3 replies

hliu
Forum|alt.badge.img+4
  • Bronze 2
  • June 16, 2026

It looks like while the precision field is available in contentHub/featuredContentRules/ and curatedRules/ endpoints, as you’ve pointed out the same field is not exposed in the detections created from these rules, in the same way the curated rule definitions are not available in the detections.
 

above figure displays part the results from the curatedRules/ endpoint containing the precision field.

Perhaps a possible workaround could be on having a data table containing the list of curated rule names or rule IDs, and its precision classification. And in the composite rule, use that table to enrich and filter for your desired outcome.
Just an untested idea. Let us know how it goes if you ever try it.


Another option could be just requesting Google to expose this information in the curated rule detections, as a ruleLabel or any other metadata field.


cmorris
Staff
Forum|alt.badge.img+13
  • Staff
  • June 16, 2026


Perhaps a possible workaround could be on having a data table containing the list of curated rule names or rule IDs, and its precision classification. And in the composite rule, use that table to enrich and filter for your desired outcome.
Just an untested idea. Let us know how it goes if you ever try it.

I have a  SOAR job that may help with this, I have been using it for enriching audit events related to rules. Go to Response > IDE and create a new job under the Chronicle integration. Add one parameter ‘Data Table Name’ with the type String and then paste in the code at the bottom and save. Create a new Data Table with columns rule_id, rule_name, pack_name, pack_precision, enabled, and alerting. Add the Data Table’s name to the DT Name parameter for the SOAR job and run once in the IDE or setup the scheduler.

 

Results of the job:

 

Job code:

from SiemplifyJob import SiemplifyJob
from SiemplifyUtils import output_handler
from TIPCommon.extraction import extract_job_param
import json
import requests
from google.oauth2 import service_account
import google.auth.transport.requests

SCOPES = ['https://www.googleapis.com/auth/cloud-platform']
INTEGRATION_NAME = "GoogleChronicle"

def get_auth_token(service_account_json):
"""Generates an OAuth2 token using the provided Service Account JSON."""
creds_info = json.loads(service_account_json)
creds = service_account.Credentials.from_service_account_info(creds_info, scopes=SCOPES)
request = google.auth.transport.requests.Request()
creds.refresh(request)
return creds.token

@output_handler
def main():
siemplify = SiemplifyJob()
siemplify.script_name = "Sync Curated Detections to Data Table"
siemplify.LOGGER.info("--------------- JOB STARTED ---------------")

try:
siemplify.LOGGER.info(f"Fetching configuration for {INTEGRATION_NAME} integration...")
integration_config = siemplify.get_configuration(INTEGRATION_NAME)

api_root = integration_config.get("API Root")
sa_json = integration_config.get("User's Service Account")
workload_email = integration_config.get("Workload Identity Email")

if not api_root:
raise ValueError(f"Could not find 'API Root' in {INTEGRATION_NAME} configuration.")

if not sa_json and not workload_email:
siemplify.LOGGER.info("Neither SA JSON nor Workload Identity Email provided. Will attempt default pod credentials.")

data_table_name = extract_job_param(siemplify, param_name="Data Table Name", is_mandatory=True, print_value=True)

siemplify.LOGGER.info("Authenticating with Google SecOps API...")
token = get_auth_token(sa_json)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}

siemplify.LOGGER.info("Fetching Category Metadata...")
categories_url = f"{api_root}/curatedRuleSetCategories"
category_name_map = {}

c_page_token = None
while True:
c_params = {}
if c_page_token:
c_params['pageToken'] = c_page_token

c_response = requests.get(categories_url, headers=headers, params=c_params)
c_response.raise_for_status()
c_json = c_response.json()

for cat in c_json.get('curatedRuleSetCategories', []):
cat_full_name = cat.get("name", "")
cat_uid = cat_full_name.split("/")[-1] if "/" in cat_full_name else cat_full_name
category_name_map[cat_uid] = cat.get("displayName", cat_uid)

c_page_token = c_json.get('nextPageToken')
if not c_page_token:
break

siemplify.LOGGER.info("Fetching Pack Metadata...")
rulesets_url = f"{api_root}/curatedRuleSetCategories/-/curatedRuleSets"
ruleset_name_map = {}

rs_page_token = None
while True:
rs_params = {}
if rs_page_token:
rs_params['pageToken'] = rs_page_token

rs_response = requests.get(rulesets_url, headers=headers, params=rs_params)
rs_response.raise_for_status()
rs_json = rs_response.json()

for rs in rs_json.get('curatedRuleSets', []):
rs_full_name = rs.get("name", "")
rs_uid = rs_full_name.split("/")[-1] if "/" in rs_full_name else rs_full_name
ruleset_name_map[rs_uid] = rs.get("displayName", rs_uid)

rs_page_token = rs_json.get('nextPageToken')
if not rs_page_token:
break

siemplify.LOGGER.info("Successfully built translation dictionaries for Categories and Packs.")

siemplify.LOGGER.info("Fetching Curated Rule Set Deployments...")
fetch_url = f"{api_root}/curatedRuleSetCategories/-/curatedRuleSets/-/curatedRuleSetDeployments"

rules_data = []
page_token = None

while True:
params = {}
if page_token:
params['pageToken'] = page_token

response = requests.get(fetch_url, headers=headers, params=params)
response.raise_for_status()
raw_json = response.json()

rules_data.extend(raw_json.get('curatedRuleSetDeployments', []))

page_token = raw_json.get('nextPageToken')
if not page_token:
break

siemplify.LOGGER.info(f"Successfully fetched {len(rules_data)} Curated Rule Set Deployments.")

if not rules_data:
siemplify.LOGGER.info("No curated deployments found. Exiting gracefully.")
siemplify.end_script()
return

requests_list = []
for dep in rules_data:
full_name = dep.get("name", "")

parts = full_name.split("/")
category_uid = parts[parts.index("curatedRuleSetCategories") + 1] if "curatedRuleSetCategories" in parts else "Unknown_Category"
ruleset_uid = parts[parts.index("curatedRuleSets") + 1] if "curatedRuleSets" in parts else "Unknown_RuleSet"

pack_readable = str(ruleset_name_map.get(ruleset_uid, ruleset_uid))
category_readable = str(category_name_map.get(category_uid, category_uid))

precision = str(dep.get("precision", "N/A"))
is_enabled = "true" if dep.get("enabled", False) else "false"
is_alerting = "true" if dep.get("alerting", False) else "false"

row_values = [
str(ruleset_uid), # 1. rule_id
pack_readable, # 2. rule_name
category_readable, # 3. pack_name
precision, # 4. pack_precision
is_enabled, # 5. enabled
is_alerting # 6. alerting
]

requests_list.append({
"dataTableRow": {
"values": row_values
}
})

siemplify.LOGGER.info(f"Replacing Data Table '{data_table_name}' with {len(requests_list)} rows...")

dt_url = f"{api_root}/dataTables/{data_table_name}/dataTableRows:bulkReplaceAsync"
payload = {"requests": requests_list}

dt_response = requests.post(dt_url, headers=headers, json=payload)

if dt_response.status_code in [200, 201, 204]:
siemplify.LOGGER.info(f"Successfully initiated Data Table replacement for '{data_table_name}'.")
else:
siemplify.LOGGER.error(f"Failed to sync Data Table. Status Code: {dt_response.status_code}. Response: {dt_response.text}")

siemplify.LOGGER.info("--------------- JOB FINISHED ---------------")

except Exception as e:
siemplify.LOGGER.error(f"Job failed with error: {e}")
siemplify.LOGGER.exception(e)
raise

siemplify.end_script()

if __name__ == "__main__":
main()

 


jstoner
Community Manager
Forum|alt.badge.img+24
  • Community Manager
  • June 16, 2026

Thanks for the note. I went back through to double check and I am also not seeing that precise/broad classifier in the detection schema. I have shared that feedback but would encourage you to raise this as a feature request. There are other detection schema descriptors in addition to the variables that could also be used, but at the moment that one specifically is not there.