Create GCP Chronicle Detection Rules

The gcp:chronicle/rule:Rule resource, part of the Pulumi GCP provider, defines Chronicle detection rules written in YARA-L that evaluate security telemetry for threat patterns. This guide focuses on three capabilities: YARA-L rule syntax and compilation, deletion policies for rules with detections, and data access scope binding.

Rules require a Chronicle instance (identified by customer ID) and optionally reference data access scopes to filter which logs they can access. The examples are intentionally small. Combine them with your own Chronicle instance configuration and data access policies.

Create a detection rule with YARA-L syntax

Security teams write detection rules in YARA-L to identify patterns in security telemetry, such as unusual user behavior or suspicious network activity.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const example = new gcp.chronicle.Rule("example", {
    location: "us",
    instance: "00000000-0000-0000-0000-000000000000",
    deletionPolicy: "DEFAULT",
    text: "rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n",
});
import pulumi
import pulumi_gcp as gcp

example = gcp.chronicle.Rule("example",
    location="us",
    instance="00000000-0000-0000-0000-000000000000",
    deletion_policy="DEFAULT",
    text="rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n")
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/chronicle"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := chronicle.NewRule(ctx, "example", &chronicle.RuleArgs{
			Location:       pulumi.String("us"),
			Instance:       pulumi.String("00000000-0000-0000-0000-000000000000"),
			DeletionPolicy: pulumi.String("DEFAULT"),
			Text:           pulumi.String("rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var example = new Gcp.Chronicle.Rule("example", new()
    {
        Location = "us",
        Instance = "00000000-0000-0000-0000-000000000000",
        DeletionPolicy = "DEFAULT",
        Text = @"rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }
",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.chronicle.Rule;
import com.pulumi.gcp.chronicle.RuleArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var example = new Rule("example", RuleArgs.builder()
            .location("us")
            .instance("00000000-0000-0000-0000-000000000000")
            .deletionPolicy("DEFAULT")
            .text("""
rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }
            """)
            .build());

    }
}
resources:
  example:
    type: gcp:chronicle:Rule
    properties:
      location: us
      instance: 00000000-0000-0000-0000-000000000000
      deletionPolicy: DEFAULT
      text: |
        rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }        

The text property contains the YARA-L rule definition. Chronicle compiles this syntax into an executable detection that runs against ingested logs. The rule matches user activity patterns over a 10-minute window. The deletionPolicy controls what happens when you delete the rule: DEFAULT requires no associated detections or retrohunts, while FORCE removes everything.

Control deletion behavior for rules with detections

Rules that have generated detections or triggered retrohunts need explicit deletion policy to prevent accidental data loss.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const example = new gcp.chronicle.Rule("example", {
    location: "us",
    instance: "00000000-0000-0000-0000-000000000000",
    deletionPolicy: "FORCE",
    text: "rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n",
});
import pulumi
import pulumi_gcp as gcp

example = gcp.chronicle.Rule("example",
    location="us",
    instance="00000000-0000-0000-0000-000000000000",
    deletion_policy="FORCE",
    text="rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n")
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/chronicle"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := chronicle.NewRule(ctx, "example", &chronicle.RuleArgs{
			Location:       pulumi.String("us"),
			Instance:       pulumi.String("00000000-0000-0000-0000-000000000000"),
			DeletionPolicy: pulumi.String("FORCE"),
			Text:           pulumi.String("rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var example = new Gcp.Chronicle.Rule("example", new()
    {
        Location = "us",
        Instance = "00000000-0000-0000-0000-000000000000",
        DeletionPolicy = "FORCE",
        Text = @"rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }
",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.chronicle.Rule;
import com.pulumi.gcp.chronicle.RuleArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var example = new Rule("example", RuleArgs.builder()
            .location("us")
            .instance("00000000-0000-0000-0000-000000000000")
            .deletionPolicy("FORCE")
            .text("""
rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }
            """)
            .build());

    }
}
resources:
  example:
    type: gcp:chronicle:Rule
    properties:
      location: us
      instance: 00000000-0000-0000-0000-000000000000
      deletionPolicy: FORCE
      text: |
        rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }        

