You know that moment after you wire up a “simple” parser, run it once, and suddenly decide you might actually like this STIX thing? Same. This is the post I wish someone handed me on day one — the fastest way to go from zero to “shipping valid STIX 2.1” with Python.

By the end you’ll be able to:

  • Create SDOs and SCOs with the stix2 library.
  • Understand UUIDs (v4 vs v5) and not get bitten by deduping.
  • Version objects the right way (minor vs major).
  • Link things with Relationship SROs.
  • Store, query, and bundle STIX like a grown-up.

Overview

The STIX2 Python library from OASIS is the quickest way to produce valid STIX 2.1. You’ll use it a lot — for prototyping, for pipelines, for tests, for everything.

This guide sticks to the 80/20: just enough API + lots of runnable examples.

Preparation

Create a venv and install:

mkdir stix2_python_tutorial
python3 -m venv stix2_python_tutorial
source stix2_python_tutorial/bin/activate
pip3 install stix2

Now let’s make one SDO and one SCO.


Creating SDOs and SCOs

SDO: Attack Pattern

generate_sdo.py

# python3 generate_sdo.py
## Start by importing all the things you will need
### https://stix2.readthedocs.io/en/latest/api/v21/stix2.v21.sdo.html#stix2.v21.sdo.AttackPattern
### https://stix2.readthedocs.io/en/latest/api/stix2.v21.html?highlight=tlp#stix2.v21.TLPMarking

from stix2 import AttackPattern, TLP_GREEN

## Create AttackPattern SDO using the files 

AttackPatternDemo = AttackPattern(
    created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    name="Spear Phishing",
    description="Used for tutorial content",
    object_marking_refs=[
        TLP_GREEN
    ]
)

## Print all the objects to the command line

print(AttackPatternDemo.serialize(pretty=True))
{
    "type": "attack-pattern",
    "spec_version": "2.1",
    "id": "attack-pattern--794709ca-2407-4da8-a6ec-e4b1e074a18d",
    "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    "created": "2020-01-01T07:38:55.364693Z",
    "modified": "2020-01-01T07:38:55.364693Z",
    "name": "Spear Phishing",
    "description": "Used for tutorial content",
    "object_marking_refs": [
        "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"
    ]
}

The library fills in type, spec_version, id, created, modified (you can pass these manually too).

If you pass a bad value to what is expected (e.g., created_by_ref="dogesec"), you’ll get a helpful validation error. The STIX 2.1 specification defines the properties you can use for each object type.

For reference, the library exposes TLP v1 constants (TLP_WHITE/GREEN/AMBER/RED). TLP v2 is yet to be implemented.

Each time this script is run, the id of the object will change (more about that in ID generation).

SCO: IPv4 Address

generate_sco.py

# python3 generate_sco.py
## Start by importing all the things you will need
### IPv4 SCO https://stix2.readthedocs.io/en/latest/api/stix2.v21.html#stix2.v21.IPv4Address

from stix2 import IPv4Address

## Create IPv4Address SCO using the files 

IPv4AddressDemo = IPv4Address(
    value="177.60.40.7"
)

## Print all the objects to the command line

print(IPv4AddressDemo.serialize(pretty=True))
{
    "type": "ipv4-addr",
    "spec_version": "2.1",
    "id": "ipv4-addr--dc63603e-e634-5357-b239-d4b562bc5445",
    "value": "177.60.40.7"
}

Each time this script is run, the id of the object will remain the same (more about that in ID generation).


A long (but vital) note on ID generation

As you’ve now seen, STIX IDs look like: object-type--UUID. It’s important to understand how the stix2 library generates these IDs:

  • SDOs/SROs/SMOsUUIDv4 by default (random).
  • SCOsUUIDv5 by default (deterministic), based on ID Contributing Properties.

Why you should care: two producers describing the same IPv4 should produce the same SCO ID.

Deterministic IDs for SCOs

domain_uuid_demo.py

# python3 domain_uuid_demo.py
# Docs:
#   DomainName SCO: https://stix2.readthedocs.io/en/latest/api/v21/stix2.v21.observables.html#stix2.v21.observables.DomainName

from stix2 import DomainName

DomainNameDemo = DomainName(value="google.com")
print(DomainNameDemo.serialize(pretty=True))
{
    "type": "domain-name",
    "spec_version": "2.1",
    "id": "domain-name--dd686e37-6889-53bd-8ae1-b1a503452613",
    "value": "google.com"
}

You can run this script again and again, the same ID will be produced.

So why is this happening?

The stix2 library is using UUIDv5 logic to generate SCOs:

  • namespace = 00abedb4-aa42-466c-9c01-fed23315a9b7
  • values = ID Contributing Properties

The ID Contributing Properties are defined in the STIX specification for each SCO.

STIX2 file structure

You can see value is the only property listed for ID Contributing Properties for domain-name SCOs.

This means the ID will always be the same, unless the value property is modified.

