Google SecOps Detections
Overview
Google SecOps Detections collects detections from a source Google SecOps (Chronicle) instance through the Chronicle API and emits each one as a log record into a Praxis pipeline. Use it to forward rule detections and curated-detection alerts to another SecOps instance or any other destination.
It replaces the usual workaround of a data-lake rule_detections export to a bucket followed by a bucket feed: the detections are read directly from the Detection Engine API.
The source holds the legacyStreamDetectionAlerts stream open and receives detection alerts as they are created:
| Rule detections | Curated detections | Non-alerting detections |
|---|---|---|
| Yes | Yes | No (the stream carries alerts only) |
A detection counts as an alert only if its rule (or curated rule set) has Alerting turned on in the source instance.
The raw detection JSON (rule ID, name, version and labels, outcomes, and the UDM events it matched) is preserved in the log body. SecOps has no default parser for detections from another instance; see Sending to Google SecOps.
Supported types: Logs
Supported platforms: Linux, Windows, macOS
Minimum collector version: 0.5.78
Authentication
| Credential type | Fields |
|---|---|
Google Auth (googleauth) | Service account key JSON (or file path), optional scopes and impersonation |
Google SecOps (google_secops) | Service account key JSON (JSON Creds) |
Chronicle Auth (chronicleauth) | Service account key JSON (or file path), optional scopes and impersonation |
The service account must come from the source instance's bound Google Cloud project (not the destination's) and needs:
| Requirement | Value |
|---|---|
| IAM role on the source project | Chronicle API Viewer (roles/chronicle.viewer). Chronicle API Editor or Admin also work but grant more than needed. |
| IAM permission the stream checks | chronicle.legacies.legacyStreamDetectionAlerts on the instance. Chronicle API Viewer includes it; a custom role must include it. |
| API enabled on the source project | Chronicle API (chronicle.googleapis.com). It is already on for a project bound to a SecOps instance. |
| OAuth scope | https://www.googleapis.com/auth/cloud-platform (the default) or https://www.googleapis.com/auth/chronicle. If you set custom scopes on a googleauth or chronicleauth credential, include one of them. |
| Network | Outbound HTTPS (443) from the collector to the API endpoint and to oauth2.googleapis.com. Proxies must allow long-lived connections. |
# On the SOURCE instance's bound project
gcloud iam service-accounts create secops-detections-reader --project=<SOURCE_PROJECT>
gcloud projects add-iam-policy-binding <SOURCE_PROJECT> \
--member="serviceAccount:secops-detections-reader@<SOURCE_PROJECT>.iam.gserviceaccount.com" \
--role="roles/chronicle.viewer"
gcloud iam service-accounts keys create key.json \
--iam-account=secops-detections-reader@<SOURCE_PROJECT>.iam.gserviceaccount.com
Paste the contents of key.json into the credential. The collector mints and refreshes the OAuth token itself and sends it as Authorization: Bearer <token> on every request.
Enable alerts in the source instance
The stream carries detection alerts only, so turn on alerting for everything you want collected:
| Detections | Where in the source instance | Setting |
|---|---|---|
Custom rules (RULE_DETECTION) | Detections → Rules & Detections → Rules Dashboard, open the rule | Live Rule on and Alerting on |
Curated detections (GCTI_FINDING) | Content Hub → Curated Detections (or Detections → Curated Detections), open the rule set | Status enabled (Precise and/or Broad) and Alerting on |
Detections created while Alerting was off are not streamed, even after Alerting is turned on.
How the connection works
All requests go to {endpoint}/v1alpha/projects/{project_id}/locations/{region}/instances/{instance_id}.
- Connect:
POST …/legacy:legacyStreamDetectionAlertswith exactly one resume point: the savedpageToken, or apageStartTime(the saved time, ornow − lookbackon first start). - Read: the server keeps the connection open and sends a JSON array of batches as alerts are created: heartbeats (about every 15 seconds), batches of detections, and a new resume point (
nextPageTokenornextPageStartTime). - Checkpoint: each resume point is saved only after the batch's detections have been accepted downstream.
- Reconnect: after a disconnect, an error from the server, or 5 minutes without any data or heartbeat, the source reopens from the saved resume point. Failed attempts back off from 1 second, doubling up to 5 minutes; a connection that stayed up at least a minute reconnects at once. The connection has no overall timeout.
Basic configuration
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
region | string | Yes | us | Location of the source instance: us, eu, europe, asia-southeast1, … |
project_id | string | Yes | none | Google Cloud project ID or number the source instance is bound to (SIEM Settings → Profile → Manage instance). |
instance_id | string | Yes | none | Source instance customer ID (UUID), from SIEM Settings → Profile → Organization Details. |
rule_detections | bool | No | true | Detections from custom YARA-L rules (RULE_DETECTION). |
curated_detections | bool | No | true | Detections from Google curated rule sets (GCTI_FINDING) enabled in the source instance. At least one of the two must be on. |
lookback | integer (seconds) | No | 3600 | Cold start only. How far back the first collection reaches when no checkpoint is stored. Minimum 1; at most 604800 (7 days), the API's limit. |
persist_offsets | bool | No | true | Persist Checkpoint. Store where collection stopped so restarts resume there. See Resumption. |
advanced
| Parameter | Type | Default | Description |
|---|---|---|---|
endpoint | string (URI) | https://chronicle.<region>.rep.googleapis.com | API base URL, no path. Older instances may need https://<region>-chronicle.googleapis.com. |
batch_size | integer | 100 | Maximum detections per stream batch (1–1000). |
Resumption
The source advances its checkpoint only after a batch has been accepted downstream, so a crash, restart or pipeline backpressure replays from the last saved point rather than skipping detections.
With Persist Checkpoint on (the default), Praxis automatically provisions a storage extension for this node under the collector's metadata directory (<collector home>/metadata/googlesecops_detections_offsets/<node name>). Turn it off only for ephemeral collectors or read-only filesystems: without it, every restart cold-starts from now − lookback.
- Resume point. The API returns either a page token or a start time with each batch. A token is saved and the saved start time is left as it was; a time is saved and the token dropped. A reconnect sends the token if one is saved, otherwise the start time. A refused page token is retried as it is.
- Long outages. The stream API rejects start times older than 7 days. When it refuses a saved start time for that reason, the source resets it to now − 7 days + 1 minute and logs a warning; detections older than that are skipped.
- After hand-off. Once the pipeline accepts a batch, the destination owns its delivery. Keep the Google SecOps destination's disk-backed backpressure queue on so queued detections survive a restart. The destination still drops a batch it cannot deliver: on a permanent error, such as a log type that does not exist in the destination instance, or once its retries exceed max time elapsed (300 s by default). Create the custom log type before deploying the pipeline.
- Backpressure vs. permanent rejection. If the pipeline is temporarily busy, the batch is requested again. If the pipeline permanently rejects a batch, it is dropped (counted as
reason="permanent") so later detections are not blocked.
Sending to Google SecOps
The source does not set a log type. In the destination instance:
- Create a custom log type under SIEM Settings → Available Log Types → Create custom log type: Vendor/Product
Google SecOps, Log TypeCHRONICLE_DETECTIONS. SecOps appends the suffix, givingCHRONICLE_DETECTIONS_CUSTOM. It takes about 10 minutes to become usable. - Write a custom parser for it. The record body is the detection JSON shown in Output mapping. Until then, logs arrive as unparsed
GENERIC_EVENTs with auto-extracted fields. - Set that log type in the pipeline: add a Google SecOps Standardization processor with Log Type
CHRONICLE_DETECTIONS_CUSTOM, or set it as the Google SecOps destination's Fallback Log Type.
Do not point the source at the same instance as the destination: that instance would re-ingest its own detections.
Output mapping
Each detection becomes one log record.
| Detection field | OTel destination |
|---|---|
| (whole detection JSON) | LogRecord.Body (string) |
detectionTime, else createdTime | LogRecord.Timestamp (falls back to receive time if unparseable) |
| (receive time) | LogRecord.ObservedTimestamp |
curated detection[].severity, else the rule's severity label | LogRecord.SeverityNumber / SeverityText: CRITICAL/HIGH/MEDIUM/LOW map to OTel FATAL/ERROR/WARN/INFO2; anything else maps to INFO rather than being dropped |
Records and their resource carry no attributes: every detection field (id, type, createdTime, the rule, alert state and severity) is already in the body, which is untouched, so a downstream parser sees the complete record. A rule detection body looks like this (shortened):
{
"id": "de_7c1a…",
"type": "RULE_DETECTION",
"createdTime": "2026-10-12T10:00:05Z",
"detectionTime": "2026-10-12T10:00:00Z",
"timeWindow": { "startTime": "2026-10-12T09:50:00Z", "endTime": "2026-10-12T10:00:00Z" },
"detection": [{
"ruleId": "ru_…", "ruleName": "suspicious_login", "ruleVersion": "ru_…@v_…",
"alertState": "ALERTING", "ruleType": "MULTI_EVENT",
"ruleLabels": [{ "key": "severity", "value": "High" }],
"outcomes": [{ "variable": "risk_score", "value": "80" }]
}],
"collectionElements": [{ "label": "e", "references": [{ "event": { "metadata": { "eventType": "USER_LOGIN" } } }] }]
}
Example configuration
{
"region": "us", // required
"project_id": "123456789012", // required
"instance_id": "ebdc4bb9-878b-11e7-8455-10604b7cb5c1", // required
"rule_detections": true, // default: true
"curated_detections": true, // default: true
"lookback": 3600, // default: 3600, at most 604800
"persist_offsets": true, // default: true
"advanced": {
"batch_size": 100 // default: 100
}
}
Attach a googleauth, google_secops or chronicleauth credential to the node; the service account key is not part of the node config.
Operational notes
- Only alerts are streamed. A rule or curated rule set with Alerting off still produces detections in the source instance, but the stream does not deliver them. Turn Alerting on for the rules and rule sets whose detections you want.
- Curated detections need their rule set enabled. In the source instance, enable the rule set (Precise and/or Broad) and turn Alerting On under Curated Detections. Google's Managed Detection Testing rule sets can be used to trigger a test alert.
- A quiet stream is normal. With no new alerts, the connection only carries heartbeats and resume points; the checkpoint still moves forward.
- Rejected requests recover on their own. A
400/401/403/404is retried every 5 minutes, so fixing the role or credential recovers the source without a redeploy.429/503honour the server'sRetry-After.
Troubleshooting
| Symptom in the collector log | Cause | Fix |
|---|---|---|
secops detection stream rejected … 403 / PERMISSION_DENIED | The service account lacks Chronicle API Viewer (permission chronicle.legacies.legacyStreamDetectionAlerts) on the source project, or belongs to another project. | Grant the role on the source instance's bound project. Role changes can take a few minutes to apply. |
… 403 / SERVICE_DISABLED "Chronicle API has not been used in project …" | project_id is not the instance's bound project (for example an ingestion key's malachite-… project). | Set project_id to the bound project shown under Manage instance, with a service account from that project. |
… 404 / NOT_FOUND | Wrong project_id, instance_id or region, or the instance needs the older endpoint. | Check the three IDs. Try advanced.endpoint = https://<region>-chronicle.googleapis.com. |
… 401 | The token was rejected, typically because custom scopes leave out cloud-platform. | Add https://www.googleapis.com/auth/cloud-platform to the credential's scopes. |
secops detection stream rejected … 400 / INVALID_ARGUMENT while resuming from a page token | The API refused the saved page token, for example after a long outage. It is retried as it is. | Stop the collector and delete the node's checkpoint folder (<collector home>/metadata/googlesecops_detections_offsets/<node name>) to resume from the lookback. |
stream start time is older than the API's 7-day limit; resetting it | The collector was stopped for more than 7 days. | None. Detections older than 7 days cannot be streamed. |
secops detection stream disconnected, repeating | Network or proxy closing long-lived connections. | Allow long-lived HTTPS connections to the endpoint through the proxy. |
| Connected, but no records | No alerts in the source instance, or Alerting is off on the rules. | Check Alerts & IOCs in the source instance, and turn Alerting On for the rules you want. |
Metrics
| Metric Name | Description |
|---|---|
prx_src_records_received_total{signal="logs"} | Log records emitted, one per detection, counted at each hand-off attempt. |
prx_src_bytes_received_total | Bytes read from the Chronicle API, for every stream batch. |
prx_src_pipeline_latency_seconds | Arrival → downstream handoff. |
prx_src_errors_total{reason="io"} | Stream disconnects and error batches, idle-stream timeouts, rejected requests and refused resume tokens. |
prx_src_parse_errors_total{parser="json"} | Each detection that could not be decoded. The rest of the batch is still sent. |
prx_src_records_dropped_total{reason} | backpressure: the downstream pipeline refused the batch, which is requested again. permanent: the pipeline permanently rejected the batch, which is dropped. |
prx_src_connections_total / _closed_total / _active | Once each at source start and stop (there is no inbound listener). |
prx_health_status{component="receiver"} | 1 while collecting; 0 when requests are rejected or failing. |