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 signalscorps.com 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
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 https://github.com/signalscorps/tutorials
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: 01-print-statement-marking-identity.py
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;
<OBJECT TYPE>
<OBJECT UUID>
<OBJECT VERSION>
in YYYYMMDDHHmmSSssssss
e.g.
identity
identity--c73bd6f8-6cd0-4b39-a5ec-81c4461f97fb
20220625172129676767.json
2. Creating an Indicator SDO setting Patter Manually
Example Code: 02-print-indicator0-sdo.py
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",
type='indicator',
pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
pattern_type="stix",
created_by_ref=SCIdentitySDO,
object_marking_refs=[TLP_GREEN,SCMarkingDefinitionStatement]
)
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 import
ing 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
Input
Output
- marking-definition–e55ae95e-54f0-4b00-96a2-678f744c3f8a
- identity–c73bd6f8-6cd0-4b39-a5ec-81c4461f97fb
- indicator–c7162dea-dbbb-42cf-be6d-fc82daeea352
Description
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
fs.add([SCIdentitySDO,SCMarkingDefinitionStatement,Indicator0SDOFileHash])
4. Creating an Indicator SDO with Helpers
Input
Output
Description
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
Input
Output
Description
When creating this Malware SDO I have added a Granular Marking Property, applying the marking to the name
field.
granular_markings=[
{
"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.
custom_properties={
"x_foo": "bar"
},
6. Linking SDOs using the generic SRO
Input
Output
Description
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(
relationship_type='indicates',
source_ref=Indicator0SDOFileHash.id,
target_ref=Malware0SDOWithGranularMarkings.id,
created_by_ref=SCIdentitySDO,
object_marking_refs=TLP_AMBER
)
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
Input
Output
- observed-data–220aadc8-b1d0-42b6-8695-e07fde007588
- mac-addr–f72d7d00-86bd-5cd2-8c86-52f7a83bef62
- mac-addr–875ad625-177b-5c2a-9101-d44b0ad55938
- ipv4-addr–dc63603e-e634-5357-b239-d4b562bc5445
Description
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(
value="a1:b2:c3:d4:e5:f6"
)
MACAddr1SCO = MACAddress(
value="a7:b8:c9:d0:e1:f2"
)
Then I create the IPv4Address
SCO, declaring the two MACAddress
SCOs under the Property resolves_to_refs
;
IPv40SCO = IPv4Address(
value="177.60.40.7",
resolves_to_refs=[MACAddr0SCO.id, MACAddr1SCO.id]
)
Finally, I create the ObservedData
SDO, with the object_refs
Property declaring the IPv4Address
SCO.
ObservedDataSROofIPv40SCO = ObservedData(
object_refs=IPv40SCO,
first_observed="2021-06-25T12:01:23.868289Z",
last_observed="2021-06-25T12:01:23.868289Z",
number_observed="1",
created_by_ref=SCIdentitySDO
)
8. Linking SDOs using the Sighting SRO
Input
Output
Description
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
observed_data_ref
: the Observed Data SDO created at step 7sighting_of_ref
: the Malware SDO created at step 5where_sighted_refs
: the Identity SDO (I assume the same Identity recorded the Sighting) created at step 3count
: 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.
Input
Output
Description
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(
property0="something",
property1="value0",
created_by_ref=SCIdentitySDO
)
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
Input
Output
Description
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;
- A new .json file for the most recent Indicator SDO is created in the FileSystemStore, see here.
- In the new copy of the Indicator SDO, the
modified
Property time is later than thecreated
Property time (whereas these times are equal in the original – the first version)
11. Bundling all our Objects
Input
Output
Description
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

Never miss an update
Sign up to receive new articles in your inbox as they published.