Detection Engineering

Sharing threat intel with STIX & TAXII

The STIX 2.1 object model and patterns, a complete bundle, TAXII 2.1 collections with curl, and TLP 2.0 markings.

1 Introduction

Threat intelligence is only useful if it reaches the tools that can act on it. Copying IOCs from a PDF into a firewall by hand is slow, error-prone and loses all the context: who is behind the activity, which malware, how confident the source is, who may see it. Two OASIS open standards solve this problem:

STIX 2.1 — the language

Structured Threat Information Expression: a JSON format to describe threat actors, malware, techniques, indicators, observations and the relationships between them, as a graph.

TAXII 2.1 — the transport

Trusted Automated Exchange of Intelligence Information: a REST API over HTTPS to publish and retrieve STIX content from servers, organised in collections.

Analogy: STIX is the letter (with a standard structure every recipient can read), TAXII is the postal service. Both reached version 2.1 as OASIS Standards in 2021, and they are what sits behind most "TAXII feeds" offered by ISACs, CERTs, vendors and platforms such as MISP or OpenCTI.

STIX 1.x (XML) and TAXII 1.x are obsolete and incompatible with version 2. If a partner still sends XML, you will need a converter.

2 Object Model

Everything in STIX is a JSON object with a type and a unique id of the form type--UUID, e.g. malware--481ba5c9-2b13-47e5-87d7-c2464ade08ec. Objects fall into three main families.

SDOs — STIX Domain Objects (the "nouns" of CTI)

STIX 2.1 defines 18 SDOs. The ones you will meet every day:

Type Describes Required (besides common properties)
indicatorA detection pattern for suspicious activitypattern, pattern_type, valid_from
malwareA malware family or a specific sampleis_family (+ name if family)
threat-actorIndividuals or groups acting with malicious intentname
intrusion-setA cluster of activity attributed to one (often unknown) actor, e.g. "APT29"name
attack-patternA TTP, typically an ATT&CK techniquename
campaignA set of attacks over a period with a common goalname
vulnerabilityA flaw, usually referenced by CVEname
identityAn organization, person or sector (authors, victims)name
reportA published report grouping other objectsname, published, object_refs

The others are course-of-action, grouping, infrastructure, location, malware-analysis, note, observed-data, opinion and tool. Every SDO carries the common properties type, spec_version ("2.1"), id, created and modified.

SROs — relationships (the "verbs")

relationship links two objects with a relationship_type:

  • indicator → indicates → malware
  • threat-actor → uses → malware / attack-pattern
  • campaign → attributed-to → threat-actor
  • malware → targets → identity / vulnerability

sighting says "this indicator, malware or actor was seen here, N times, between these dates". Sightings are how defenders give feedback to producers.

SCOs — cyber-observables (the "facts")

Technical things observed on a system or network, with no judgement attached:

  • file — hashes, name, size
  • ipv4-addr / ipv6-addr — value
  • domain-name, url, email-addr
  • process, network-traffic, windows-registry-key, mutex, x509-certificate…

SCOs have no created/modified, and their IDs should be deterministic UUIDv5 computed from key properties, so the same IP always gets the same ID.

Observable vs indicator: ipv4-addr 203.0.113.45 is a fact. An indicator whose pattern matches that IP is a claim ("this IP is malicious") with a validity period, a confidence and an author. That distinction is the core of STIX.

3 Pattern Language

The pattern of an indicator is written in the STIX Patterning language ("pattern_type": "stix"). It describes which observables must be seen for the indicator to match. The building block is a comparison expression object-type:property operator value, wrapped in square brackets to form an observation expression.

# One SHA-256 hash
[file:hashes.'SHA-256' = '9c165ab24690c682be401bdaafca19fd5124eb9b9f08dd495de87d2e0c80e652']

# Domain OR IP: two observations, either one is enough
[domain-name:value = 'cdn-update.example'] OR [ipv4-addr:value = '203.0.113.45']

# Two conditions on the SAME object
[file:name = 'upd.dll' AND file:size < 300000]

