Skip to content
Back to blog
Network security

How STIX 2.1 and TAXII 2.1 make threat intel machine-readable

Build a STIX 2.1 bundle step by step, see how TAXII 2.1 moves it, and why the Pyramid of Pain says behaviour-level intel costs attackers more than IPs.

Advanced10 min read

On this page

Most of us meet threat intelligence as a list: a few thousand IP addresses, some domains, a column of SHA-256 hashes, maybe a CSV attached to an email. You load it into the firewall or the SIEM, a few alerts fire, and a month later nobody can say which entries still matter, who sent them, or why they were bad in the first place.

This post is about the other way of doing it. We will build one small, machine-readable piece of threat intelligence step by step in STIX 2.1, send it over TAXII 2.1, and use David Bianco's Pyramid of Pain to explain why the most useful part of that package is not the IP address.

Signals on their own are dots. Linked to context, they become a picture you can act on.

Data, information, intelligence

NIST's Guide to Cyber Threat Information Sharing (SP 800-150) is a good place to get the vocabulary straight. It calls threat information anything that can help an organisation identify, assess, monitor and respond to cyber threats. Inside that, it separates a few kinds (full text, section 2):

  • Indicators: technical artifacts or observables that suggest an attack is coming, is under way, or has already happened. The guide's examples are the IP address of a suspected command-and-control server, a suspicious domain, a malicious URL, a file hash, or an email subject line.
  • TTPs (tactics, techniques and procedures): descriptions of how an actor behaves.
  • Security alerts, threat intelligence reports and tool configurations.

And then the sentence I like most: threat intelligence is threat information that has been "aggregated, transformed, analyzed, interpreted, or enriched" to give the context needed for decisions. An IP address is data. "This IP was seen in our proxy logs" is information. "This IP hosts the command channel of a malware family that arrives as a spearphishing attachment, we are 70% sure, and it is only worth blocking until the end of the month" is intelligence. The rest of this post is about writing that last sentence in a way a machine can read.

The same NIST guide also lists what good indicators look like: timely, relevant, accurate, specific and actionable. Keep those five words in mind; most of STIX exists to carry the context that makes an indicator meet them.

The Pyramid of Pain

In March 2013 David Bianco published The Pyramid of Pain, a simple drawing that ranks indicator types by how much it hurts an adversary when you detect and deny them. In a January 2014 update he added the hash level at the base. From the base up, with the labels on his current drawing:

Level Pain for the attacker
Hash values Trivial
IP addresses Easy
Domain names Simple
Network/host artifacts Annoying
Tools Challenging
TTPs Tough!

His reasoning is easy to follow. Change one bit of a file and its hash is completely different. Any reasonably advanced adversary can change IP addresses whenever it suits them, with very little effort. Domains cost a little more because they have to be registered and hosted. At the top, if you can detect and respond to a behaviour (his example is Pass-the-Hash), the adversary has to learn a new way of working, which is the most expensive thing you can make them do.

Bianco's pyramid, filled from the cheap-to-change base to the expensive top.

Two practical lessons follow. First, an indicator feed made only of the bottom two layers will always be racing the attacker, so expiry and context matter more than volume. Second, the valuable part of a shared report is often the link from a cheap indicator to an expensive behaviour. STIX gives that link a shape.

Building one STIX 2.1 bundle, piece by piece

STIX 2.1 is an OASIS standard, a JSON language for "cyber threat and observable information". It has 18 STIX Domain Objects (SDOs) such as Indicator, Malware, Attack Pattern, Threat Actor and Vulnerability, plus two relationship objects (SROs): the generic Relationship and the Sighting. Every object has an id made of its type, two hyphens and a UUID, such as indicator--8e2e2d2b-…, and spec_version must be "2.1".

Let's build a small story. All values below are made up for this post; the IP comes from 203.0.113.0/24, a block reserved for documentation that should not appear on the public internet.

Step 1: the indicator

An Indicator holds a detection pattern. Required fields specific to it are pattern, pattern_type and valid_from. The pattern may be in STIX Patterning or another language such as Snort or YARA, and pattern_type says which.

{
  "type": "indicator",
  "spec_version": "2.1",
  "id": "indicator--5f6b8c1e-3a8d-4f7e-9b62-0d1c2e3f4a5b",
  "created": "2026-10-20T08:00:00.000Z",
  "modified": "2026-10-20T08:00:00.000Z",
  "name": "C2 address for example loader",
  "indicator_types": ["malicious-activity"],
  "pattern": "[ipv4-addr:value = '203.0.113.45']",
  "pattern_type": "stix",
  "valid_from": "2026-10-20T08:00:00Z",
  "valid_until": "2026-11-20T08:00:00Z",
  "confidence": 70
}

