Chapter 4 — Data models
Lesson map
Lesson 04 published. Map, example, EX010.
flowchart TD
N0["Learning outcomes"]
N1["4.1 Objects and relationships"]
N2["Lab — reading a bundle"]
N3["4.2 Reading the result"]
N4["4.3 Patterns"]
N5["4.4 What to keep locally"]
N6["4.5 Other formats"]
N7["4.6 Timestamps"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7
N7 --> EX["Worked example"]
EX --> Q["Exercise EX010"]
Logical drawing
flowchart LR
N0["Learning outcomes"]
N1["4.1 Objects and relationships"]
N2["Lab — reading a bundle"]
N3["4.2 Reading the result"]
N4["4.3 Patterns"]
N5["4.4 What to keep locally"]
N6["4.5 Other formats"]
N7["4.6 Timestamps"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7 --> Q["EX010"]
[Learning outcomes]
|
v
[4.1 Objects and relationships]
|
v
[Lab — reading a bundle]
|
v
[4.2 Reading the result]
|
v
[4.3 Patterns]
|
v
[4.4 What to keep locally]
|
v
[4.5 Other formats]
|
v
[4.6 Timestamps]
|
v
[Worked example] --> [Exercise EX010]
Learning outcomes
You will (1) explain the structure of STIX 2.1 objects; (2) read a bundle with Python's json module; (3) check relationships for missing targets; (4) choose fields worth storing locally.
4.1 Objects and relationships
STIX 2.1 represents intelligence as typed JSON objects — indicator, attack-pattern, malware, threat-actor, report — each with an id made from its type and a unique identifier. Relationship objects connect them with a source_ref, a target_ref and a relationship_type such as "indicates" or "uses".
The id field makes each object addressable, so a relationship can point to it by name instead of copying it. An indicator that "indicates" an attack-pattern is therefore two objects and a link between them. This structure lets a sender share pieces separately, which is also how a link can end up pointing to an object that was never sent, as the lab shows.
Lab — reading a bundle
"""Lab (ch 04): read a small synthetic STIX 2.1-style bundle with json and list objects and links."""
import json
bundle = json.loads('''{"type":"bundle","id":"bundle--1","objects":[
{"type":"indicator","id":"indicator--a","name":"pay-portal domain","pattern":"[domain-name:value = 'pay-portal.example']","valid_from":"2026-09-01T00:00:00Z"},
{"type":"attack-pattern","id":"attack-pattern--b","name":"Phishing","external_references":[{"source_name":"mitre-attack","external_id":"T1566"}]},
{"type":"relationship","id":"relationship--c","relationship_type":"indicates","source_ref":"indicator--a","target_ref":"attack-pattern--b"},
{"type":"relationship","id":"relationship--d","relationship_type":"indicates","source_ref":"indicator--a","target_ref":"malware--z"}]}''')
ids = {o["id"]: o for o in bundle["objects"]}
from collections import Counter
print(dict(Counter(o["type"] for o in bundle["objects"])))
for o in bundle["objects"]:
if o["type"] == "relationship":
ok = o["source_ref"] in ids and o["target_ref"] in ids
print(o["id"], o["relationship_type"], "OK" if ok else "DANGLING target " + o["target_ref"])
Run with python3 lab_stix.py. Verified output:
{'indicator': 1, 'attack-pattern': 1, 'relationship': 2}
relationship--c indicates OK
relationship--d indicates DANGLING target malware--z
4.2 Reading the result
The bundle holds one indicator, one attack-pattern (Phishing, external identifier T1566) and two relationships. The first links the indicator to the attack pattern correctly. The second points to malware--z, which is not in the bundle — a dangling reference. It may exist in the sender's database, but CMS cannot use it without the object.
Lena checks the counts against the bundle text: one indicator, one attack-pattern and two relationships, four objects in all, matching the printed dictionary. The check that finds the dangling link is the simplest possible one, whether each referenced id is present in the bundle, and it is enough to catch the export fault MCX later fixed.
4.3 Patterns
An indicator's pattern field uses STIX patterning, such as matching a domain-name value. Lena extracts the value for matching but keeps the full pattern for reference.
In the lab bundle the pattern is "[domain-name:value = 'pay-portal.example']", from which Lena extracts the value pay-portal.example. Matching only needs that value, but the full pattern records exactly what the sender meant, including the object type. Keeping both avoids losing information if a later pattern is more complex than a single equality.
4.4 What to keep locally
CMS's simple store keeps: indicator value, type, source, TLP marking, valid_from, and linked ATT&CK identifiers. Other fields are stored in the original JSON for reference.
These fields answer the questions Lena asks when a match occurs: what was matched, what kind of value it is, who sent it, how widely it may be shared, since when it applies, and what technique it relates to. The TLP marking is stored with each value, so a match never leads to sharing more widely than the source allowed.
4.5 Other formats
Partners also send CSV and email lists. Mapping each to the same local fields lets all sources be searched together.
A CSV row from one partner and an email list from another end up with the same value, type and source fields as an indicator taken from STIX. Lena can then search for pay-portal.example once and see every source that mentioned it. Without the mapping she would search three formats separately and might miss a source. Fields a format lacks, such as a TLP marking in a plain email list, are filled from the message's own label.
4.6 Timestamps
STIX timestamps are in UTC with a trailing "Z". Keep them in UTC in storage and convert only when displaying to staff, so comparisons across sources stay correct.
The resolver log and an MCX indicator may come from different systems, but if both are stored in UTC, comparing a lookup time with an indicator's valid_from date is direct. Staff see local time in their reports, converted at display, so they read a familiar clock while the stored records remain consistent with every other source.
Worked example
Lena reports the dangling reference to MCX. The sender explains that the malware object is TLP:AMBER and was filtered out before the GREEN bundle was published, leaving the relationship behind. MCX fixes its export to drop relationships whose targets are filtered.
Synthetic case. The bundle and exchange are invented.
Errors and limits
Valid JSON can still be poor intelligence; structure checks are the first step only.
Exercises
- EX010 — Object count. What object types and counts did the lab bundle contain? Rubric: 3 = indicator 1, attack-pattern 1, relationship 2; 2 = partial; 1 = wrong; 0 = none.
- EX011 — Dangling cause. What caused the dangling reference in the worked example? Rubric: 3 = AMBER target filtered out of GREEN export, relationship left; 2 = partial; 1 = typo; 0 = none.
- EX012 — UTC rule. Why keep timestamps in UTC in storage? Rubric: 3 = consistent comparison across sources; convert only for display; 2 = partial; 1 = no reason; 0 = none.
Synthesis
Read STIX as typed objects and relationships, check references, keep useful fields and store times in UTC.
Continue studying — chapter 04 of 30
← Chapter 03 (draft) · Course chapter index · Chapter 05 (draft) →