domain_uuid_contrib_demo.py

from stix2 import DomainName

DomainNameDemo = DomainName(
    value="google.com",
    resolves_to_refs=["ipv4-addr--dc63603e-e634-5357-b239-d4b562bc5445"]  # must be a list
)
print(DomainNameDemo.serialize(pretty=True))
{
    "type": "domain-name",
    "spec_version": "2.1",
    "id": "domain-name--dd686e37-6889-53bd-8ae1-b1a503452613",
    "resolves_to_refs": [
        "ipv4-addr--dc63603e-e634-5357-b239-d4b562bc5445"
    ],
    "value": "google.com"
}

See the ID remains unchanged.

It’s important to note that some SCOs have ID Contributing Properties that are used for ID generation.

For SDOs/SROs: when to choose UUIDv5

The STIX specification recommends UUIDv4 (random) when generating SDOs and SROs.

However, in many cases you want stable IDs (e.g., generating the same SDO deterministically in a build). A good example of this is CVE ID generation for Vulnerability objects in Vulmatch. We use our own namespace and pass the CVE ID as the value. This is beneficial because we can calculate the STIX ID of a Vulnerability object during search (without querying the database).

# python3 generate_sdo_with_uuidv5.py
import uuid
from uuid import UUID
from stix2 import AttackPattern, TLP_GREEN

namespace = UUID("d2916708-57b9-5636-8689-62f049e9f727")  # your namespace
value = "Some fixed value"
generated_id = "attack-pattern--" + str(uuid.uuid5(namespace, value))

AttackPatternUUID5Demo = AttackPattern(
    id=generated_id,
    created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    name="Spear Phishing",
    description="Used for tutorial content",
    object_marking_refs=[TLP_GREEN],
)

print(AttackPatternUUID5Demo.serialize(pretty=True))
{
    "type": "attack-pattern",
    "spec_version": "2.1",
    "id": "attack-pattern--6b948b5a-3c09-5365-b48a-da95c3964cb5",
    "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    "created": "2020-01-01T11:21:07.478851Z",
    "modified": "2020-01-01T11:21:07.478851Z",
    "name": "Spear Phishing",
    "description": "Used for tutorial content",
    "object_marking_refs": [
        "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da"
    ]
}

Use this intentionally; don’t fight randomness unless you need stable references.


Versioning (a close cousin of IDs)

SDOs/SROs/Language Content SMOs have id, created, modified properties.

  • Minor change → same id, updated modified.
  • Major change → new object (new id).
    • usually only required when a serious error exists in the original object (e.g. wrong identity used)
    • in this case the original object (if not SCO) is also marked with the revoked property

Example of a minor change

For example, let’s use the original Attack Pattern object I generated earlier;

{
    "type": "attack-pattern",
    "spec_version": "2.1",
    "id": "attack-pattern--794709ca-2407-4da8-a6ec-e4b1e074a18d",
    "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    "created": "2020-01-01T07:38:55.364693Z",
    "modified": "2020-01-01T07:38:55.364693Z",
    "name": "Spear Phishing",
    "description": "Used for tutorial content",
    "object_marking_refs": [
        "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"
    ]
}

To update this, and ensure the UUID persists, I can use the new_version function in the stix2 library.

update_sdo.py

# python3 update_sdo.py
## Start by importing all the things you will need
### https://stix2.readthedocs.io/en/latest/api/v21/stix2.v21.sdo.html#stix2.v21.sdo.AttackPattern
### https://stix2.readthedocs.io/en/latest/api/stix2.v21.html?highlight=tlp#stix2.v21.TLPMarking

from stix2 import AttackPattern, TLP_GREEN, new_version

## Create Attack Pattern SDO using the files 

AttackPatternDemo = AttackPattern(
    created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    name="Spear Phishing",
    description="Used for tutorial content",
    object_marking_refs=[
        TLP_GREEN
    ]
)

## Print all the objects to the command line

print(AttackPatternDemo.serialize(pretty=True))

## Update the Attack Pattern SDO

UpdatedAttackPatternDemo = new_version(
    AttackPatternDemo,
    description="new description")

## Print all the objects to the command line

print(UpdatedAttackPatternDemo.serialize(pretty=True))

Prints:

{
    "type": "attack-pattern",
    "spec_version": "2.1",
    "id": "attack-pattern--f6455edf-222b-48c3-8604-d672929cd40e",
    "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    "created": "2020-01-01T08:28:30.688249Z",
    "modified": "2020-01-01T08:28:30.688249Z",
    "name": "Spear Phishing",
    "description": "Used for tutorial content",
    "object_marking_refs": [
        "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da"
    ]
}
{
    "type": "attack-pattern",
    "spec_version": "2.1",
    "id": "attack-pattern--f6455edf-222b-48c3-8604-d672929cd40e",
    "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    "created": "2020-01-01T08:28:30.688249Z",
    "modified": "2020-02-01T07:38:55.364693Z",
    "name": "Spear Phishing",
    "description": "new description",
    "object_marking_refs": [
        "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da"
    ]
}

