Posted by:

David Greenwood

David Greenwood, Chief of Signal

If you are reading this blog post via a 3rd party source it is very likely that many parts of it will not render correctly. Please view the post on for the full interactive viewing experience.

In this post I will introduce you to a few tools that will help you create and manage STIX 2.1 content.

Over the last two months I have shown many examples of STIX 2.1 content. Today I will lift the curtain and show you exactly how I created it.


cti-python-stix2 from OASIS is a set of Python APIs that allow you to quickly start creating STIX 2.1 content. It is likely to be the tool you use most as a STIX 2.1 producer.

There are a wide range of functions it can be used for. This post aims to cover some of the most common that you will likely want to perform.

To follow along with this tutorial, first clone our tutorial repository and install the cti-python-stix2 library like so;

git clone
cd tutorials/cti-python-stix2-tutorial
python3 -m venv tutorial_env
source tutorial_env/bin/activate
pip3 install -r requirements.txt
cd examples

1. Creating some core Objects

Example Code:

First I will start by creating an Identity SDO (SCIdentitySDO) and Marking Definition (SCMarkingDefinitionStatement) for the content I will create during this post. For this I can use the appropriate classes Identity and MarkingDefinition respectively like so;

SCIdentitySDO = Identity(
SCMarkingDefinitionStatement = MarkingDefinition(

When creating any Object, certain required Properties will be added automatically if not provided as keyword arguments using the class logic. For other Properties you need to explicitly pass the required Properties (as well as any optional properties you want to use).

For the Identity SDO, after reviewing the STIX 2.1 specification, the name Property is required. I have also added a description Property.

SCIdentitySDO = Identity(
                        name="Signal Corps Tutorial",
                        description="Used for tutorial content")

It is exactly the same approach, albeit from a different specification, for the Statement Marking Definition.

Do not forget to import the required classes at the top of the code;

from stix2 import Identity
from stix2 import MarkingDefinition, StatementMarking

When you run the script, you’ll see the objects are stored in the following structure;



  • identity
    • identity--c73bd6f8-6cd0-4b39-a5ec-81c4461f97fb
      • 20220625172129676767.json

2. Creating an Indicator SDO setting Patter Manually

Example Code:

Now I create a more verbose Indicator SDO (in addition to the Identity SDO and Marking Definition).

Indicator0SDOFileHash = Indicator(
                        name="My first SDO",
                        description="Getting started with cti-python-stix2",
                        pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",

You will see I have created the pattern manually in this example. If the Pattern does not conform to the STIX 2.1 Pattern specification, the creation of the Indicator SDO will fail with an error similar to;

Invalid value for Indicator 'pattern': FAIL: Error found at line 1:6. no viable alternative at input 'bad:pattern'

In addition to setting some fields as a string, you can see I can call the previously created SCIdentitySDO Object (at the top of the code) in the created_by_ref Property to set the Data Marking.

I also import the generic STIX 2.1 TLP Objects Marking Definition by importing them (in this case I use TLP_GREEN), in addition to my custom Statement Marking Definition.

from stix2 import MarkingDefinition, StatementMarking, TLP_GREEN

3. Saving Objects to the FileSystemStore




In the last two examples I printed the Objects created. Though it typically makes more sense to save them for reuse later than create them all in one go. For this I can use the FileSystemStore API.

To do this I first create the FileSystemStore in the directory tmp/stix2_store like so;

fs = FileSystemStore("tmp/stix2_store")

Now, instead of just printing the Objects (like in example 2), you will also see them now saved to the FileSystemStore specified (tmp/stix2_store).

from stix2 import FileSystemStore

4. Creating an Indicator SDO with Helpers




In the second example, I declared the Indicator SDO pattern Properties manually. cti-python-stix2 has an API for generating STIX Patterns too.

In this example you can see the Pattern being constructed where I define it as an ObservationExpression with a EqualityComparisonExpression;

setSCOandContributingProperty = ObjectPath("file", ["parent_directory_ref","path"])
fileParentDirectoryPattern = ObservationExpression(EqualityComparisonExpression(setSCOandContributingProperty, "C:\\Windows\\System32"))

And then reference the created pattern in the Indicator SDOs pattern Property (pattern=fileParentDirectoryPattern).

Also, do not forget to call the other Objects from the FileSystemStore that are needed to create the Indicator Object, in this case the Identity SDO created in step 3, referenced in the Indicator SDOs created_by_ref Property;

fs = FileSystemStore("tmp/stix2_store")
SCIdentitySDO = fs.get("identity--c73bd6f8-6cd0-4b39-a5ec-81c4461f97fb")

5. Create a Malware SDO with Granular Marking and Custom Properties




When creating this Malware SDO I have added a Granular Marking Property, applying the marking to the name field.

        "selectors": ["name"],
        "marking_ref": TLP_AMBER

You will see I have also declared a custom_properties Property, with a JSON Object nested within it where the custom a Custom Property (prefixed with x_) and the value is declared.

    "x_foo": "bar"

6. Linking SDOs using the generic SRO




In this example I link the Indicator SDO (created at step 3) to the Malware SDO (created at step 5), using the relationship_type='indicates'.

Indicator0ToMalware0SRO = Relationship(

You can see I also call the following Objects from the FileSystemStore; Indicator0SDOFileHash and Indicator0SDOFileHash, because I need to declare the ID’s of these SDOs in the source_ref and target_ref Properties of the Relationship SRO.

7. Creating an Observed Data SDO with linked SCOs




Let us assume I am tracking some indicators of compromise (an IPv4 address that resolves to two MAC Addresses). By using SCOs linked to an Observed Data SDO I can model this relationship.

I start by creating the two MACAddress SCOs;

MACAddr0SCO = MACAddress(
MACAddr1SCO = MACAddress(

Then I create the IPv4Address SCO, declaring the two MACAddress SCOs under the Property resolves_to_refs;

IPv40SCO = IPv4Address(

Finally, I create the ObservedData SDO, with the object_refs Property declaring the IPv4Address SCO.

ObservedDataSROofIPv40SCO = ObservedData(

8. Linking SDOs using the Sighting SRO




Now I can create a Sighting of the Malware SDO that I know is linked to the Observed Data SDO created in step 7.

To do this in the Sighting SRO, I need to set the

  1. observed_data_ref: the Observed Data SDO created at step 7
  2. sighting_of_ref: the Malware SDO created at step 5
  3. where_sighted_refs: the Identity SDO (I assume the same Identity recorded the Sighting) created at step 3
  4. count: of known Sightings
SightingSRO = Sighting(
                sighting_of_ref = Malware0SDOWithGranularMarkings,
                observed_data_refs = [ObservedDataSROofIPv40SCO],
                where_sighted_refs = [SCIdentitySDO],
                count = 1,
                created_by_ref = SCIdentitySDO

9. Creating a Legacy Custom Object

As noted in 104 Customisation, creating Objects using this method has since been deprecated in the STIX 2.1 Specification. I strongly recommond creating custom Objects or Properties using Extension Definitions instead.




In addition to generating the Custom Object, you can also declare (and restrict) the Properties and corresponding Property values for this Object.

To create the Custom Object and define the predefined and required Properties (aka the specification);

@CustomObject('x-dummy-object', [
    ('property0', properties.StringProperty(required=True)),
    ('property1', properties.StringProperty()),

To restrict the values allowed for a Property, I then create a new class (in this case Dummy class), and declare the values allowed for the property1 Property.

class Dummy(object):
    def __init__(self, property1=None, **kwargs):
        if property1 and property1 not in ['value0', 'value1', 'value2']:
            raise ValueError("'%s' is not a recognized value for property." % property1)

Finally I create the Custom Object using the defined Dummy class in the same way as other Objects, by supplying all the arguments for the class;

Custom0SDO = Dummy(

If I try and pass a Property that is not predefined in the specification (and not as a Custom Property), omit a value that is required, or pass an invalid Property value (e.g. property1=value3), the creation of the Object will fail.

10. Versioning the Indicator SDO




Over time it is very likely the existing Objects will need to be updated and possibly even revoked.

In this case I want to create a new version of the Indicator SDO created at step 3.

To do this, I pass the original version of the Indicator SDO, declaring a new_version adding a new Property not used in the original indicator_types = ["anomalous-activity"].

It is also possible to remove data, passing the Property as None (e.g. description=None will remove a description).

Update0Indicator0SDOFileHash = Indicator0SDOFileHash.new_version(
                                indicator_types = ["anomalous-activity"]

To revoke an Object, instead of using new_version, I can pass revoke() with no arguments. For example;

Update0Indicator0SDOFileHash = Indicator0SDOFileHash.revoked()

In my update I added a new value to the indicator_types Property. Note that the Indicator retains its original id Property (indicator--c7162dea-dbbb-42cf-be6d-fc82daeea352), but you will see two things have happened;

  1. A new .json file for the most recent Indicator SDO is created in the FileSystemStore, see here.
  2. In the new copy of the Indicator SDO, the modified Property time is later than the created Property time (whereas these times are equal in the original – the first version)

11. Bundling all our Objects




Now all that is left to do is package all the Objects in a STIX Bundle.

In the case of the versioned Indicator SDO, the Bundle class will only take the latest version of the Object so I do not need to worry about specifying that.

In the case of the Custom Object, you will notice the final argument to the Bundle class is allow_custom=True.

Without this the bundling will fail due to the inclusion of the Custom Object.

It is important downstream tools are aware your Bundles and/or Objects contain custom content to ensure they are also parsed correctly using allow_custom=True.

Storing STIX 2.1 Objects

In the final part of this tutorial

Discuss this post

Signals Corps Slack

Never miss an update

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