Setting deletionPolicy to FORCE allows you to delete the rule even if it has associated detections or retrohunts. This removes the rule, its deployment, all detections, and all retrohunts. The DEFAULT policy (shown in the first example) only succeeds if the rule has no associated data.

Restrict rule evaluation to specific log types

Organizations with multiple data sources often limit which logs a rule can access for compliance or performance optimization.

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const dataAccessScopeTest = new gcp.chronicle.DataAccessScope("data_access_scope_test", {
    location: "us",
    instance: "00000000-0000-0000-0000-000000000000",
    dataAccessScopeId: "scope-name",
    description: "scope-description",
    allowedDataAccessLabels: [{
        logType: "GCP_CLOUDAUDIT",
    }],
});
const example = new gcp.chronicle.Rule("example", {
    location: "us",
    instance: "00000000-0000-0000-0000-000000000000",
    scope: googleChronicleDataAccessScope.dataAccessScopeTest.name,
    text: "rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n",
});
import pulumi
import pulumi_gcp as gcp

data_access_scope_test = gcp.chronicle.DataAccessScope("data_access_scope_test",
    location="us",
    instance="00000000-0000-0000-0000-000000000000",
    data_access_scope_id="scope-name",
    description="scope-description",
    allowed_data_access_labels=[{
        "log_type": "GCP_CLOUDAUDIT",
    }])
example = gcp.chronicle.Rule("example",
    location="us",
    instance="00000000-0000-0000-0000-000000000000",
    scope=google_chronicle_data_access_scope["dataAccessScopeTest"]["name"],
    text="rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n")
package main

