Scenarios
Defining step-by-step event generation sequences.
A scenario is the core building block in Tracemill. It defines a stateful sequence of event emissions — a single "story" of activity, like a brute-force attack or a user login session.
Structure
A scenario YAML file has three main sections:
type: scenario
state:
source_ip: gen.ipv4()
user_id: gen.uuid()
steps:
- emit:
type: "console-login@1.0"
fields:
src_ip: ref.source_ip
user: ref.user_id
outcome: "failure"
- emit:
type: "console-login@1.0"
fields:
src_ip: ref.source_ip
user: ref.user_id
outcome: "success"State
The state block declares variables resolved once per scenario run. Variables can reference generators (gen.*) or literal values. They are resolved in topological order — a variable can reference another variable declared in the same block via ref.*.
Steps
Each step is one of:
emit— emit an event of a given type. Thefieldsblock maps field names to values — literals, generator calls, or references to state variables.wait— pause for aduration(with optionaljitter_pct).foreach— iterate a collection, running a nested step sequence once per element.
Looping with foreach
A foreach step lets a scenario define its own repetition, so a scenario named for a burst of activity actually produces that burst when run standalone — without relying on a job to loop it.
state:
actions:
- { event_name: DescribeInstances, event_source: ec2.amazonaws.com }
- { event_name: ListBuckets, event_source: s3.amazonaws.com }
steps:
- foreach:
in: ref.actions # required — a ref.<var> pointing at a collection; never an inline list
as: action # required — a dot-free name; the per-iteration binding
steps: # required (≥1) — recursive: emit | wait | foreach
- emit:
event_type: aws.cloudtrail@v1
fields:
eventName: ref.action.event_name
eventSource: ref.action.event_source
- wait: { duration: 2s, jitter_pct: 0.3 }inmust be aref.<var>[.<path>]— a reference to a scenario state variable (or an enclosing iteration variable, for nested loops). It is never an inline literal list, so a job can override the collection through the normal binding mechanism. Override it with a job-state inline list (actions: ref.action_list) or by binding a whole pool (actions: pool.discovery-actions[]).asis the per-iteration binding. The whole element is bound under this name for the nested steps only — it does not leak across iterations or back into scenario state. A record element is read viaref.<as>.<field>(same-row fields stay correlated); a scalar element is read directly asref.<as>.- Record fields evaluate. Values nested inside a record in
state:parse as ExprStr —event_name: gen.uuid()inside a record draws a fresh UUID at run start, and${ref.region}interpolates against scenario state. Resolution timing matches top-level state: once per scenario run, not per emit (the latter is how emitfields:work). Event-typedefaults:remain the exception: nested values there stay literal. - Nesting is allowed (a
foreachinside aforeach), capped at 5 levels. - Caps: a single collection may have at most 100,000 elements, and a single run may execute at most 1,000,000 total iterations across all (including nested) loops.
Event Types
Event types (e.g., console-login@1.0) are defined separately and provide a schema with default values. Scenario fields override event type defaults. A timestamp field is stamped automatically from the runner's clock.