# Wildcards, CIDR and regular expressions
[domain-name:value LIKE '%.cdn-update.example']
[ipv4-addr:value ISSUBSET '203.0.113.0/24']
[process:command_line MATCHES '^powershell.*-enc']

# Sequence + time window: the file, then the domain, within 5 minutes
([file:hashes.'SHA-256' = 'fb18cd2a74c578b056a81a207cfe4b93d412c707c7f6751c0d4ea32219da4bf2'] FOLLOWEDBY [domain-name:value = 'cdn-update.example']) WITHIN 300 SECONDS

# Repetition: 5 connections to the C2 within 10 minutes
[network-traffic:dst_ref.value = '203.0.113.45' AND network-traffic:dst_port = 443] REPEATS 5 TIMES WITHIN 600 SECONDS
Element Values
Comparison operators= != > < >= <= IN LIKE MATCHES ISSUBSET ISSUPERSET, NOT to negate, and EXISTS (prefix: [EXISTS windows-registry-key:values])
Inside bracketsAND, OR — with AND, all comparisons must match the same object
Between bracketsAND, OR, FOLLOWEDBY
QualifiersWITHIN n SECONDS, REPEATS n TIMES, START t'…' STOP t'…'

The pattern does not have to be STIX. pattern_type can also be sigma, yara, snort, suricata or pcre, which lets you ship a YARA, Sigma or Suricata rule inside an indicator, with all its STIX context.

4 A Full Bundle

Objects travel in a bundle: a wrapper with "type": "bundle", an ID and an objects array (the bundle itself is not a STIX object and has no spec_version). The bundle below describes a fictional phishing campaign: an actor, its malware, the technique used, two indicators, the observables, the relationships, and a sighting from a SOC. It is valid STIX 2.1 JSON.