import (
	"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/chronicle"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := chronicle.NewDataAccessScope(ctx, "data_access_scope_test", &chronicle.DataAccessScopeArgs{
			Location:          pulumi.String("us"),
			Instance:          pulumi.String("00000000-0000-0000-0000-000000000000"),
			DataAccessScopeId: pulumi.String("scope-name"),
			Description:       pulumi.String("scope-description"),
			AllowedDataAccessLabels: chronicle.DataAccessScopeAllowedDataAccessLabelArray{
				&chronicle.DataAccessScopeAllowedDataAccessLabelArgs{
					LogType: pulumi.String("GCP_CLOUDAUDIT"),
				},
			},
		})
		if err != nil {
			return err
		}
		_, err = chronicle.NewRule(ctx, "example", &chronicle.RuleArgs{
			Location: pulumi.String("us"),
			Instance: pulumi.String("00000000-0000-0000-0000-000000000000"),
			Scope:    pulumi.Any(googleChronicleDataAccessScope.DataAccessScopeTest.Name),
			Text:     pulumi.String("rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }\n"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Gcp = Pulumi.Gcp;

return await Deployment.RunAsync(() => 
{
    var dataAccessScopeTest = new Gcp.Chronicle.DataAccessScope("data_access_scope_test", new()
    {
        Location = "us",
        Instance = "00000000-0000-0000-0000-000000000000",
        DataAccessScopeId = "scope-name",
        Description = "scope-description",
        AllowedDataAccessLabels = new[]
        {
            new Gcp.Chronicle.Inputs.DataAccessScopeAllowedDataAccessLabelArgs
            {
                LogType = "GCP_CLOUDAUDIT",
            },
        },
    });

    var example = new Gcp.Chronicle.Rule("example", new()
    {
        Location = "us",
        Instance = "00000000-0000-0000-0000-000000000000",
        Scope = googleChronicleDataAccessScope.DataAccessScopeTest.Name,
        Text = @"rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }
",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.gcp.chronicle.DataAccessScope;
import com.pulumi.gcp.chronicle.DataAccessScopeArgs;
import com.pulumi.gcp.chronicle.inputs.DataAccessScopeAllowedDataAccessLabelArgs;
import com.pulumi.gcp.chronicle.Rule;
import com.pulumi.gcp.chronicle.RuleArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var dataAccessScopeTest = new DataAccessScope("dataAccessScopeTest", DataAccessScopeArgs.builder()
            .location("us")
            .instance("00000000-0000-0000-0000-000000000000")
            .dataAccessScopeId("scope-name")
            .description("scope-description")
            .allowedDataAccessLabels(DataAccessScopeAllowedDataAccessLabelArgs.builder()
                .logType("GCP_CLOUDAUDIT")
                .build())
            .build());

        var example = new Rule("example", RuleArgs.builder()
            .location("us")
            .instance("00000000-0000-0000-0000-000000000000")
            .scope(googleChronicleDataAccessScope.dataAccessScopeTest().name())
            .text("""
rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }
            """)
            .build());

    }
}
resources:
  dataAccessScopeTest:
    type: gcp:chronicle:DataAccessScope
    name: data_access_scope_test
    properties:
      location: us
      instance: 00000000-0000-0000-0000-000000000000
      dataAccessScopeId: scope-name
      description: scope-description
      allowedDataAccessLabels:
        - logType: GCP_CLOUDAUDIT
  example:
    type: gcp:chronicle:Rule
    properties:
      location: us
      instance: 00000000-0000-0000-0000-000000000000
      scope: ${googleChronicleDataAccessScope.dataAccessScopeTest.name}
      text: |
        rule test_rule { meta: events:  $userid = $e.principal.user.userid  match: $userid over 10m condition: $e }        

The scope property binds the rule to a DataAccessScope resource, which defines allowed log types through allowedDataAccessLabels. In this configuration, the rule only evaluates GCP_CLOUDAUDIT logs. Chronicle validates that any reference lists used in the rule are compatible with both the user’s scope and the rule’s scope.

Beyond these examples

These snippets focus on specific rule-level features: YARA-L rule definition and compilation, deletion policies for rules with detections, and data access scopes for log filtering. They’re intentionally minimal rather than full detection engineering workflows.

The examples require pre-existing infrastructure such as a Chronicle instance (customer ID) and data access scopes for scoped rules. They focus on rule configuration rather than provisioning the Chronicle environment.

To keep things focused, common rule patterns are omitted, including:

  • Rule metadata extraction (author, displayName, severity)
  • Compilation diagnostics and error handling
  • Run frequency configuration (allowedRunFrequencies)
  • Reference lists and data tables in rules
  • Etag-based optimistic concurrency control

These omissions are intentional: the goal is to illustrate how each rule feature is wired, not provide drop-in detection modules. See the Chronicle Rule resource reference for all available configuration options.

Let's create GCP Chronicle Detection Rules

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Rule Lifecycle & Deletion
Why can't I delete my rule?
If deletionPolicy is set to DEFAULT (or not specified), deletion fails if the rule has associated retrohunts (including completed ones) or detections. Use deletionPolicy: "FORCE" to delete rules with these associations.
What are the deletion policy options and what do they delete?

You have two options:

  • DEFAULT - Deletes the rule only if no retrohunts or detections exist
  • FORCE - Deletes the rule along with all associated retrohunts and detections

Both options always delete the rule deployment.

How do I force delete a rule that has detections or retrohunts?
Set deletionPolicy to “FORCE” when creating the rule. This allows deletion even when retrohunts or detections exist.
Rule Updates & Immutability
Why am I getting ABORTED errors when updating my rule?
Updates require the etag to match the server’s current value. If the etag is stale or mismatched, the update fails with an ABORTED error. Pulumi typically handles etag management automatically.
What properties can't I change after creating a rule?
The instance, location, and project properties are immutable. Changing any of these requires recreating the rule.
What happens when I update the rule text?
A new revision is created automatically whenever the text property changes. The revisionId follows the format v_{10 digits}_{9 digits} and the revisionCreateTime is updated.
Data Access & Scoping
How does the scope property affect my rule?
The scope binds a DataAccessScope to your rule. If your rule uses reference lists, Chronicle validates that those lists are compatible with both the user’s scope and the rule’s scope.
How do I bind a rule to a specific data access scope?
Set the scope property to the DataAccessScope resource name in the format: “projects/{project}/locations/{location}/instances/{instance}/dataAccessScopes/{scope}”.
Rule Compilation
How do I check if my rule compiled successfully?
Check the compilationState output property. It will be SUCCEEDED or FAILED. For failures, review compilationDiagnostics for specific errors and warnings.

Using a different cloud?

Explore security guides for other cloud providers: