Object model
Threat vectors, detection objectives, and MDR rules — what each is for, how they chain, and how to author them.
opentide organises detection content into three object families under objects/. Together they answer three questions: what do we defend against? (threat), what are we trying to detect? (objective), and how do we detect it? (rule).
| Object | Answers | Directory | Schema | Acronym |
|---|---|---|---|---|
| Threat | What do we defend against? | objects/threats/ | threat::1.0 | TVM — Threat Vector Model |
| Objective | What are we trying to detect? | objects/objectives/ | objective::1.0 | DOM — Detection Objective Model |
| Rule | How do we detect it, on which platform? | objects/rules/ | rule::1.0 | MDR — Managed Detection Rule |
Every object is a YAML file with a top-level name and a shared metadata block. The normative field lists live in the specifications; this page is the working author's tour.
The shared metadata block
metadata:
uuid: 00000000-0000-4000-8003-000000000001 # stable identity — references use this
schema: rule::1.0 # structural revision (selects the model)
version: 1 # instance content version (semver-ish)
created: 2026-01-01
modified: 2026-01-02
tlp: clear # sharing sensitivityuuid is the identity that never changes — cross-object links use it, so you can rename or move files freely. schema vs version trips people up; see Schema revision.
A worked example: one chain
The clearest way to understand the model is to read one full chain — a threat, an objective that covers it, and a rule that implements that objective. These mirror the conformance fixtures.
Threat — what we defend against
name: Simulated Actor
criticality: High
metadata:
uuid: 00000000-0000-4000-8001-000000000001
schema: threat::1.0
version: 1
created: 2026-01-01
modified: 2026-01-02
tlp: clear
threat:
description: Simulated threat actor for credential access
severity: Substantial incident
impact:
- Data Breach
leverage:
- Information Gathering
viability: Likely
terrain: Endpoint workstations and user devices.
surface:
- OS::Windows::Desktop
mitre_attack:
- T1059
actors:
- name: att&ck::G0006
# cve: # optional; CIRCL Vulnerability-Lookup (CVE, GHSA, GCVE)
# - CVE-2024-3094threat.impact and threat.leverage are non-empty lists of vocabulary names, one name per item. A scalar string or a ;-joined string is invalid.
Optional threat.actors entries are objects. name is a scoped key from the bundled actors vocabulary (att&ck::G0006, or a MISP cluster ID). Optional sighting and references sit on the same object — not a bare string list.
Objective — what we want to detect
The objective references the threat by UUID under objective.threats, and declares the signals that satisfy the goal.
name: Credential Access Objective
metadata:
uuid: 00000000-0000-4000-8002-000000000001
schema: objective::1.0
version: 1
created: 2026-01-01
modified: 2026-01-02
tlp: clear
composition:
strategy: Combined
description: Compose signals for credential access detection
objective:
priority: High
type: Threat
description: Detect credential access techniques
composition: # mirrors the top-level composition
strategy: Combined
description: Compose signals for credential access detection
threats:
- 00000000-0000-4000-8001-000000000001 # ← the threat above
signals:
- name: Suspicious logon signal
uuid: 00000000-0000-4000-8099-000000000001
description: Suspicious authentication activity
severity: Medium
methodology: Statistical
entities: [Host::Hostname]
data:
availability: Complete
requirements: Security event logsRule — how we detect it
The rule references what it implements by UUID via detection_model: the whole objective, or one signal defined under it. It carries one configuration block per platform it deploys to.
name: Sentinel KQL Rule
metadata:
uuid: 00000000-0000-4000-8003-000000000001
schema: rule::1.0
version: 1
created: 2026-01-01
modified: 2026-01-02
tlp: clear
description: Detects credential access via suspicious process creation
status: STAGING
severity: High
techniques: [T1059]
detection_model: 00000000-0000-4000-8002-000000000001 # ← the objective above
response:
alert_severity: High
configurations:
sentinel:
enabled: true
name: Sentinel KQL Rule
status: STAGING
query: |
SecurityEvent
| where EventID == 4688
| take 1
scheduling:
frequency: PT1H
lookback: PT2H
alert:
title: Sentinel KQL Rule
suppression: false
grouping:
event: SingleAlert
alert:
enabled: falseChaining
Objects reference each other by UUID. References point rule → objective → threat; detection coverage flows the other way.
flowchart LR
subgraph refs [References point this way]
direction LR
Rule -->|detection_model| Objective
Objective -->|objective.threats| Threat
end
Threat -. "is covered by" .-> Objective
Objective -. "is implemented by" .-> Rule
Inspect the graph without reading YAML by hand:
opentide info # summary of objects and their links
opentide --json info --technique T1059 coverage # coverage for one ATT&CK technique# MCP equivalent
get_chaining(uuid="00000000-0000-4000-8002-000000000001")Cross-object reference and chaining checks run as part of full validation.
Anti-patterns to avoid
The validator catches these, but designing against them keeps your graph healthy.
- Orphan rule — a rule whose
detection_modelpoints at no existing objective (or omits it when your policy requires one). It deploys, but nobody can say what threat it addresses. - Dangling reference — an
objective.threatsUUID that no threat file defines. Usually a typo or a deleted object. - Duplicate UUID — two objects sharing a UUID. Uniqueness is enforced; regenerate UUIDs, never hand-copy them.
- Circular chain — objectives that reference each other in a loop. Keep the graph a DAG: threat ← objective ← rule.
- Coverage island — a threat that no objective covers. Valid, but it is a visible gap in your coverage report — often intentional, sometimes a to-do.
Typed Python access
The same objects are available programmatically through the registry:
from opentide import Opentide
Opentide.initialise()
rule = Opentide.Rules["00000000-0000-4000-8003-000000000001"]
objective = Opentide.Objectives["00000000-0000-4000-8002-000000000001"]
threat = Opentide.Threats["00000000-0000-4000-8001-000000000001"]
obj = Opentide.lookup("00000000-0000-4000-8002-000000000001") # cross-type lookupRules loaded through the registry expose delegation methods: validate(), validate_query(platform), deploy(), and document(). See SDK models.
Generated artifacts
After opentide generate:
| Artifact | Location |
|---|---|
| JSON Schema | .opentide/schemas/{family}.{major}.{minor}.schema.json |
| Template | .opentide/templates/{family}.{major}.{minor}.template.yaml |
| IDE router | .opentide/schemas/opentide.schema.json |
Further reading
- Schema revision —
metadata.schemavsmetadata.version. - Platform block schemas — links to the field specs, which list every required and optional key.
- Tutorial — author this exact chain yourself, end to end.
- Normative specs: Threat · Objective · Rule · Metadata.