As you can see the id and created properties persist, but notice how the modified time changes.

SCOs don’t have modified properties. If you change an ID contributing property, you’ve made a major change (new ID).


Creating Relationship SROs

Once you have the two objects you want to join, you can create an SRO to link them.

generate_sro.py

# python3 generate_sro.py
# Docs:
#   Relationship SRO: https://stix2.readthedocs.io/en/latest/api/v21/stix2.v21.sro.html#stix2.v21.sro.Relationship

from stix2 import Relationship

RelationshipDemo = Relationship(
    created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    relationship_type="indicates",
    source_ref="indicator--2f559518-c844-4c4e-bca3-cc97520c164a",
    target_ref="malware--09d22009-b575-4880-889f-6c539157dbc7",
)

print(RelationshipDemo.serialize(pretty=True))
{
    "type": "relationship",
    "spec_version": "2.1",
    "id": "relationship--44ceafde-0027-45cc-bab9-b46c5e001ceb",
    "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    "created": "2020-01-01T15:25:53.686507Z",
    "modified": "2020-01-01T15:25:53.686507Z",
    "relationship_type": "indicates",
    "source_ref": "indicator--2f559518-c844-4c4e-bca3-cc97520c164a",
    "target_ref": "malware--09d22009-b575-4880-889f-6c539157dbc7"
}

Don’t forget to consult the STIX specification for recommended links between source and target objects and the associated relationship_type for them.

Bundling everything together

Bundles are how you ship multiple STIX objects in one payload.

bundle_filesystem_objects.py

from stix2 import AttackPattern, ThreatActor, Relationship, TLP_GREEN, Bundle

ap = AttackPattern(
    created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    name="Spear Phishing",
    object_marking_refs=[TLP_GREEN],
)

ta = ThreatActor(
    created_by_ref="identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
    name="A bad guy",
    threat_actor_types=["sensationalist"],
    object_marking_refs=[TLP_GREEN],
)

rel = Relationship(
    relationship_type="uses",
    source_ref=ta,
    target_ref=ap,
)

bundle = Bundle(objects=[ap, ta, rel])
print(bundle.serialize(pretty=True))
{
    "type": "bundle",
    "id": "bundle--ed31bd4b-46ab-4965-8796-12b4d5ad9fcc",
    "objects": [
        {
            "type": "attack-pattern",
            "spec_version": "2.1",
            "id": "attack-pattern--b2c77df1-7aac-4b02-bdf1-6e71cb023d61",
            "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
            "created": "2020-01-01T08:28:30.688249Z",
            "modified": "2020-01-01T08:28:30.688249Z",
            "name": "Spear Phishing",
            "object_marking_refs": [
                "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da"
            ]
        },
        {
            "type": "threat-actor",
            "spec_version": "2.1",
            "id": "threat-actor--db09d012-6be1-4c08-bd0e-15f6910f1758",
            "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
            "created": "2020-01-01T08:28:30.688249Z",
            "modified": "2020-01-01T08:28:30.688249Z",
            "name": "A bad guy",
            "threat_actor_types": [
                "sensationalist"
            ],
            "object_marking_refs": [
                "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da"
            ]
        },
        {
            "type": "relationship",
            "spec_version": "2.1",
            "id": "relationship--4a58e575-8ace-49b3-9137-26c76aaa25b8",
            "created_by_ref": "identity--9779a2db-f98c-5f4b-8d08-8ee04e02dbb5",
            "created": "2020-01-01T08:28:30.688249Z",
            "modified": "2020-01-01T08:28:30.688249Z",
            "relationship_type": "uses",
            "source_ref": "threat-actor--db09d012-6be1-4c08-bd0e-15f6910f1758",
            "target_ref": "attack-pattern--b2c77df1-7aac-4b02-bdf1-6e71cb023d61"
        }
    ]
}

Cheat Codes

Have a look at our txt2stix code. txt2stix creates a variety of STIX objects using all the ways ID generation is described in this post.


TL;DR

  • Use stix2 to create valid SDOs/SCOs fast.
  • IDs: v4 random for SDO/SRO, v5 deterministic for SCOs (don’t fight that).
  • Versioning: minor → same ID, new modified; major → new object
  • Ship: bundle objects into a single JSON when you share.

That’s it — you’re dangerous now.


Obstracts

The RSS reader for threat intelligence teams. Turn any blog into machine readable STIX 2.1 data ready for use with your security stack.

Obstracts. The RSS reader for threat intelligence teams.

Discuss this post

Head on over to the dogesec community to discuss this post.

dogesec community

Posted by:

David Greenwood

David Greenwood, Do Only Good Everyday



Never miss an update


Sign up to receive new articles in your inbox as they published.

Your subscription could not be saved. Please try again.
Your subscription has been successful.