Skip to content

DetectionRule

A Guardian alert rule: a detection run on a schedule over the tenant's logs, raising alerts. Fields, YAML, HCL and CLI samples, and how apply treats it.

Checked against config-schemas@v1.15.0, terraform-provider-resolve@82cb23c, cli@v4.9.1

A Guardian alert rule: a detection run on a schedule over the tenant’s logs, raising alerts. Name exactly one of body, file, searchRef, countermeasure, sigmahq or ioc. body and file are the tenant’s own rule; the others run a rule Guardian already has, which can be tuned (level, threshold, suppressions, timing) but not edited. A rule whose detection does not compile, or names a log source Guardian has no table for, is stored turned off. The plan caps how many rules run at once. Deleting one that an AlertTrigger still uses is refused.

apiVersion vulnetix.com/v1
kind DetectionRule
Group SIEM
Status Served. The config engine plans and applies it.
Objects per tenant many, keyed by metadata.name
Token and role A vmt_ config management token that lists DetectionRule in its kinds. The owner or admin role writes; the member role reads only. A console session or an ApiKey is refused with kind_not_allowed, because the kind is written in Guardian with the caller’s own token.
Applied after SavedSearch, MessagingConnection (deleted before them)
Terraform resource vulnetix_detection_rule, imported by name (see Terraform and OpenTofu)
CLI vulnetix config apply, CLI v3.165.1 and later.
Pinned schema v1.15.0/DetectionRule.schema.json
Newest schema latest/DetectionRule.schema.json
First released in v1.3.0
Print the built-in copy vulnetix config schema DetectionRule

Minimal

Only the fields the kind requires. From examples/DetectionRule/minimal.yaml.

YAML

# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/DetectionRule.schema.json
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: sigmahq-aws-root-login
spec:
  sigmahq: rules/cloud/aws/cloudtrail/aws_root_account_usage.yml

HCL (resource vulnetix_detection_rule)

# Only the arguments the kind requires. From config-schemas examples/DetectionRule/minimal.yaml.

resource "vulnetix_detection_rule" "sigmahq_aws_root_login" {
  name    = "sigmahq-aws-root-login"
  sigmahq = "rules/cloud/aws/cloudtrail/aws_root_account_usage.yml"
}

CLI (applies it)

export VULNETIX_CONFIG_TOKEN=vmt_...   # a config management token
vulnetix config apply -f - --manager gitops:acme/platform --auto-approve <<'EOF'
# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/DetectionRule.schema.json
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: sigmahq-aws-root-login
spec:
  sigmahq: rules/cloud/aws/cloudtrail/aws_root_account_usage.yml
EOF

Advanced

Every field the kind supports, with a when expression where it takes one. From examples/DetectionRule/advanced.yaml.

YAML

# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/DetectionRule.schema.json
# A rule names exactly one of body, file, searchRef, countermeasure, sigmahq
# or ioc, so each form has its own document.
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: dynamic-dns
  labels:
    team: secops
    mitre-tactic: command-and-control
  annotations:
    runbook: https://wiki.example.com/secops/dynamic-dns
spec:
  displayName: Dynamic DNS lookup
  language: sigma
  body: |
    title: Dynamic DNS lookup
    id: 6f3e0c52-0d4b-4c8e-9a51-2a6b9a0e7d10
    level: medium
    logsource:
      category: dns_query
    detection:
      selection:
        QueryName|endswith:
          - .duckdns.org
          - .no-ip.org
      condition: selection
  enabled: true
  level: medium
  severityOverride: warning
  intervalSec: 120
  latenessSec: 300
  suppressSec: 1800
  maxAlertsPerRun: 50
  threshold:
    count: 5
    windowSec: 600
    by: [src_ip]
  suppressions:
    - field: QueryName
      value: updates.duckdns.org
      until: "2026-12-31"
    - field: src_ip
      value: 10.20.0.15
  target:
    connections: [secops-slack]
    emails: [secops@example.com]
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: brute-force-file
spec:
  displayName: SSH brute force
  language: sigma
  file: siem/rules/brute-force.yml
  enabled: false
  level: high
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: dns-search-rule
spec:
  searchRef: dynamic-dns-lookups
  level: low
  intervalSec: 600
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: log4shell-countermeasure
spec:
  countermeasure: 3b1f8a62-7d2e-4c41-9a0b-5e6f7c8d9e01
  severityOverride: critical
  intervalSec: 300
  target:
    connections: [secops-slack]
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: sigmahq-aws-root-login
spec:
  sigmahq: rules/cloud/aws/cloudtrail/aws_root_account_usage.yml
  level: high
  suppressSec: 3600
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: indicator-domains
spec:
  displayName: Indicator domain resolved
  ioc: ioc-dns
  maxAlertsPerRun: 100
  latenessSec: 120

