Time: ~2 hours · Difficulty: Beginner · You need: a Datadog organisation you may send test data to and create rules in (Datadog offers a 14-day free trial of its whole product suite), an API (application programming interface) key, and a terminal with curl and jq. Terraform is optional (step 9).
How this page was checked: Cloud SIEM runs only as a hosted service and no Datadog account was used, so none of the searches or rules on this page were executed in Datadog. Every operator and setting was checked against Datadog's documentation and the CloudTrail rules Datadog publishes (both as of 27 September 2026), and the results quoted are what those documented rules give on the lab file, worked out by a local script, not captured from Datadog. What did run: the shell step, against a local test receiver in place of Datadog's intake, and terraform validate on every rule with Datadog's official Terraform provider 4.22.0.
A search that selects, a rule that counts
Datadog is a hosted monitoring platform, and Cloud SIEM is its security product: it runs detection rules over the logs you send to Datadog and records a security signal whenever a rule's conditions are met. Datadog ships a large catalogue of out-of-the-box rules, and detection engineers add custom ones in the rule editor, or as code with Terraform or Datadog's API.
The distinction that matters: a rule has two halves. The query is a Log Explorer search, and a search only selects logs; it cannot count them. Everything else is a setting of the rule: how to group the matches, how many is too many, over how long, and how severe that is. "Five refusals from one address in ten minutes" is a search for refusals, a group by on the address, a case of a >= 5 and a ten-minute window.
And the search runs on processed logs. Datadog's CloudTrail pipeline maps several CloudTrail fields onto Datadog's standard attributes, and Datadog's own rules search those: the event name as @evt.name, the error code as @error.kind. A query written from the field list in AWS (Amazon Web Services) documentation is a guess about what the pipeline kept; one written from a processed log is not.
On this page
Searches, attributes and the renamed fields
A search is a list of terms, and a log must match every term (AND is implied between them). A term is usually name:value. A few reserved attributes, such as source, service and host, are written bare; every other attribute takes an @ in front. Tags, such as env:lab, are written bare too.
source:cloudtrail @evt.name:StopLogging
Read it as: logs whose source is cloudtrail, and whose attribute evt.name is exactly StopLogging. Against the lab, one log. The value match is exact and case sensitive, and a dot in the name walks into nested JSON (JavaScript Object Notation), so @userIdentity.arn is the arn inside userIdentity.
But CloudTrail calls that field eventName. When logs arrive with the source cloudtrail, Datadog installs its CloudTrail pipeline, which maps a few fields onto standard attributes, names Datadog's other integrations use too. Datadog's own CloudTrail rules show where they land:
| CloudTrail field | Search it as | Searched by Datadog's rule |
|---|---|---|
eventName | @evt.name | AWS CloudTrail configuration modified |
sourceIPAddress | @network.client.ip | AWS root account activity |
errorCode | @error.kind | Enumeration from multiple regions using a long-term access key failed |
userIdentity.arn, requestParameters.*, responseElements.*, eventSource | unchanged, with @ | Failed attempt to elevate privileges via multiple AWS roles, Potential brute force attack on AWS ConsoleLogin |
additionalEventData.MFAUsed | @additionalEventData.MFAUsed, assumed | none: check a sign-in log |
Whether the original names survive beside the new ones is not documented. A remapper deletes the field it copies unless told to keep it, yet two of Datadog's rule pages describe their match with @eventName. This page writes the standard names, as Datadog's rules almost always do, and when a search finds nothing, the first check is one processed log's side panel.
Walk-through
The lab is 24 AWS CloudTrail events. A continuous integration user's access key, build-bot, has leaked. From 203.0.113.50 someone checks whose key it is, tries to list Identity and Access Management (IAM) users, EC2 (Elastic Compute Cloud) resources and secrets and is mostly refused, then creates a user, backup-svc, gives it administrator rights and an access key, and uses it to stop CloudTrail logging. Around that sits ordinary work, including a sign-in without multi-factor authentication (MFA), a failed sign-in and an application refused one file three times. The page builds five detections:
- D1, trail tampering: CloudTrail logging stopped, deleted or changed.
- D2, denied-call burst: one source address refused five or more times.
- D3, new user given a key: a user created and handed an access key within 10 minutes.
- D4, console login without MFA: a successful console sign-in without multi-factor authentication.
- D5, administrator policy attached:
AdministratorAccessattached to a user.
1. Get an organisation you can test in
Use a trial or a non-production organisation, never one whose signals reach an on-call rota. Note your Datadog site (such as datadoghq.com or datadoghq.eu), and create an API key in your organisation settings, on the API keys tab. Cloud SIEM analyses only the logs its filters select, so make sure it takes source:cloudtrail; Datadog's guide to choosing the logs Cloud SIEM analyses shows the switch for your edition.
2. Load the lab data
mkdir -p ~/datadog-lab && cd ~/datadog-lab
cat > cloudtrail-lab.jsonl <<'EOF'
{"eventTime":"2026-09-15T08:55:12Z","eventSource":"signin.amazonaws.com","eventName":"ConsoleLogin","awsRegion":"us-east-1","sourceIPAddress":"198.51.100.20","userAgent":"Mozilla/5.0","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/alice","userName":"alice"},"responseElements":{"ConsoleLogin":"Success"},"additionalEventData":{"MFAUsed":"Yes"}}
{"eventTime":"2026-09-15T09:02:40Z","eventSource":"iam.amazonaws.com","eventName":"CreateUser","awsRegion":"us-east-1","sourceIPAddress":"198.51.100.20","userAgent":"AWS Internal","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/alice","userName":"alice"},"requestParameters":{"userName":"svc-reporting"},"readOnly":false}
{"eventTime":"2026-09-15T09:03:05Z","eventSource":"s3.amazonaws.com","eventName":"ListBuckets","awsRegion":"us-east-1","sourceIPAddress":"198.51.100.20","userAgent":"AWS Internal","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/alice","userName":"alice"},"readOnly":true}
{"eventTime":"2026-09-15T09:10:22Z","eventSource":"ec2.amazonaws.com","eventName":"DescribeInstances","awsRegion":"us-east-1","sourceIPAddress":"192.0.2.10","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"AssumedRole","arn":"arn:aws:sts::111122223333:assumed-role/ci-deploy/runner-4821"},"readOnly":true}
{"eventTime":"2026-09-15T09:15:47Z","eventSource":"signin.amazonaws.com","eventName":"ConsoleLogin","awsRegion":"us-east-1","sourceIPAddress":"198.51.100.21","userAgent":"Mozilla/5.0","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/bob","userName":"bob"},"responseElements":{"ConsoleLogin":"Success"},"additionalEventData":{"MFAUsed":"No"}}
{"eventTime":"2026-09-15T09:20:03Z","eventSource":"signin.amazonaws.com","eventName":"ConsoleLogin","awsRegion":"us-east-1","sourceIPAddress":"192.0.2.77","userAgent":"Mozilla/5.0","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/alice","userName":"alice"},"errorMessage":"Failed authentication","responseElements":{"ConsoleLogin":"Failure"},"additionalEventData":{"MFAUsed":"No"}}
{"eventTime":"2026-09-15T09:31:10Z","eventSource":"s3.amazonaws.com","eventName":"GetObject","awsRegion":"us-east-1","sourceIPAddress":"192.0.2.20","userAgent":"Boto3/1.35.10","userIdentity":{"type":"AssumedRole","arn":"arn:aws:sts::111122223333:assumed-role/reports-app/i-0a12b34c56d78e90f"},"errorCode":"AccessDenied","requestParameters":{"bucketName":"finance-reports","key":"2026/09/summary.csv"},"readOnly":true}
{"eventTime":"2026-09-15T09:47:55Z","eventSource":"s3.amazonaws.com","eventName":"GetObject","awsRegion":"us-east-1","sourceIPAddress":"192.0.2.20","userAgent":"Boto3/1.35.10","userIdentity":{"type":"AssumedRole","arn":"arn:aws:sts::111122223333:assumed-role/reports-app/i-0a12b34c56d78e90f"},"errorCode":"AccessDenied","requestParameters":{"bucketName":"finance-reports","key":"2026/09/summary.csv"},"readOnly":true}
{"eventTime":"2026-09-15T10:01:30Z","eventSource":"s3.amazonaws.com","eventName":"GetObject","awsRegion":"us-east-1","sourceIPAddress":"192.0.2.20","userAgent":"Boto3/1.35.10","userIdentity":{"type":"AssumedRole","arn":"arn:aws:sts::111122223333:assumed-role/reports-app/i-0a12b34c56d78e90f"},"errorCode":"AccessDenied","requestParameters":{"bucketName":"finance-reports","key":"2026/09/summary.csv"},"readOnly":true}
{"eventTime":"2026-09-15T10:02:05Z","eventSource":"sts.amazonaws.com","eventName":"GetCallerIdentity","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"readOnly":true}
{"eventTime":"2026-09-15T10:02:31Z","eventSource":"iam.amazonaws.com","eventName":"ListUsers","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"errorCode":"AccessDenied","readOnly":true}
{"eventTime":"2026-09-15T10:02:38Z","eventSource":"iam.amazonaws.com","eventName":"ListRoles","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"errorCode":"AccessDenied","readOnly":true}
{"eventTime":"2026-09-15T10:02:52Z","eventSource":"iam.amazonaws.com","eventName":"GetAccountAuthorizationDetails","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"errorCode":"AccessDenied","readOnly":true}
{"eventTime":"2026-09-15T10:03:05Z","eventSource":"ec2.amazonaws.com","eventName":"DescribeInstances","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"errorCode":"Client.UnauthorizedOperation","readOnly":true}
{"eventTime":"2026-09-15T10:03:09Z","eventSource":"ec2.amazonaws.com","eventName":"DescribeVpcs","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"errorCode":"Client.UnauthorizedOperation","readOnly":true}
{"eventTime":"2026-09-15T10:03:14Z","eventSource":"ec2.amazonaws.com","eventName":"DescribeSecurityGroups","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"errorCode":"Client.UnauthorizedOperation","readOnly":true}
{"eventTime":"2026-09-15T10:03:22Z","eventSource":"secretsmanager.amazonaws.com","eventName":"ListSecrets","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"errorCode":"AccessDenied","readOnly":true}
{"eventTime":"2026-09-15T10:03:40Z","eventSource":"s3.amazonaws.com","eventName":"ListBuckets","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"readOnly":true}
{"eventTime":"2026-09-15T10:06:12Z","eventSource":"iam.amazonaws.com","eventName":"CreateUser","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"requestParameters":{"userName":"backup-svc"},"readOnly":false}
{"eventTime":"2026-09-15T10:06:30Z","eventSource":"iam.amazonaws.com","eventName":"AttachUserPolicy","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"requestParameters":{"userName":"backup-svc","policyArn":"arn:aws:iam::aws:policy/AdministratorAccess"},"readOnly":false}
{"eventTime":"2026-09-15T10:06:51Z","eventSource":"iam.amazonaws.com","eventName":"CreateAccessKey","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/build-bot","userName":"build-bot"},"requestParameters":{"userName":"backup-svc"},"responseElements":{"accessKey":{"userName":"backup-svc","accessKeyId":"AKIAIOSFODNN7EXAMPLE","status":"Active"}},"readOnly":false}
{"eventTime":"2026-09-15T10:09:03Z","eventSource":"cloudtrail.amazonaws.com","eventName":"StopLogging","awsRegion":"us-east-1","sourceIPAddress":"203.0.113.50","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/backup-svc","userName":"backup-svc"},"requestParameters":{"name":"arn:aws:cloudtrail:us-east-1:111122223333:trail/org-trail"},"readOnly":false}
{"eventTime":"2026-09-15T10:40:18Z","eventSource":"ec2.amazonaws.com","eventName":"DescribeInstances","awsRegion":"us-east-1","sourceIPAddress":"192.0.2.10","userAgent":"aws-cli/2.17.40","userIdentity":{"type":"AssumedRole","arn":"arn:aws:sts::111122223333:assumed-role/ci-deploy/runner-4821"},"readOnly":true}
{"eventTime":"2026-09-15T11:40:26Z","eventSource":"iam.amazonaws.com","eventName":"CreateAccessKey","awsRegion":"us-east-1","sourceIPAddress":"198.51.100.20","userAgent":"AWS Internal","userIdentity":{"type":"IAMUser","arn":"arn:aws:iam::111122223333:user/alice","userName":"alice"},"requestParameters":{"userName":"svc-reporting"},"responseElements":{"accessKey":{"userName":"svc-reporting","accessKeyId":"AKIAI44QH8DHBEXAMPLE","status":"Active"}},"readOnly":false}
EOF
shasum -a 256 cloudtrail-lab.jsonl
jq -c -s '(map(.eventTime | fromdateiso8601) | max) as $last
| (now - 300 - $last | floor) as $shift
| map((.eventTime |= (fromdateiso8601 + $shift | todateiso8601))
+ {ddsource: "cloudtrail", service: "cloudtrail", ddtags: "env:lab", message: .eventName})' \
cloudtrail-lab.jsonl > upload.jsonThe hash should be 35b7c80582d0650ec4850acf84f84fa4b8e5d37169ac3f7276c76f29b67c3319. The jq command moves the lab so its last event is five minutes ago, keeping every gap between events, and adds a source, a tag and the message field Datadog's intake requires. Next, set your site (datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ap1.datadoghq.com and so on) and read your API key into a variable: after the read line, paste the key and press Enter. Nothing is echoed, and the key stays out of your shell history.
DD_SITE=datadoghq.com read -rs DD_API_KEY
curl -sS -o /dev/null -w '%{http_code}\n' -X POST "https://http-intake.logs.$DD_SITE/api/v2/logs" \
-H 'Content-Type: application/json' -H "DD-API-KEY: $DD_API_KEY" --data-binary @upload.jsonExpect 202. ddsource becomes each log's source, which is what makes Datadog install its CloudTrail pipeline (step 3 checks that it ran); every other field arrives as an attribute of the log.
202 means accepted for processing, not stored. Datadog takes each log's date from eventTime and rejects any log dated more than 18 hours ago, and the lab's own dates are long past. That is why the jq step moves it. Count what arrived before you trust anything else.
3. Find the logs and read their names
source:cloudtrail env:lab
Set Log Explorer's time range to cover the past 4 hours. Expect 24 logs, their event names in the content column; if your organisation already receives CloudTrail, keep env:lab in every search below. Then two controls. Add @evt.name:*, which matches logs that have the attribute: you should still see 24. Fewer means the pipeline did not process them (Datadog installs it when the first CloudTrail log arrives): check the Pipelines page, upload again with a new tag such as env:lab2, and use that tag. If your edition uses Security Filters, Datadog tags each log datadog.cloud_siem:true or false; with datadog.cloud_siem:true added you should still see 24, or no rule will ever see the lab. Open the StopLogging log and compare its side panel with the table above.
4. Filter on one attribute: trail tampering (D1)
source:cloudtrail @evt.name:(StopLogging OR DeleteTrail OR UpdateTrail OR PutEventSelectors)
Expect one log: StopLogging by backup-svc from 203.0.113.50, the lab's 10:09:03 event (Datadog shows it at the shifted time). The other 23 are the negative case. Parentheses list alternatives for one attribute, OR must be in capitals, and a - in front of a term excludes: -@evt.name:DescribeInstances keeps the 21 others. Values are exact and case sensitive, so @evt.name:stoplogging finds nothing, while * stands for any run of characters: @evt.name:Describe* finds 5. A value containing a colon, hyphen or space needs double quotes, as ARNs (Amazon Resource Names) do: @userIdentity.arn:"arn:aws:iam::111122223333:user/build-bot" finds 12. Numeric attributes also take ranges such as [400 TO 499] once they are facets, and CIDR(@network.client.ip, 203.0.113.0/24) matches an address range (13 logs here), though Datadog's list of places that function works leaves out detection rules.
5. Nested attributes: console sign-in without MFA (D4)
source:cloudtrail @additionalEventData.MFAUsed:No
Expect 2 logs, and only one is D4. bob signed in from 198.51.100.21 without MFA; the other is the failed attempt on alice from 192.0.2.77, because failed sign-ins carry MFAUsed No as well. Require success too:
source:cloudtrail @evt.name:ConsoleLogin @responseElements.ConsoleLogin:Success @additionalEventData.MFAUsed:No
Expect bob only. AWS's own check for console sign-ins without MFA, Security Hub control CloudWatch.3, tests the same pair of fields: MFAUsed not Yes, and ConsoleLogin Success. No Datadog rule page names additionalEventData, though, so its path in Datadog is the one on this page no Datadog source shows; if the search finds nothing, open a sign-in log and read what the field is called there. As a rule, D4 needs only this query and a case of a > 0 (step 9 builds one like it), and its technique in MITRE ATT&CK (Adversarial Tactics, Techniques, and Common Knowledge) v19.2 is T1078.004 Valid Accounts: Cloud Accounts.
6. Counting lives in the rule: the burst that never fires (D2)
A rule turns a search into a count with three settings. The group by splits the matching logs by an attribute, and each value gets its own count and its own signal. The case is a condition on that count, written with the query's label: the first query is a, the second b. The evaluation window is how far back each count looks. Before building the rule, look at the counts it would see: in Log Explorer, show this search as a Top List grouped by @network.client.ip (if it is not offered, create a facet for it from a log's side panel).
source:cloudtrail @error.kind:AccessDenied
Expect 203.0.113.50 with 4 and 192.0.2.20 with 3. A rule with this query and a case of a >= 5 never fires. Did nothing happen, or can the query not see what happened? Run the control: every error code that exists, grouped by @error.kind.
source:cloudtrail @error.kind:*
Expect AccessDenied 7 and Client.UnauthorizedOperation 3. EC2 reports a refusal as Client.UnauthorizedOperation, so three of the attacker's seven refusals were invisible to the rule. AWS's own check for unauthorized calls, Security Hub control CloudWatch.2, counts every code that starts with AccessDenied or ends with UnauthorizedOperation, which also takes in AccessDeniedException and a bare UnauthorizedOperation, so the rule lists all four, here as Terraform (step 9 checks it):
resource "datadog_security_monitoring_rule" "d2_denied_burst" {
name = "CloudTrail calls refused 5 or more times from one address"
message = "{{@network.client.ip}} was refused 5 or more times in 10 minutes. Review every call from that address in the last hour."
query {
query = "source:cloudtrail @error.kind:(AccessDenied OR AccessDeniedException OR Client.UnauthorizedOperation OR UnauthorizedOperation)"
group_by_fields = ["@network.client.ip"]
}
case {
status = "medium"
condition = "a >= 5"
}
options {
evaluation_window = 600
keep_alive = 3600
max_signal_duration = 86400
}
tags = ["source:cloudtrail", "tactic:TA0007-discovery", "technique:T1580-cloud-infrastructure-discovery", "mitre_technique:T1087.004"]
}Expect one signal, for 203.0.113.50: 7 refusals in 51 seconds. 192.0.2.20 is the negative case, with 3 refusals spread over half an hour and never more than 1 in any 10 minutes. In the rule editor the same settings are the query, its group by, the condition under Set conditions, and the windows under the rule's other parameters.
A query that returns nothing and a query that cannot work return the same thing. Before you trust a rule that never fires, run a control that must find something: the search without its threshold, or a count of the values that really exist.
7. Time: evaluation window, keep alive, maximum duration
The evaluation window slides: at every moment the count covers the last 600 seconds, so a burst is counted whole wherever it falls on the clock. Once a signal exists, new matches keep updating it while they arrive within the keep alive window, and it closes at the maximum signal duration, counted from its first match. Datadog allows only listed values (such as 0, 60, 300, 600 and 3600 seconds), and the evaluation window may not exceed either of the other two. Widening the window is not a fix for a blind query: on the lab, even an hour-long window gives the AccessDenied-only rule 4 for the attacker.
8. One user, two events, in order (D3)
Threshold, the method so far, is one of several, and each rule has exactly one. Datadog's page on creating a custom rule lists seven: Threshold; New Value, for a value not seen during a learning period; Anomaly, for a spike above a learned baseline; Content Anomaly, for field content unlike the usual; Impossible Travel, for two sign-ins too far apart for the time between them; Third Party, for turning another tool's alerts into signals; and Sequence, for ordered steps. The rules overview lists Signal correlation, which combines signals from other rules, in place of Sequence; its old page now leads to the same creation page, where a sequence step can match signals or rules as well as logs.
The Sequence detection method links steps: each step is a condition on one query, and each transition says how long the next step may take. The group by is what makes both events belong to the same new user.
resource "datadog_security_monitoring_rule" "d3_new_user_key" {
name = "New IAM user given an access key within 10 minutes"
message = "{{@requestParameters.userName}} was created and given an access key within 10 minutes. Check who created it and what the key has done since."
query {
query = "source:cloudtrail @evt.name:CreateUser"
group_by_fields = ["@requestParameters.userName"]
}
query {
query = "source:cloudtrail @evt.name:CreateAccessKey"
group_by_fields = ["@requestParameters.userName"]
}
case {
status = "high"
condition = "key_created > 0"
}
options {
detection_method = "sequence_detection"
evaluation_window = 0
keep_alive = 3600
max_signal_duration = 86400
sequence_detection_options {
steps {
name = "user_created"
condition = "a > 0"
evaluation_window = 60
}
steps {
name = "key_created"
condition = "b > 0"
evaluation_window = 60
}
step_transitions {
parent = "user_created"
child = "key_created"
evaluation_window = 600 # the "within 10 minutes"
}
}
}
tags = ["source:cloudtrail", "tactic:TA0003-persistence", "technique:T1136-create-account", "mitre_technique:T1136.003", "mitre_technique:T1098.001"]
}Expect one signal, for backup-svc: created at 10:06:12 lab time and keyed 39 seconds later, both by build-bot. The control: set the transition's window to 10800 (3 hours) and svc-reporting appears too, alice's routine work, keyed 2 hours 37 minutes after she created it. The window is the detection. The rule editor can also join two queries with a Then condition ("query A occurs and then query B occurs") on a threshold rule, and Datadog ships a close cousin of D3, AWS IAM User created with AdministratorAccess policy attached, whose documented logic matches backup-svc here too.
9. Keep the rules as code
Put this in main.tf with the rules from steps 6 and 8. It turns D1's search into a rule: one query, a case of a > 0, so a single matching log is enough. With your keys set, terraform apply creates the three rules; the rule editor builds the same thing by hand.
terraform {
required_providers {
datadog = {
source = "DataDog/datadog"
version = "~> 4.22"
}
}
}
# Keys come from DD_API_KEY and DD_APP_KEY (and DD_HOST outside the US1 site).
provider "datadog" {}
resource "datadog_security_monitoring_rule" "d1_trail_tampering" {
name = "CloudTrail logging stopped, deleted or changed"
message = "{{@userIdentity.arn}} called {{@evt.name}} from {{@network.client.ip}}. If nobody planned it, restore logging and rotate that identity's keys."
query {
query = "source:cloudtrail @evt.name:(StopLogging OR DeleteTrail OR UpdateTrail OR PutEventSelectors)"
group_by_fields = ["@userIdentity.arn"]
}
case {
status = "high"
condition = "a > 0"
}
options {
evaluation_window = 0
keep_alive = 3600
max_signal_duration = 86400
}
tags = [
"source:cloudtrail",
"tactic:TA0005-defense-evasion", # the pre-v19 pair Datadog's own rules use
"technique:T1562-impair-defenses",
"mitre_technique:T1685.002", # the ATT&CK v19.2 technique
]
}terraform init terraform validate
Success! The configuration is valid.
Datadog's ATT&CK map lists a custom rule only when its tactic: and technique: tags form a correct pair in the ATT&CK version the map states, and Datadog's own CloudTrail tamper rules still carry this pre-v19 pair. In ATT&CK v19.2, T1562 is revoked: stopping CloudTrail is T1685.002 Disable or Modify Cloud Log under the new Defense Impairment tactic (TA0112), and TA0005 is now called Stealth. The free-form mitre_technique tag keeps the current ID; check the version on the map page before changing the pair. terraform validate checks the file's shape and allowed values, not the search or the condition: a >>= 5 and a keep alive shorter than the window both pass it. Add validate = true to a rule and terraform plan asks Datadog to check it, which needs your keys.
10. Test each rule both ways
For the single-log rules, use Unit Test in the rule editor: copy the StopLogging log from Log Explorer (the export button in its side panel), paste it, mark it expected to match, and run; then paste the ListBuckets log marked expected not to match. Copy processed logs, not lines from the lab file: the rule matches evt.name, which only the processed log has. For D2 and D3, open the rule's menu and choose Run as Historical Job over the past 4 hours (not offered on Datadog's government sites). Expect results for 203.0.113.50 and backup-svc, and none for 192.0.2.20 or svc-reporting. Real-time rules analyse logs as they arrive, and Datadog does not document how they treat a batch dated hours back, so the historical job is the dependable test here.
Hands-on exercises
Write the search for D5, AdministratorAccess attached to a user, and say what the rule around it needs.
Show the answer
source:cloudtrail @evt.name:AttachUserPolicy @requestParameters.policyArn:"arn:aws:iam::aws:policy/AdministratorAccess"
Expect one log: build-bot attaching the policy to backup-svc from 203.0.113.50. The ARN needs double quotes because it contains colons. The rule is D1's shape: one query and a case of a > 0. In the log, requestParameters.userName is the target and userIdentity.arn the actor; both are worth putting in the message. Datadog's rule for this, AWS IAM AdministratorAccess policy was applied to a user, is tagged TA0004 and T1098; the v19.2 sub-technique is T1098.003 Additional Cloud Roles.
This search finds nothing, although the lab contains a sign-in without MFA. Why, and how do you prove it rather than guess?
source:cloudtrail @evt.name:ConsoleLogin @additionalEventData.MFAUsed:no
Show the answer
Attribute values are case sensitive, and CloudTrail writes No, not no. To prove it, drop the term that decides and group what is left by the field in question:
source:cloudtrail @evt.name:ConsoleLogin
Grouped by @additionalEventData.MFAUsed, expect No 2 and Yes 1. Seeing the real values moves the question from "is there data?" to "what exactly is it called?", which is the question an empty result never answers.
One source address used two different identities. Write a rule that finds any address used by more than one identity within 10 minutes. Which address fires?
Show the answer
resource "datadog_security_monitoring_rule" "ex3_many_identities" {
name = "One address used by more than one identity"
message = "{{@network.client.ip}} acted as more than one identity within 10 minutes."
query {
query = "source:cloudtrail"
aggregation = "cardinality"
distinct_fields = ["@userIdentity.arn"]
group_by_fields = ["@network.client.ip"]
}
case {
status = "low"
condition = "a > 1"
}
options {
evaluation_window = 600
max_signal_duration = 86400
}
}cardinality counts distinct values (the editor's Count setting). Expect 203.0.113.50 only, with 2: the leaked build-bot key and the backup-svc user it created. Count the ARN, which every event has, and never group by userIdentity.userName: roles have none, and a rule ignores logs that lack a group-by value unless optional group by is on.
Datadog ships a rule called AWS console login without MFA. Its page says it matches when @userIdentity.sessionContext.attributes.mfaAuthenticated is false. Would it catch bob's sign-in in this lab?
Show the answer
No, going by its page. The lab's sign-in events carry additionalEventData.MFAUsed and no sessionContext at all, so the attribute that rule tests is missing from every one of them. MFAUsed is also the field AWS's own Security Hub control CloudWatch.3 reads for this case. CloudTrail can describe MFA in more than one field, and a rule written for one of them is blind to events that carry the other. Before relying on any rule, yours or a vendor's, check that its fields exist on your own events: search for the field with :* and confirm it finds something.
Common mistakes
- Guessing field names from AWS's documentation. Datadog's rules search
@evt.name,@network.client.ipand@error.kind, and whethereventName,sourceIPAddressanderrorCodesurvive beside them is not documented. Read a processed log first. - Looking for a count in the search. The search only selects. Grouping, thresholds and windows are rule settings.
- Counting only
AccessDenied. EC2 records refusals asClient.UnauthorizedOperation. List the error codes that exist before choosing which to count. - Grouping by a field some logs lack. Logs without the group-by value are ignored unless optional group by is on, and the missing rows look like quiet.
- Changing case or dropping quotes.
stoploggingis notStopLogging, and values with colons, hyphens or spaces need double quotes. - Loading old events. Datadog rejects logs dated more than 18 hours back, and a 202 only means the request was accepted for processing.
- Taking
terraform validateas a test of the rule. It checks the file, not the search, the condition or the result. - Assuming the ATT&CK tags match the current release. The map reads the version it states, and Datadog's rules still use pre-v19 IDs.
- Testing only the rule that should fire. Every rule here has a negative case in the lab; run it.
Where next
- Splunk SPL, KQL and Sumo Logic run the same lab and the same five detections, a quick way to see which ideas are Datadog's and which are universal.
- Sigma and YARA for writing a detection once and converting it, and jq and JMESPath for the
jqin step 2. - The CloudTrail to SIEM lab for a real CloudTrail feed, and detection engineering for testing, tuning and retiring rules.
- Datadog's log search syntax, its guide to creating a custom rule and the Terraform resource reference.
- Datadog's catalogue of out-of-the-box rules: each page states what its rule matches and which fields it uses.
