TILLSHIELD · TILLTELL

Rules of your own.

GA

The library covers attacks every workspace faces. Your own rules cover the ones only you know about: the admin who should never act from a script, the repository nobody clones from home. A workspace rule is written in the same format as the library, runs on the same stream, and earns its stage the same way. Write one under Shield → TillTell → Rules → New rule, or copy a library rule and tune it.

01Format

A rule, top to bottom

Rules are YAML in a Sigma-compatible shape. This one is the starting point the editor opens with. It checks, saves and passes its tests as written:

yaml
id: team-admin-from-script
title: Admin change from a scripted client
kind: event
level: medium
description: A workspace role or key changed from curl or a scripting library rather than a browser.
tags:
  - attack.t1098
detection:
  change:
    action:
      - iam.role_changed
      - iam.api_key_created
  scripted:
    actor.user_agent|re|i: ^(curl|wget|python-requests|go-http-client)/
  condition: change and scripted
falsepositives:
  - Your own automation, if it changes roles or keys
tests:
  - name: a role change from curl fires
    record:
      kind: event
      action: iam.role_changed
      actor: { user_agent: curl/8.4.0 }
    expect: match
  - name: the same change from a browser stays quiet
    record:
      kind: event
      action: iam.role_changed
      actor: { user_agent: Mozilla/5.0 }
    expect: no_match
KeyWhat it holds
idLowercase letters, digits and dashes, 3 to 64 characters, starting with a letter. It can’t change once saved. Ids starting with tell- belong to the library, and new, validate and backtest are reserved.
titleUp to 200 characters. Findings carry it.
kindAlways event for a workspace rule: rules read the security event stream.
levelinformational, low, medium, high or critical.
tagsAt least one ATT&CK technique, like attack.t1098. Tactic tags such as attack.persistence are allowed too. Techniques put the rule on the coverage map.
requires_trustserver to ignore events an app SDK reported. Leave it out to accept both.
detectionNamed selections and a condition. See below.
correlationOptional. Fire on a pattern across events instead of one event.
responseOptional suggest_playbook: one of the library playbooks. A live rule offers it with each finding.
testsRequired. At least one that fires and one that stays quiet.
stageOptional. draft saves the rule without running it. Anything else starts in shadow.
description, falsepositives, references, remediationOptional notes shown with the rule. remediation says how to fix what it finds, in up to 2,000 characters.

Sigma metadata such as status, logsource, author and date is accepted and ignored, so a Sigma rule moves over with small changes. Any other key is an error. A version key is ignored with a warning: TillTell numbers each save itself.

02Detection

Selections, fields and modifiers

A selection is a map of field to value, and every field in it must match. A list of maps matches when any of them does. A list of values matches when any value does, unless the field carries |all. Fields are dot paths into the event:

FieldHolds
actionWhat happened, like auth.login_failed, iam.role_changed, secret.reveal or repo.force_push. An action no product records yet is a warning.
outcomesuccess, failure or denied.
sourceThe product that wrote it, like tillauth, tillsecrets, tillforge, tillark or workspace.
trustserver or client.
actor.type, actor.idWho: user, service or unknown, and their id.
actor.ip, actor.country, actor.asn, actor.user_agent, actor.session_idWhere from, when known.
target.type, target.id, target.nameWhat it was done to.
project_idThe project, for project-scoped events.
raw.…Product-specific detail, like raw.role on a role change.

Modifiers

Add modifiers to a field with a pipe: actor.user_agent|startswith. Plain values compare whole and ignore case, and accept Sigma wildcards: * for any run of characters and ? for one.

ModifierMatches when
contains, startswith, endswithThe text holds, begins or ends with the value. Add cased to respect case.
reAn RE2 regex finds a match. Case-sensitive unless you add i. Add m for multi-line anchors and s for a dot that matches newlines.
cidrThe address is inside the range, like 10.0.0.0/8 or 2001:db8::/32.
gt, gte, lt, lteThe number compares as written.
existsThe field is present (true) or absent (false).
allEvery value in the list matches, not just one.

A value of null matches a field that is missing. Text operators and regexes read the first 8,192 characters of a field.

The condition

The condition combines selections with and, or, not and parentheses. 1 of admin_* and all of admin_* match against selections whose names start with admin_, and 1 of them or all of them against every selection. Sigma’s pipe aggregations, like | count() > 5, are not supported: use a correlation block.

Regexes run in linear time
Patterns use RE2 syntax, which never backtracks, so no pattern can stall detection however it’s written. The price is that lookahead, lookbehind and backreferences are refused. A pattern is limited to 512 characters and 1,000 compiled instructions; large counted repeats like {500} cost the most.
03Correlation

Patterns across events

A correlation block makes the rule fire on a pattern instead of a single event. Events that match the detection feed it, grouped by up to five group-by fields, inside a timespan written like 30s, 5m, 12h or 30d. Counting, ordering and travel rules fire once per group per timespan. A first-seen rule fires on each new value.