HCL (resource vulnetix_detection_rule)

# Every argument the kind supports. From config-schemas examples/DetectionRule/advanced.yaml.

resource "vulnetix_detection_rule" "dynamic_dns" {
  name               = "dynamic-dns"
  body               = "title: Dynamic DNS lookup\nid: 6f3e0c52-0d4b-4c8e-9a51-2a6b9a0e7d10\nlevel: medium\nlogsource:\n  category: dns_query\ndetection:\n  selection:\n    QueryName|endswith:\n      - .duckdns.org\n      - .no-ip.org\n  condition: selection\n"
  display_name       = "Dynamic DNS lookup"
  enabled            = true
  interval_sec       = 120
  language           = "sigma"
  lateness_sec       = 300
  level              = "medium"
  max_alerts_per_run = 50
  severity_override  = "warning"
  suppress_sec       = 1800
  suppressions = [
    {
      field = "QueryName"
      until = "2026-12-31"
      value = "updates.duckdns.org"
    },
    {
      field = "src_ip"
      value = "10.20.0.15"
    },
  ]
  target = {
    connections = ["secops-slack"]
    emails      = ["secops@example.com"]
  }
  threshold = {
    by         = ["src_ip"]
    count      = 5
    window_sec = 600
  }
  labels = {
    mitre-tactic = "command-and-control"
    team         = "secops"
  }
  annotations = {
    runbook = "https://wiki.example.com/secops/dynamic-dns"
  }
}

resource "vulnetix_detection_rule" "brute_force_file" {
  name         = "brute-force-file"
  display_name = "SSH brute force"
  enabled      = false
  file         = "siem/rules/brute-force.yml"
  language     = "sigma"
  level        = "high"
}

resource "vulnetix_detection_rule" "dns_search_rule" {
  name         = "dns-search-rule"
  interval_sec = 600
  level        = "low"
  search_ref   = "dynamic-dns-lookups"
}

resource "vulnetix_detection_rule" "log4shell_countermeasure" {
  name              = "log4shell-countermeasure"
  countermeasure    = "3b1f8a62-7d2e-4c41-9a0b-5e6f7c8d9e01"
  interval_sec      = 300
  severity_override = "critical"
  target = {
    connections = ["secops-slack"]
  }
}

resource "vulnetix_detection_rule" "sigmahq_aws_root_login" {
  name         = "sigmahq-aws-root-login"
  level        = "high"
  sigmahq      = "rules/cloud/aws/cloudtrail/aws_root_account_usage.yml"
  suppress_sec = 3600
}

resource "vulnetix_detection_rule" "indicator_domains" {
  name               = "indicator-domains"
  display_name       = "Indicator domain resolved"
  ioc                = "ioc-dns"
  lateness_sec       = 120
  max_alerts_per_run = 100
}

CLI (applies it)

export VULNETIX_CONFIG_TOKEN=vmt_...   # a config management token
vulnetix config apply -f - --manager gitops:acme/platform --auto-approve <<'EOF'
# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/DetectionRule.schema.json
# A rule names exactly one of body, file, searchRef, countermeasure, sigmahq
# or ioc, so each form has its own document.
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: dynamic-dns
  labels:
    team: secops
    mitre-tactic: command-and-control
  annotations:
    runbook: https://wiki.example.com/secops/dynamic-dns
spec:
  displayName: Dynamic DNS lookup
  language: sigma
  body: |
    title: Dynamic DNS lookup
    id: 6f3e0c52-0d4b-4c8e-9a51-2a6b9a0e7d10
    level: medium
    logsource:
      category: dns_query
    detection:
      selection:
        QueryName|endswith:
          - .duckdns.org
          - .no-ip.org
      condition: selection
  enabled: true
  level: medium
  severityOverride: warning
  intervalSec: 120
  latenessSec: 300
  suppressSec: 1800
  maxAlertsPerRun: 50
  threshold:
    count: 5
    windowSec: 600
    by: [src_ip]
  suppressions:
    - field: QueryName
      value: updates.duckdns.org
      until: "2026-12-31"
    - field: src_ip
      value: 10.20.0.15
  target:
    connections: [secops-slack]
    emails: [secops@example.com]
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: brute-force-file
spec:
  displayName: SSH brute force
  language: sigma
  file: siem/rules/brute-force.yml
  enabled: false
  level: high
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: dns-search-rule
spec:
  searchRef: dynamic-dns-lookups
  level: low
  intervalSec: 600
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: log4shell-countermeasure
spec:
  countermeasure: 3b1f8a62-7d2e-4c41-9a0b-5e6f7c8d9e01
  severityOverride: critical
  intervalSec: 300
  target:
    connections: [secops-slack]
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: sigmahq-aws-root-login
spec:
  sigmahq: rules/cloud/aws/cloudtrail/aws_root_account_usage.yml
  level: high
  suppressSec: 3600
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: indicator-domains
spec:
  displayName: Indicator domain resolved
  ioc: ioc-dns
  maxAlertsPerRun: 100
  latenessSec: 120