Two fields here answer questions a bare CSV can't. valid_until is the time after which the indicator "should no longer be considered a valid indicator"; if it is missing, there is no end date at all. confidence is an integer from 0 to 100 that expresses how sure the creator is.

Step 2: the malware, and "indicates"

A Malware object describes a family or instance. The link between them is a Relationship whose relationship_type is indicates, which the spec defines as "the Indicator can detect evidence of" the target.

{ "type": "malware", "spec_version": "2.1",
  "id": "malware--0c7b5e1a-2f4d-4a8b-9e3c-6d5f4a3b2c1d",
  "created": "2026-10-20T08:00:00.000Z", "modified": "2026-10-20T08:00:00.000Z",
  "name": "Example Loader", "is_family": true }

{ "type": "relationship", "spec_version": "2.1",
  "id": "relationship--7a1e2d3c-4b5a-4f6e-8d7c-9b0a1e2d3c4b",
  "created": "2026-10-20T08:00:00.000Z", "modified": "2026-10-20T08:00:00.000Z",
  "relationship_type": "indicates",
  "source_ref": "indicator--5f6b8c1e-3a8d-4f7e-9b62-0d1c2e3f4a5b",
  "target_ref": "malware--0c7b5e1a-2f4d-4a8b-9e3c-6d5f4a3b2c1d" }

Step 3: the behaviour, and "uses"

Now we climb the pyramid. An Attack Pattern is a type of TTP that describes how adversaries try to compromise targets; the spec's own example is spear phishing. Malware can point at it with a uses relationship. If you want everyone to mean the same behaviour, reference MITRE ATT&CK in external_references. MITRE publishes the whole of ATT&CK as STIX 2.1 bundles, where techniques and sub-techniques are attack-pattern objects looked up by their external_id. Our story uses T1566.001 Spearphishing Attachment, under the Initial Access tactic.

{ "type": "attack-pattern", "spec_version": "2.1",
  "id": "attack-pattern--3d2c1b0a-9e8f-4a7b-8c6d-5e4f3a2b1c0d",
  "created": "2026-10-20T08:00:00.000Z", "modified": "2026-10-20T08:00:00.000Z",
  "name": "Spearphishing Attachment",
  "external_references": [{ "source_name": "mitre-attack", "external_id": "T1566.001" }] }

Plus one more relationship: malware --uses--> attack-pattern. The IP in step 1 will change soon. This link will not, and it tells the reader where to look next: mail gateways, attachment handling, user execution.

Step 4: the sighting

A Sighting says "I saw this". The spec is careful about the difference: Observed Data is information ("I saw this file"), while a Sighting is an intelligence assertion ("I saw this threat actor"). Only sighting_of_ref is required; count, first_seen, last_seen and where_sighted_refs are optional, and a count of 0 can even say an indicator was not seen. Sightings are how intel flows back to the producer.

{ "type": "sighting", "spec_version": "2.1",
  "id": "sighting--9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "created": "2026-10-22T03:15:00.000Z", "modified": "2026-10-22T03:15:00.000Z",
  "sighting_of_ref": "indicator--5f6b8c1e-3a8d-4f7e-9b62-0d1c2e3f4a5b",
  "count": 3,
  "first_seen": "2026-10-22T01:02:00Z", "last_seen": "2026-10-22T02:47:00Z" }

Step 5: the bundle