{
  "type": "bundle",
  "id": "bundle--e558bf9b-eb83-400b-8375-5cc6d2c587e0",
  "objects": [
    {
      "type": "identity",
      "spec_version": "2.1",
      "id": "identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb",
      "created": "2026-10-14T09:00:00.000Z",
      "modified": "2026-10-14T09:00:00.000Z",
      "name": "Example SOC (mlab Academy)",
      "identity_class": "organization"
    },
    {
      "type": "marking-definition",
      "spec_version": "2.1",
      "id": "marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb",
      "created": "2022-10-01T00:00:00.000Z",
      "name": "TLP:GREEN",
      "extensions": {
        "extension-definition--60a3c5c5-0d10-413e-aab3-9e08dde9e88d": {
          "extension_type": "property-extension",
          "tlp_2_0": "green"
        }
      }
    },
    {
      "type": "threat-actor",
      "spec_version": "2.1",
      "id": "threat-actor--ea786151-92a0-45df-a007-f6f8ae65fd23",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "created_by_ref": "identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb",
      "name": "Grey Ledger",
      "description": "Fictional financially motivated actor used for training.",
      "threat_actor_types": ["crime-syndicate"],
      "primary_motivation": "personal-gain",
      "object_marking_refs": ["marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb"]
    },
    {
      "type": "malware",
      "spec_version": "2.1",
      "id": "malware--481ba5c9-2b13-47e5-87d7-c2464ade08ec",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "created_by_ref": "identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb",
      "name": "LedgerLoader",
      "is_family": true,
      "malware_types": ["downloader", "backdoor"],
      "object_marking_refs": ["marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb"]
    },
    {
      "type": "attack-pattern",
      "spec_version": "2.1",
      "id": "attack-pattern--a2b2da60-af37-4e61-b37b-cc0adfd6b0ea",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "name": "Spearphishing Attachment",
      "external_references": [
        {
          "source_name": "mitre-attack",
          "external_id": "T1566.001",
          "url": "https://attack.mitre.org/techniques/T1566/001/"
        }
      ],
      "kill_chain_phases": [
        { "kill_chain_name": "mitre-attack", "phase_name": "initial-access" }
      ]
    },
    {
      "type": "campaign",
      "spec_version": "2.1",
      "id": "campaign--660a8186-7fe0-4645-b44d-2ea1ed3a806a",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "created_by_ref": "identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb",
      "name": "Fake invoice wave - October 2026",
      "first_seen": "2026-10-06T00:00:00Z",
      "object_marking_refs": ["marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb"]
    },
    {
      "type": "indicator",
      "spec_version": "2.1",
      "id": "indicator--18a84965-183d-433e-a90f-79569c6eaf77",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "created_by_ref": "identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb",
      "name": "Invoice_4471.docm (LedgerLoader dropper)",
      "indicator_types": ["malicious-activity"],
      "pattern": "[file:hashes.'SHA-256' = '9c165ab24690c682be401bdaafca19fd5124eb9b9f08dd495de87d2e0c80e652']",
      "pattern_type": "stix",
      "valid_from": "2026-10-14T09:12:00Z",
      "object_marking_refs": ["marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb"]
    },
    {
      "type": "indicator",
      "spec_version": "2.1",
      "id": "indicator--11125fd5-ae7d-471a-83f7-9c397123b080",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "created_by_ref": "identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb",
      "name": "LedgerLoader payload server",
      "indicator_types": ["malicious-activity"],
      "pattern": "[domain-name:value = 'cdn-update.example'] OR [ipv4-addr:value = '203.0.113.45']",
      "pattern_type": "stix",
      "valid_from": "2026-10-14T09:12:00Z",
      "valid_until": "2026-11-14T09:12:00Z",
      "object_marking_refs": ["marking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb"]
    },
    {
      "type": "domain-name",
      "spec_version": "2.1",
      "id": "domain-name--a584802c-c4fa-5a2d-8623-483171fa04b8",
      "value": "cdn-update.example",
      "resolves_to_refs": ["ipv4-addr--cb9d70c5-d244-531c-9efb-bcae57adc793"]
    },
    {
      "type": "ipv4-addr",
      "spec_version": "2.1",
      "id": "ipv4-addr--cb9d70c5-d244-531c-9efb-bcae57adc793",
      "value": "203.0.113.45"
    },
    {
      "type": "relationship",
      "spec_version": "2.1",
      "id": "relationship--830010b0-0d38-4ea0-87c8-5067c88405f1",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "relationship_type": "indicates",
      "source_ref": "indicator--18a84965-183d-433e-a90f-79569c6eaf77",
      "target_ref": "malware--481ba5c9-2b13-47e5-87d7-c2464ade08ec"
    },
    {
      "type": "relationship",
      "spec_version": "2.1",
      "id": "relationship--4d7b9f9f-a0b0-4729-bd87-4b8545a365ac",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "relationship_type": "uses",
      "source_ref": "threat-actor--ea786151-92a0-45df-a007-f6f8ae65fd23",
      "target_ref": "malware--481ba5c9-2b13-47e5-87d7-c2464ade08ec"
    },
    {
      "type": "relationship",
      "spec_version": "2.1",
      "id": "relationship--58ff3faa-2029-40f9-bf69-03e9905da6ff",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "relationship_type": "uses",
      "source_ref": "campaign--660a8186-7fe0-4645-b44d-2ea1ed3a806a",
      "target_ref": "attack-pattern--a2b2da60-af37-4e61-b37b-cc0adfd6b0ea"
    },
    {
      "type": "relationship",
      "spec_version": "2.1",
      "id": "relationship--11fc53ce-a4f7-47f8-91dc-0a91e714f493",
      "created": "2026-10-14T09:12:00.000Z",
      "modified": "2026-10-14T09:12:00.000Z",
      "relationship_type": "attributed-to",
      "source_ref": "campaign--660a8186-7fe0-4645-b44d-2ea1ed3a806a",
      "target_ref": "threat-actor--ea786151-92a0-45df-a007-f6f8ae65fd23",
      "confidence": 60
    },
    {
      "type": "sighting",
      "spec_version": "2.1",
      "id": "sighting--7c3e9b1a-5d24-4f6e-9a0b-2e8f4c6d1a37",
      "created": "2026-10-15T14:03:00.000Z",
      "modified": "2026-10-15T14:03:00.000Z",
      "created_by_ref": "identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb",
      "sighting_of_ref": "indicator--11125fd5-ae7d-471a-83f7-9c397123b080",
      "where_sighted_refs": ["identity--2ca7f0b5-433d-4e29-9e91-db1d737c4bcb"],
      "first_seen": "2026-10-15T13:41:07Z",
      "last_seen": "2026-10-15T13:58:22Z",
      "count": 3
    }
  ]
}