EOF

Fields

Paths use dots for nesting and [] for the items of a list. Required means required whenever the parent object is present. A default is what the server uses when the field is left out.

Field Type Required Default Allowed values Description
apiVersion string yes exactly vulnetix.com/v1 The document format version. Every kind in this release is vulnetix.com/v1.
kind string yes exactly DetectionRule Always DetectionRule.
metadata object yes no other keys
metadata.name string yes pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ The object’s name, unique per kind within a tenant. It is the natural key: plan, apply, export and the Terraform provider all address an object by kind and name.
metadata.org string no format uuid The tenant (organization UUID) the document belongs to. When set, plan and apply refuse to run against any other tenant.
metadata.labels object no map of string to string (length 0 to 256); keys match ^[a-z0-9]([a-z0-9._/-]{0,61}[a-z0-9])?$ Free key/value labels. Stored with the object, never interpreted by the server.
metadata.annotations object no map of string to string (length 0 to 4096) Free key/value notes for people and tools, such as a ticket link.
spec object yes no other keys
spec.displayName string no length 1 to 200 The name the console shows. Defaults to the Sigma rule’s title, or the saved search’s, countermeasure’s or built-in rule’s name.
spec.language string no sigma, dsl The language of body or file. sigma: a Sigma rule, or several in one multi-document YAML with one correlation rule, which is the one that alerts. dsl: the Guardian search language. Guardian keeps a DSL rule only as a copy of a saved search today, so a dsl body or file has no direct write path yet; use searchRef until Guardian takes DSL rule text.
spec.body string no length 1 to 262144 The rule text, at most 256 KiB.
spec.file string no length 1 to 1024 A file holding the rule text, the same as body.
spec.searchRef string no pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ The metadata.name of a SavedSearch whose text the rule runs. Its text is copied when the rule is created: editing the search later does not change the rule.
spec.countermeasure string no format uuid A Vulnetix countermeasure of kind SIGMA, by uuid: the virtual patching lane’s detection for a vulnerability.
spec.sigmahq string no pattern ^[A-Za-z0-9_./-]+\.ya?ml$ A rule from the SigmaHQ catalogue Guardian imports weekly, by its path in the SigmaHQ repository. Only rules that compile are offered.
spec.ioc string no ioc-dns, ioc-net A built-in indicator rule. ioc-dns: a domain on an indicator list resolved. ioc-net: a connection to an indicator address.
spec.enabled boolean no true Whether the rule runs. A rule that cannot compile is never on, whatever this says.
spec.level string no informational, low, medium, high, critical The Sigma level. Defaults to the rule’s own level, or medium.
spec.severityOverride string no info, notice, warning, critical The alert severity, instead of the one the level maps to (informational to info, low to notice, medium to warning, high and critical to critical).
spec.intervalSec integer no 60 60 to 3600 Seconds between runs.
spec.latenessSec integer no 300 0 to 3600 How long a run waits for late rows before it reads a window, in seconds.
spec.suppressSec integer no 900 0 to 604800 Seconds during which a repeat of the same alert is merged into the open one instead of raising another.
spec.maxAlertsPerRun integer no 50 1 to 1000 Alerts one run may raise. Beyond it, one overflow alert says how many more matched.
spec.threshold object no no other keys Alert only when at least count rows match within windowSec, counted per the by fields. Wraps the detection as a Sigma event_count correlation.
spec.threshold.count integer yes 1 to 1000000
spec.threshold.windowSec integer yes 60 to 86400
spec.threshold.by array of string no 0 to 5 items; each item: length 1 to 80
spec.suppressions array of object no 0 to 50 items Values that never alert. Each adds a filter on field equal to value to the rule’s detection until it lapses.
spec.suppressions[].field string yes pattern ^[A-Za-z0-9_.-]{1,80}$
spec.suppressions[].value string yes length 1 to 512
spec.suppressions[].until string no format date The last day it applies is the day before this date (UTC). Left out, it never lapses.
spec.target object no no other keys Where the rule’s alerts are sent, beyond the Guardian alerts list.
spec.target.connections array of string no 0 to 10 items; items unique; each item: pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$
spec.target.emails array of string no 0 to 20 items; items unique; each item: format email; length 0 to 254

Rules

Conditions the schema enforces beyond each field’s own type. vulnetix config validate and the engine’s plan report a document that breaks one.