TypeFires whenAlso takes
event_countThe group’s matching events reach the condition, like { gte: 20 }.condition
value_countThe group shows enough distinct values of field.field, condition
temporalEvery selection in steps has matched for the group, in any order.steps: 2 to 10 selection names
temporal_orderedThe steps matched in the order written.steps
geo_velocityTwo events for the group are further apart than travel allows, compared in km/h.condition, min_distance_km (default 100)
first_seenA value of field the group hasn’t used in the timespan appears, once the group has history.field, min_history (default 3)

Counts only grow as events arrive, so counting conditions take gt, gte or eq. An event missing a group-by field counts toward no one. This rule fires when someone with a history of clones pulls a repository from a new address:

yaml
id: team-clone-new-address
title: Repository cloned from a new address
kind: event
level: medium
description: Someone with a history of clones pulled a repository from an address they have not used in thirty days.
tags: [attack.collection, attack.t1213.003]
requires_trust: server
falsepositives:
  - A new office, home connection or VPN exit
detection:
  clone: { action: repo.clone, outcome: success }
  condition: clone
correlation:
  type: first_seen
  group-by: [actor.id]
  field: actor.ip
  timespan: 30d
  min_history: 5
tests:
  - name: a clone from a new address after a week of usual ones fires
    sequence:
      - record: { action: repo.clone, outcome: success, org_id: o1, trust: server, source: tillforge, actor: { id: u1, ip: 198.51.100.7 }, timestamp: "2026-01-01T08:00:00Z" }
        repeat: 5
        every: 1d
      - record: { action: repo.clone, outcome: success, org_id: o1, trust: server, source: tillforge, actor: { id: u1, ip: 203.0.113.50 }, timestamp: "2026-01-07T08:00:00Z" }
    expect_fires: 1
  - name: clones from the usual address stay quiet
    sequence:
      - record: { action: repo.clone, outcome: success, org_id: o1, trust: server, source: tillforge, actor: { id: u1, ip: 198.51.100.7 }, timestamp: "2026-01-01T08:00:00Z" }
        repeat: 8
        every: 1d
    expect_fires: 0
04Tests

Every rule proves itself

A rule can’t be saved until its tests pass, and it needs at least one test that fires and one that stays quiet. A rule that matches single events tests with a record and expect: match or expect: no_match. A correlation rule tests with a sequence of records replayed in order, each optionally sent repeat times every so far apart, and expect_fires: how many findings the sequence should produce. A record’s timestamp sets where its run starts.

The tests run on every check and every save, inside 2 seconds and 2,000 replayed events. A rule holds at most 50 tests.

05Limits

One workspace can’t slow another

Workspace rules run beside every other workspace’s, so each is held to a budget:

LimitValue
Rules per workspace100
Value comparisons per event, across every field1,000
Compiled regex size, across every pattern2,000
Correlation timespan7 days; 90 for first_seen
Rule text64 KB

The check shows a rule’s cost against these limits before you save. Detection also times each rule as it runs. A rule that spends more than its share matching one batch of events is paused: it stops running, the rules page says why, and owners and admins see it in their notifications. Resume it from the rules page, or save a new version.

06Lifecycle

Saving, stages and versions

  • A new rule starts in shadow: it records findings and never alerts. Promote it through the same stages as a library rule, and the same false-positive arithmetic demotes it.
  • Each save is a new version. A save that changes the detection, correlation, trust or suggested playbook of an advisory or live rule drops it back to shadow and clears any automation confirmation, so the new logic earns its stage again. Wording changes keep the stage.
  • The editor refuses to overwrite a version someone else saved after you opened it.
  • Deleting a rule stops it and removes its links. Its findings, verdicts and history stay.
  • Library rules can’t be edited. Copy one into a new rule, which gets a team- id you can change before saving.

Owners and admins write, save, delete and resume rules. Any member can check one. Every save, deletion, pause and resume lands in the workspace audit log.

07Backtest

Try it before it runs

The editor’s Try on recent events replays up to 5,000 of your workspace’s most recent events, from the last hour up to the last 7 days, through the rule as written. It shows how many matched, how many times it would have fired, and up to 20 of those events. Nothing is saved or recorded, and drill events are left out. A rule with failing tests can still be backtested, as long as it compiles.

08CLI and API

From the terminal

bash
# check a rule without saving: problems, tests and cost; exits 1 on problems
tilldev shield tell rules validate rule.yaml

# what it would have fired on in the last three days
tilldev shield tell rules backtest rule.yaml --hours 72

# save it (starts in shadow), then a new version of it
tilldev shield tell rules create rule.yaml
tilldev shield tell rules update team-admin-from-script rule.yaml --version 1

# read it from stdin instead of a file
cat rule.yaml | tilldev shield tell rules validate -

# after a pause, and when it's no longer wanted
tilldev shield tell rules resume team-admin-from-script
tilldev shield tell rules rm team-admin-from-script --yes

The same operations are on the HTTP API under /api/shield/tell/rules, documented in the API reference. Validate in CI to keep rules in a repository and catch a broken one before it ships.