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 FREEFrequently Asked Questions
Rule Lifecycle & Deletion
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.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.
deletionPolicy to “FORCE” when creating the rule. This allows deletion even when retrohunts or detections exist.Rule Updates & Immutability
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.instance, location, and project properties are immutable. Changing any of these requires recreating the rule.text property changes. The revisionId follows the format v_{10 digits}_{9 digits} and the revisionCreateTime is updated.Data Access & Scoping
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.scope property to the DataAccessScope resource name in the format: “projects/{project}/locations/{location}/instances/{instance}/dataAccessScopes/{scope}”.Rule Compilation
compilationState output property. It will be SUCCEEDED or FAILED. For failures, review compilationDiagnostics for specific errors and warnings.