On Rule
spec When : language is required. Otherwise: language is not allowed.
spec Exactly one of body, file, searchRef, countermeasure, sigmahq or ioc is set.

Applying this kind

  • Command. vulnetix config apply -f <file>; the CLI line above says from which release. Plan first with vulnetix config plan: it shows each object as create, update, delete or no change, with a planHash that apply then holds to. Apply writes in one transaction, except for the second phase below.
  • Manager. Each object records the manager that applied it (terraform, console or gitops:<repo>). --prune deletes only objects its own manager owns. See The manager and the config engine.
  • Order. Applied after SavedSearch and MessagingConnection, so a document that refers to one by metadata.name can be in the same apply. Deleted before them.
  • Second phase. This kind is not written inside the apply transaction. The other kinds apply and commit first, then each DetectionRule is written on its own, in Guardian. One that fails reports status.phase: Failed and a message, and nothing is recorded for it. The others stay applied and the apply answers 207. Plan again to see what is left. A single PUT from the Terraform provider answers 502 external_failed instead.
  • Status. A failed second-phase write reports phase Failed with a message. The phases the engine reports are explained under status phases.

How apply treats it

Written in Guardian, in the second phase; see SIEM kinds.

  • Levers, threshold, target, suppressions, level, display name and a Sigma body change in place.
  • The source (searchRef, countermeasure, sigmahq, ioc, or a body versus another form) cannot move. The plan blocks it: declare the new rule under a new name.
  • sigmahq is resolved by path through Guardian’s catalogue.
  • file is refused, because the server never reads a path. Send the text as body.
  • A dsl body is refused, because Guardian keeps a DSL rule only as a copy of a saved search. Use searchRef.
  • A rule Guardian cannot compile stays off whatever enabled says, so enabled is not a change for it and the plan warns with the compile error.
  • displayName and level default to Guardian’s choice, so they are compared only when the document sets them.

Terraform and OpenTofu

The resource is vulnetix_detection_rule in the vulnetix/resolve provider. Its arguments are the spec in snake case, beside name, labels and annotations from metadata. The Minimal and Advanced HCL are above. The provider’s own reference is vulnetix_detection_rule.

Import an object that already exists by its name:

# Import an existing DetectionRule by its name (metadata.name).
terraform import vulnetix_detection_rule.sigmahq_aws_root_login sigmahq-aws-root-login

Install, authentication and how state is read: Terraform and OpenTofu.

More examples

examples/DetectionRule/rules.yaml from Vulnetix/config-schemas:

# yaml-language-server: $schema=https://github.com/Vulnetix/config-schemas/releases/latest/download/DetectionRule.schema.json
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: dynamic-dns
spec:
  language: sigma
  body: |
    title: Dynamic DNS lookup
    id: 6f3e0c52-0d4b-4c8e-9a51-2a6b9a0e7d10
    level: medium
    logsource:
      category: dns_query
    detection:
      selection:
        QueryName|endswith:
          - .duckdns.org
          - .no-ip.org
      condition: selection
  threshold: { count: 5, windowSec: 600, by: [src_ip] }
  suppressions:
    - field: QueryName
      value: updates.duckdns.org
      until: "2026-12-31"
  target:
    connections: [secops-slack]
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: brute-force-file
spec:
  language: sigma
  file: siem/rules/brute-force.yml
  enabled: false
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: log4shell-countermeasure
spec:
  countermeasure: 3b1f8a62-7d2e-4c41-9a0b-5e6f7c8d9e01
  severityOverride: critical
  intervalSec: 300
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: sigmahq-aws-root-login
spec:
  sigmahq: rules/cloud/aws/cloudtrail/aws_root_account_usage.yml
  level: high
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: indicator-domains
spec:
  ioc: ioc-dns
  maxAlertsPerRun: 100
---
apiVersion: vulnetix.com/v1
kind: DetectionRule
metadata:
  name: dns-search-rule
spec:
  searchRef: dynamic-dns-lookups
  level: low

Check a file with vulnetix config validate -f <file>. See Validate documents. In an editor, start the file with the schema comment.

Edit this pageSources: config-schemas/schemas/DetectionRule.schema.json, config-schemas/schemas/common.schema.json, config-schemas/examples/DetectionRule/minimal.yaml, config-schemas/examples/DetectionRule/advanced.yaml, config-schemas/examples/DetectionRule/rules.yaml, terraform-provider-resolve/examples/resources/vulnetix_detection_rule/minimal.tf, terraform-provider-resolve/examples/resources/vulnetix_detection_rule/advanced.tf, vdb-site/docs/platform-iac-engine.md, vdb-site/api/internal/handler/config_engine_kinds.go, vdb-site/api/internal/config/siem_rule.go, vdb-site/api/internal/config/guardian.go
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.