Finally, wrap it all in a Bundle: {"type": "bundle", "id": "bundle--…", "objects": [ … ]}. One detail surprises people: a bundle "does not have any semantic meaning", and objects are not related just because they travel together. The links live in the relationship objects (and in reference properties such as the sighting's sighting_of_ref). That is why our package has six objects: four SDO/SRO boxes plus two relationships.

Our bundle: the relationship objects carry the meaning, not the envelope.

TAXII 2.1: how the bundle travels

TAXII 2.1 (OASIS Standard, 10 June 2021) is an application-layer protocol for exchanging CTI over HTTPS. It was designed for STIX but is not limited to it. The parts fit together like this:

  • Discovery: a client asks GET /taxii2/ and learns which API Roots the server offers. Servers can also be advertised with a DNS SRV record.
  • API Roots: "logical groupings" of collections, so one server can host separate roots for separate trust groups, each with its own access control.
  • Collections: a logical repository of CTI objects. You list them with GET {api-root}/collections/, and each one says whether you can_read and can_write.
  • Objects: GET {api-root}/collections/{id}/objects/ pulls, POST to the same URL pushes. Content travels inside a TAXII envelope, and every request uses the media type application/taxii+json;version=2.1.

Pulling is where the useful details are. Filters such as ?match[type]=indicator,sighting narrow the result. For polling, added_after asks for objects added since a timestamp, and the spec stresses that this is the time the object was added to the server, not any date inside the STIX object. Large result sets come in pages: the envelope's more flag says there is more, and the client continues with next or with the X-TAXII-Date-Added-Last header.

GET /api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/objects/?match[type]=indicator&added_after=2026-10-24T00:00:00Z HTTP/1.1
Host: taxii.example.com
Accept: application/taxii+json;version=2.1

Pushing is asynchronous. A successful POST returns 202 Accepted with a status resource, and the client polls {api-root}/status/{status-id}/ until it says complete. The spec also reserves Channels (publish-subscribe) but leaves them to a future version, so in practice TAXII 2.1 means collections.

Where the sharing happens

Two examples show the range.

CISA Automated Indicator Sharing (AIS) is a no-cost service for the "real-time exchange of machine-readable cyber threat indicators" between public- and private-sector organisations. Joining means agreeing to terms, having a STIX/TAXII capability (an open-source TAXII 2.1 client works), a PKI certificate and an interconnection agreement. CISA encourages participants to share back over the bidirectional TAXII connection, not only consume.

MISP is an open-source threat intelligence platform for collecting, storing and sharing indicators, with STIX export among its formats and community taxonomies that include TLP. Two of its features are worth copying whatever platform you use. Each event has a distribution level (your organisation only, this community, connected communities, all communities, or a named sharing group), and attributes carry a to_ids flag that separates "actionable indicator" from "context only". MISP also keeps warninglists, lists of well-known values that are likely false positives: public DNS resolvers, CDN ranges, top-site rankings and RFC 1918 private ranges.

On the consuming side, SIEMs speak the same protocol. Microsoft Sentinel, for example, has a TAXII data connector that pulls from TAXII 2.0 or 2.1 servers given an API root URL and a collection ID, and an export connector for TAXII 2.1.

From intel to action, without blocking the internet

Here is where my firewall background makes me cautious. The fastest way to lose trust in threat intel is one bad entry: a CDN address, a public resolver, a cloud provider range, pushed straight into a deny rule. These are my own habits, not a standard:

  1. Run every feed through an allowlist filter first. Warninglists exist because shared feeds do contain popular, legitimate addresses. Your own critical destinations (identity provider, update servers, partner VPN peers) belong on that list too.
  2. Use the firewall's exception mechanism, not feed editing. On Palo Alto Networks firewalls, an External Dynamic List imports IPs, domains or URLs from a web server and refreshes them on a schedule, with no manual commit needed. The EDL settings let you check for updates every five minutes (the default), hourly, daily, weekly or monthly, and add manual exceptions to the list.
  3. Alert before you block. Start new feeds in the SIEM as detections. Move to blocking only the subset that has high confidence, a valid_until, and a sighting or two in your own logs.
  4. Let behaviour drive hunting, and atomic indicators drive short blocks. The attack-pattern in our bundle is a reason to review mail and endpoint detections. The IP is a reason for a block that expires.

Expiry, confidence and handling

A feed without expiry only grows. STIX gives you three tools: valid_until on indicators, revoked: true when the creator says an object is no longer valid (revoking is permanent), and confidence from 0 to 100. Consumers should honour all three, and producers should set them. NIST SP 800-150 also asks producers to provide a way to update and retract indicators that were wrong or shared by mistake.

Handling is the other half. The Traffic Light Protocol sets who may receive shared information. It is "not a formal classification scheme". FIRST's current version, TLP 2.0, has been authoritative since August 2022. It has four labels, RED, AMBER, GREEN and CLEAR, and AMBER+STRICT narrows AMBER to the recipient's organisation only. One detail to check: the TLP marking definitions built into STIX 2.1 are WHITE, GREEN, AMBER and RED, the older TLP names. So check how your tools map them before you rely on an automated "share with community" rule.

Treat the marking as part of the data. If a TLP:AMBER+STRICT indicator ends up in a public blocklist because a tool dropped the marking, that is a sharing failure, even if the block worked.

Closing thought

The IP address in our bundle has a short, useful life. The relationship that says this loader uses spearphishing attachments will still be useful in a year. STIX gives that relationship a shape, TAXII moves it between organisations, and fields like valid_until, confidence and the TLP marking keep it honest once it arrives. If you build only one habit from this post, make it this: never import an indicator you couldn't explain.

Sources

Keep reading