What to notice

1

IDs: SDOs and SROs use random UUIDv4; the two SCOs (domain-name, ipv4-addr) use UUIDv5 derived from their value, so another producer describing the same domain gets the same ID.

2

Timestamps are UTC in RFC 3339 format ending with Z; created and modified are precise to the millisecond. To update an object, keep its ID and increase modified: that is how STIX versions objects.

3

ATT&CK link: the attack-pattern points to T1566.001 through external_references with source_name: "mitre-attack", the convention every tool understands.

4

Lifetime: the network indicator has a valid_until one month later. Infrastructure is short-lived; a hash indicator can stay valid much longer.

5

Confidence: the attribution relationship carries "confidence": 60 (0–100 scale). Attribution is rarely certain; say so in the data.

Don't build bundles by hand in production: libraries such as the OASIS stix2 Python package generate IDs and timestamps for you and validate required properties.

5 TAXII 2.1

A TAXII 2.1 server exposes a small, predictable REST API. Every request must send the header Accept: application/taxii+json;version=2.1, and authentication is usually HTTP Basic or an API key over HTTPS.

GET /taxii2/

Discovery: lists the server's API roots.

GET {api-root}/collections/

API root: an independent group of collections (e.g. one per trust group). Collection: a named set of STIX objects, with can_read/can_write permissions.

GET {api-root}/collections/{id}/objects/

Objects: returns an envelope {"more", "next", "objects"}. Filters: added_after, limit, next, match[type], match[id], match[version], match[spec_version]. A POST on the same URL publishes objects (if can_write).

GET {api-root}/collections/{id}/manifest/

Manifest: metadata only (ID, version, date added) to see what changed before downloading.

Hands-on: MITRE's public TAXII server

MITRE publishes ATT&CK itself over TAXII 2.1 without authentication, which makes it a perfect playground. Note the -g (--globoff) option: without it, curl tries to interpret the square brackets of match[type] as a pattern.

# 1. Discovery
curl -s -H 'Accept: application/taxii+json;version=2.1' \
  https://attack-taxii.mitre.org/taxii2/

# 2. Collections of the default API root
curl -s -H 'Accept: application/taxii+json;version=2.1' \
  https://attack-taxii.mitre.org/api/v21/collections/ | jq -r '.collections[] | "\(.id)  \(.title)"'

# 3. Objects of type intrusion-set (ATT&CK groups) from the Enterprise collection
curl -g -s -D headers.txt -H 'Accept: application/taxii+json;version=2.1' \
  'https://attack-taxii.mitre.org/api/v21/collections/x-mitre-collection--1f5f1533-f617-4ca8-9ab4-6a02367fa019/objects/?match[type]=intrusion-set&limit=50' \
  | jq -r '.more, .next, (.objects[] | .external_references[0].external_id + "  " + .name)'

A response looks like this (truncated):

{
  "more": true,
  "next": "1",
  "objects": [
    {
      "type": "intrusion-set",
      "spec_version": "2.1",
      "id": "intrusion-set--f29b7c5e-2439-42ad-a86f-9f8984fafae3",
      "created": "2018-10-17T00:14:20.652Z",
      "modified": "2026-07-31T20:48:51.374Z",
      "name": "HEXANE",
      "aliases": [
        "HEXANE",
        "Lyceum",
        "Siamesekitten",
        "Spirlin"
      ],
      "external_references": [
        {
          "source_name": "mitre-attack",
          "url": "https://attack.mitre.org/groups/G1001",
          "external_id": "G1001"
        }
      ]
    },
    {
      "type": "intrusion-set",
      "spec_version": "2.1",
      "id": "intrusion-set--e6db1e55-b199-4b6b-8633-989345ee45e0",
      "created": "2024-08-26T17:39:06.020Z",
      "modified": "2026-07-31T18:48:24.240Z",
      "name": "Moonstone Sleet",
      "aliases": [
        "Moonstone Sleet",
        "Storm-1789"
      ],
      "external_references": [
        {
          "source_name": "mitre-attack",
          "url": "https://attack.mitre.org/groups/G1036",
          "external_id": "G1036"
        }
      ]
    }
  ]
}

ATT&CK groups are published as intrusion-set, not threat-actor: they are clusters of activity, not confirmed identities. When more is true, repeat the request with next=<value> to get the next page. For incremental polling, store the X-TAXII-Date-Added-Last response header and pass it as added_after next time. Against a private server, add -u "$TAXII_USER:$TAXII_PASS" and keep credentials in environment variables, never in scripts.

6 TLP Markings

The Traffic Light Protocol (TLP), maintained by FIRST, tells recipients how far they may share a piece of information. Version 2.0 (August 2022) replaced TLP:WHITE with TLP:CLEAR and added TLP:AMBER+STRICT. In STIX, a marking is a marking-definition object referenced from object_marking_refs. TLP 2.0 markings have fixed IDs published by the OASIS CTI Technical Committee:

Label May be shared with STIX marking-definition ID
TLP:REDNamed recipients only, e.g. those present in the meetingmarking-definition--e828b379-4e03-4974-9ac4-e53a884c97c1
TLP:AMBER+STRICTThe recipient's organization onlymarking-definition--939a9414-2ddd-4d32-a0cd-375ea402b003
TLP:AMBERThe recipient's organization and its clients, on a need-to-know basismarking-definition--55d920b0-5e8b-4f79-9ee9-91f868d9b421
TLP:GREENThe recipient's community, but not publiclymarking-definition--bab4a63c-aed9-4cf5-a766-dfca5abac2bb
TLP:CLEARAnyone, publicly (copyright still applies)marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487

The STIX 2.1 specification itself only predefines the older TLP 1.0 markings (WHITE, GREEN, AMBER, RED). TLP 2.0 is added through an extension-definition, which is why the marking in the bundle above has an extensions block. Some tools still only understand TLP 1.0: check what your platform expects.

TLP is a social contract, not a technical control: nothing in STIX stops a recipient from forwarding a TLP:RED object. Platforms enforce it by filtering what each user or sharing group can see.

Try it on mlab.sh

Before turning an observable into a STIX indicator, check its reputation: look up IPs, domains, URLs and file hashes on mlab.sh.

Start analyzing on mlab.sh

7 In Practice

MISP

Open-source sharing platform, very common among CERTs and ISACs. Its native model is not STIX: information is grouped into events containing attributes (an IP, a hash), objects (groups of attributes), tags and taxonomies (including TLP) and galaxies (including ATT&CK). MISP instances synchronise with each other directly, and import/export STIX 2.1 through the misp-stix library. Conversions are good but not lossless: check the result.

OpenCTI

Open-source CTI platform developed by Filigran (initially with ANSSI and CERT-EU) whose data model is built on STIX 2.1. Everything is a graph of SDOs, SCOs and relationships. Connectors import ATT&CK, MISP events, TAXII collections, CVE data or sandbox results, and OpenCTI can itself expose TAXII collections and live streams to your SIEM or EDR.

From feed to detection

Collect

Your TIP polls TAXII collections and other threat-intel feeds on a schedule, using added_after.

Deduplicate and score

The same IOC arrives from several sources: merge it, keep the best confidence, drop expired indicators (valid_until).

Push to controls

Indicators go to the SIEM (matching), EDR (hash blocking), proxy/DNS (blocklists), respecting TLP.

Feed back

When an indicator fires, create a sighting and share it back. Sightings tell producers which indicators are actually useful.

Indicators without context or expiry

A feed of bare IPs with no valid_until, no relationship and no confidence will flood your SIEM with false positives within months (shared hosting, recycled cloud IPs).

"STIX" that isn't STIX

Missing spec_version, IDs that are not type--UUID, timestamps without Z: run a validator (e.g. the OASIS stix2-validator) before publishing.

Related Modules