STIX 2 Python API Documentation

Welcome to the STIX 2 Python API’s documentation. This library is designed to help you work with STIX 2 content. For more information about STIX 2, see the website of the OASIS Cyber Threat Intelligence Technical Committee.

Get started with an overview of the library, then take a look at the guides and tutorials to see how to use it. For information about a specific class or function, see the API reference.

Overview

Goals

High level goals/principles of the Python stix2 library:

  1. It should be as easy as possible (but no easier!) to perform common tasks of producing, consuming, and processing STIX 2 content.
  2. It should be hard, if not impossible, to emit invalid STIX 2.
  3. The library should default to doing “the right thing”, complying with both the STIX 2.0 spec, as well as associated best practices. The library should make it hard to do “the wrong thing”.

Design Decisions

To accomplish these goals, and to incorporate lessons learned while developing python-stix (for STIX 1.x), several decisions influenced the design of the stix2 library:

  1. All data structures are immutable by default. In contrast to python-stix, where users would create an object and then assign attributes to it, in stix2 all properties must be provided when creating the object.
  2. Where necessary, library objects should act like dict‘s. When treated as a str, the JSON reprentation of the object should be used.
  3. Core Python data types (including numeric types, datetime) should be used when appropriate, and serialized to the correct format in JSON as specified in the STIX 2 spec.

Architecture

The stix2 library is divided into three logical layers, representing different levels of abstraction useful in different types of scripts and larger applications. It is possible to combine multiple layers in the same program, and the higher levels build on the layers below.

Object Layer

The lowest layer, the Object Layer, is where Python objects representing STIX 2 data types (such as SDOs, SROs, and Cyber Observable Objects, as well as non-top-level objects like External References, Kill Chain phases, and Cyber Observable extensions) are created, and can be serialized and deserialized to and from JSON representation.

This layer is appropriate for stand-alone scripts that produce or consume STIX 2 content, or can serve as a low-level data API for larger applications that need to represent STIX objects as Python classes.

At this level, non-embedded reference properties (those ending in _ref, such as the links from a Relationship object to its source and target objects) are not implemented as references between the Python objects themselves, but by simply having the same values in id and reference properties. There is no referential integrity maintained by the stix2 library.

Environment Layer

The Environment Layer adds several components that make it easier to handle STIX 2 data as part of a larger application and as part of a larger cyber threat intelligence ecosystem.

  • Data Sources represent locations from which STIX data can be retrieved, such as a TAXII server, database, or local filesystem. The Data Source API abstracts differences between these storage location, giving a common API to get objects by ID or query by various properties, as well as allowing federated operations over multiple data sources.
  • Similarly, Data Sink objects represent destinations for sending STIX data.
  • An Object Factory provides a way to add common properties to all created objects (such as the same created_by_ref, or a StatementMarking with copyright information or terms of use for the STIX data).

Each of these components can be used individually, or combined as part of an Environment. These Environment objects allow different settings to be used by different users of a multi-user application (such as a web application). For more information, check out this Environment tutorial.

Workbench Layer

The highest layer of the stix2 APIs is the Workbench Layer, designed for a single user in a highly-interactive analytical environment (such as a Jupyter Notebook). It builds on the lower layers of the API, while hiding most of their complexity. Unlike the other layers, this layer is designed to be used directly by end users. For users who are comfortable with Python, the Workbench Layer makes it easy to quickly interact with STIX data from a variety of sources without needing to write and run one-off Python scripts. For more information, check out this Workbench tutorial.

User’s Guide

This section of documentation contains guides and tutorials on how to use the stix2 library.

Creating STIX Content

Creating STIX Domain Objects

To create a STIX object, provide keyword arguments to the type’s constructor:

In [3]:
from stix2 import Indicator

indicator = Indicator(name="File hash for malware variant",
                      labels=["malicious-activity"],
                      pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
print(indicator)
Out[3]:
{
    "type": "indicator",
    "id": "indicator--548af3be-39d7-4a3e-93c2-1a63cccf8951",
    "created": "2018-04-05T18:32:24.193Z",
    "modified": "2018-04-05T18:32:24.193Z",
    "name": "File hash for malware variant",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T18:32:24.193659Z",
    "labels": [
        "malicious-activity"
    ]
}

Certain required attributes of all objects will be set automatically if not provided as keyword arguments:

  • If not provided, type will be set automatically to the correct type. You can also provide the type explicitly, but this is not necessary:
In [4]:
indicator2 = Indicator(type='indicator',
                       labels=["malicious-activity"],
                       pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")

Passing a value for type that does not match the class being constructed will cause an error:

In [5]:
indicator3 = Indicator(type='xxx',
                       labels=["malicious-activity"],
                       pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
InvalidValueError: Invalid value for Indicator 'type': must equal 'indicator'.

  • If not provided, id will be generated randomly. If you provide an id argument, it must begin with the correct prefix:
In [6]:
indicator4 = Indicator(id="campaign--63ce9068-b5ab-47fa-a2cf-a602ea01f21a",
                       labels=["malicious-activity"],
                       pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
InvalidValueError: Invalid value for Indicator 'id': must start with 'indicator--'.

For indicators, labels and pattern are required and cannot be set automatically. Trying to create an indicator that is missing one of these properties will result in an error:

In [7]:
indicator = Indicator()
MissingPropertiesError: No values for required properties for Indicator: (labels, pattern).

However, the required valid_from attribute on Indicators will be set to the current time if not provided as a keyword argument.

Once created, the object acts like a frozen dictionary. Properties can be accessed using the standard Python dictionary syntax:

In [8]:
indicator['name']
Out[8]:
'File hash for malware variant'

Or access properties using the standard Python attribute syntax:

In [9]:
indicator.name
Out[9]:
'File hash for malware variant'

Attempting to modify any attributes will raise an error:

In [10]:
indicator['name'] = "This is a revised name"
TypeError: 'Indicator' object does not support item assignment

In [11]:
indicator.name = "This is a revised name"
ImmutableError: Cannot modify 'name' property in 'Indicator' after creation.

To update the properties of an object, see the Versioning section.

Creating a Malware object follows the same pattern:

In [12]:
from stix2 import Malware

malware = Malware(name="Poison Ivy",
                  labels=['remote-access-trojan'])
print(malware)
Out[12]:
{
    "type": "malware",
    "id": "malware--3d7f0c1c-616a-4868-aa7b-150821d2a429",
    "created": "2018-04-05T18:32:46.584Z",
    "modified": "2018-04-05T18:32:46.584Z",
    "name": "Poison Ivy",
    "labels": [
        "remote-access-trojan"
    ]
}

As with indicators, the type, id, created, and modified properties will be set automatically if not provided. For Malware objects, the labels and name properties must be provided.

You can see the full list of SDO classes here.

Creating Relationships

STIX 2 Relationships are separate objects, not properties of the object on either side of the relationship. They are constructed similarly to other STIX objects. The type, id, created, and modified properties are added automatically if not provided. Callers must provide the relationship_type, source_ref, and target_ref properties.

In [13]:
from stix2 import Relationship

relationship = Relationship(relationship_type='indicates',
                            source_ref=indicator.id,
                            target_ref=malware.id)
print(relationship)
Out[13]:
{
    "type": "relationship",
    "id": "relationship--34ddc7b4-4965-4615-b286-1c8bbaa1e7db",
    "created": "2018-04-05T18:32:49.474Z",
    "modified": "2018-04-05T18:32:49.474Z",
    "relationship_type": "indicates",
    "source_ref": "indicator--548af3be-39d7-4a3e-93c2-1a63cccf8951",
    "target_ref": "malware--3d7f0c1c-616a-4868-aa7b-150821d2a429"
}

The source_ref and target_ref properties can be either the ID’s of other STIX objects, or the STIX objects themselves. For readability, Relationship objects can also be constructed with the source_ref, relationship_type, and target_ref as positional (non-keyword) arguments:

In [14]:
relationship2 = Relationship(indicator, 'indicates', malware)
print(relationship2)
Out[14]:
{
    "type": "relationship",
    "id": "relationship--0a646403-f7e7-4cfd-b945-cab5cde05857",
    "created": "2018-04-05T18:32:51.417Z",
    "modified": "2018-04-05T18:32:51.417Z",
    "relationship_type": "indicates",
    "source_ref": "indicator--548af3be-39d7-4a3e-93c2-1a63cccf8951",
    "target_ref": "malware--3d7f0c1c-616a-4868-aa7b-150821d2a429"
}

Creating Bundles

STIX Bundles can be created by passing objects as arguments to the Bundle constructor. All required properties (type, id, and spec_version) will be set automatically if not provided, or can be provided as keyword arguments:

In [15]:
from stix2 import Bundle

bundle = Bundle(indicator, malware, relationship)
print(bundle)
Out[15]:
{
    "type": "bundle",
    "id": "bundle--f83477e5-f853-47e1-a267-43f3aa1bd5b0",
    "spec_version": "2.0",
    "objects": [
        {
            "type": "indicator",
            "id": "indicator--548af3be-39d7-4a3e-93c2-1a63cccf8951",
            "created": "2018-04-05T18:32:24.193Z",
            "modified": "2018-04-05T18:32:24.193Z",
            "name": "File hash for malware variant",
            "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
            "valid_from": "2018-04-05T18:32:24.193659Z",
            "labels": [
                "malicious-activity"
            ]
        },
        {
            "type": "malware",
            "id": "malware--3d7f0c1c-616a-4868-aa7b-150821d2a429",
            "created": "2018-04-05T18:32:46.584Z",
            "modified": "2018-04-05T18:32:46.584Z",
            "name": "Poison Ivy",
            "labels": [
                "remote-access-trojan"
            ]
        },
        {
            "type": "relationship",
            "id": "relationship--34ddc7b4-4965-4615-b286-1c8bbaa1e7db",
            "created": "2018-04-05T18:32:49.474Z",
            "modified": "2018-04-05T18:32:49.474Z",
            "relationship_type": "indicates",
            "source_ref": "indicator--548af3be-39d7-4a3e-93c2-1a63cccf8951",
            "target_ref": "malware--3d7f0c1c-616a-4868-aa7b-150821d2a429"
        }
    ]
}

Custom STIX Content

Custom Properties

Attempting to create a STIX object with properties not defined by the specification will result in an error. Try creating an Identity object with a custom x_foo property:

In [3]:
from stix2 import Identity

Identity(name="John Smith",
         identity_class="individual",
         x_foo="bar")
ExtraPropertiesError: Unexpected properties for Identity: (x_foo).

To create a STIX object with one or more custom properties, pass them in as a dictionary parameter called custom_properties:

In [4]:
from stix2 import Identity

identity = Identity(name="John Smith",
                    identity_class="individual",
                    custom_properties={
                        "x_foo": "bar"
                    })
print(identity)
Out[4]:
{
    "type": "identity",
    "id": "identity--87aac643-341b-413a-b702-ea5820416155",
    "created": "2018-04-05T18:38:10.269Z",
    "modified": "2018-04-05T18:38:10.269Z",
    "name": "John Smith",
    "identity_class": "individual",
    "x_foo": "bar"
}

Alternatively, setting allow_custom to True will allow custom properties without requiring a custom_properties dictionary.

In [5]:
identity2 = Identity(name="John Smith",
                     identity_class="individual",
                     x_foo="bar",
                     allow_custom=True)
print(identity2)
Out[5]:
{
    "type": "identity",
    "id": "identity--a1ad0a6f-39ab-4642-9a72-aaa198b1eee2",
    "created": "2018-04-05T18:38:12.270Z",
    "modified": "2018-04-05T18:38:12.270Z",
    "name": "John Smith",
    "identity_class": "individual",
    "x_foo": "bar"
}

Likewise, when parsing STIX content with custom properties, pass allow_custom=True to parse():

In [6]:
from stix2 import parse

input_string = """{
    "type": "identity",
    "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
    "created": "2015-12-21T19:59:11Z",
    "modified": "2015-12-21T19:59:11Z",
    "name": "John Smith",
    "identity_class": "individual",
    "x_foo": "bar"
}"""
identity3 = parse(input_string, allow_custom=True)
print(identity3.x_foo)
Out[6]:
bar

Custom STIX Object Types

To create a custom STIX object type, define a class with the @CustomObject decorator. It takes the type name and a list of property tuples, each tuple consisting of the property name and a property instance. Any special validation of the properties can be added by supplying an __init__ function.

Let’s say zoo animals have become a serious cyber threat and we want to model them in STIX using a custom object type. Let’s use a species property to store the kind of animal, and make that property required. We also want a property to store the class of animal, such as “mammal” or “bird” but only want to allow specific values in it. We can add some logic to validate this property in __init__.

In [7]:
from stix2 import CustomObject, properties

@CustomObject('x-animal', [
    ('species', properties.StringProperty(required=True)),
    ('animal_class', properties.StringProperty()),
])
class Animal(object):
    def __init__(self, animal_class=None, **kwargs):
        if animal_class and animal_class not in ['mammal', 'bird', 'fish', 'reptile']:
            raise ValueError("'%s' is not a recognized class of animal." % animal_class)

Now we can create an instance of our custom Animal type.

In [8]:
animal = Animal(species="lion",
                animal_class="mammal")
print(animal)
Out[8]:
{
    "type": "x-animal",
    "id": "x-animal--b1e4fe7f-7985-451d-855c-6ba5c265b22a",
    "created": "2018-04-05T18:38:19.790Z",
    "modified": "2018-04-05T18:38:19.790Z",
    "species": "lion",
    "animal_class": "mammal"
}

Trying to create an Animal instance with an animal_class that’s not in the list will result in an error:

In [9]:
Animal(species="xenomorph",
       animal_class="alien")
ValueError: 'alien' is not a recognized class of animal.

Parsing custom object types that you have already defined is simple and no different from parsing any other STIX object.

In [10]:
input_string2 = """{
    "type": "x-animal",
    "id": "x-animal--941f1471-6815-456b-89b8-7051ddf13e4b",
    "created": "2015-12-21T19:59:11Z",
    "modified": "2015-12-21T19:59:11Z",
    "species": "shark",
    "animal_class": "fish"
}"""
animal2 = parse(input_string2)
print(animal2.species)
Out[10]:
shark

However, parsing custom object types which you have not defined will result in an error:

In [11]:
input_string3 = """{
    "type": "x-foobar",
    "id": "x-foobar--d362beb5-a04e-4e6b-a030-b6935122c3f9",
    "created": "2015-12-21T19:59:11Z",
    "modified": "2015-12-21T19:59:11Z",
    "bar": 1,
    "baz": "frob"
}"""
parse(input_string3)
ParseError: Can't parse unknown object type 'x-foobar'! For custom types, use the CustomObject decorator.

Custom Cyber Observable Types

Similar to custom STIX object types, use a decorator to create custom Cyber Observable types. Just as before, __init__() can hold additional validation, but it is not necessary.

In [12]:
from stix2 import CustomObservable

@CustomObservable('x-new-observable', [
    ('a_property', properties.StringProperty(required=True)),
    ('property_2', properties.IntegerProperty()),
])
class NewObservable():
    pass

new_observable = NewObservable(a_property="something",
                               property_2=10)
print(new_observable)
Out[12]:
{
    "type": "x-new-observable",
    "a_property": "something",
    "property_2": 10
}

Likewise, after the custom Cyber Observable type has been defined, it can be parsed.

In [13]:
from stix2 import ObservedData

input_string4 = """{
    "type": "observed-data",
    "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
    "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
    "created": "2016-04-06T19:58:16.000Z",
    "modified": "2016-04-06T19:58:16.000Z",
    "first_observed": "2015-12-21T19:00:00Z",
    "last_observed": "2015-12-21T19:00:00Z",
    "number_observed": 50,
    "objects": {
        "0": {
            "type": "x-new-observable",
            "a_property": "foobaz",
            "property_2": 5
        }
    }
}"""
obs_data = parse(input_string4)
print(obs_data.objects["0"].a_property)
print(obs_data.objects["0"].property_2)
Out[13]:
foobaz
Out[13]:
5

Custom Cyber Observable Extensions

Finally, custom extensions to existing Cyber Observable types can also be created. Just use the @CustomExtension decorator. Note that you must provide the Cyber Observable class to which the extension applies. Again, any extra validation of the properties can be implemented by providing an __init__() but it is not required. Let’s say we want to make an extension to the File Cyber Observable Object:

In [16]:
from stix2 import File, CustomExtension

@CustomExtension(File, 'x-new-ext', [
    ('property1', properties.StringProperty(required=True)),
    ('property2', properties.IntegerProperty()),
])
class NewExtension():
    pass

new_ext = NewExtension(property1="something",
                       property2=10)
print(new_ext)
Out[16]:
{
    "property1": "something",
    "property2": 10
}

Once the custom Cyber Observable extension has been defined, it can be parsed.

In [17]:
input_string5 = """{
    "type": "observed-data",
    "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
    "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
    "created": "2016-04-06T19:58:16.000Z",
    "modified": "2016-04-06T19:58:16.000Z",
    "first_observed": "2015-12-21T19:00:00Z",
    "last_observed": "2015-12-21T19:00:00Z",
    "number_observed": 50,
    "objects": {
        "0": {
            "type": "file",
            "name": "foo.bar",
            "hashes": {
                "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f"
            },
            "extensions": {
                "x-new-ext": {
                    "property1": "bla",
                    "property2": 50
                }
            }
        }
    }
}"""
obs_data2 = parse(input_string5)
print(obs_data2.objects["0"].extensions["x-new-ext"].property1)
print(obs_data2.objects["0"].extensions["x-new-ext"].property2)
Out[17]:
bla
Out[17]:
50

DataStore API

The stix2 library features an interface for pulling and pushing STIX 2 content. This interface consists of DataStore, DataSource and DataSink constructs: a DataSource for pulling STIX 2 content, a DataSink for pushing STIX 2 content, and a DataStore for both pulling and pushing.

The DataStore, DataSource, DataSink (collectively referred to as the “DataStore suite”) APIs are not referenced directly by a user but are used as base classes, which are then subclassed by real DataStore suites. The stix2 library provides the DataStore suites of FileSystem, Memory, and TAXII. Users are also encouraged to subclass the base classes and create their own custom DataStore suites.

CompositeDataSource

CompositeDataSource is an available controller that can be used as a single interface to a set of defined DataSources. The purpose of this controller is allow for the grouping of DataSources and making get()/query() calls to a set of DataSources in one API call. CompositeDataSources can be used to organize/group DataSources, federate get()/all_versions()/query() calls, and reduce user code.

CompositeDataSource is just a wrapper around a set of defined DataSources (e.g. FileSystemSource) that federates get()/all_versions()/query() calls individually to each of the attached DataSources , collects the results from each DataSource and returns them.

Filters can be attached to CompositeDataSources just as they can be done to DataStores and DataSources. When get()/all_versions()/query() calls are made to the CompositeDataSource, it will pass along any query filters from the call and any of its own filters to the attached DataSources. In addition, those DataSources may have their own attached filters as well. The effect is that all the filters are eventually combined when the get()/all_versions()/query() call is actually executed within a DataSource.

A CompositeDataSource can also be attached to a CompositeDataSource for multiple layers of grouped DataSources.

CompositeDataSource API
CompositeDataSource Examples
In [4]:
from taxii2client import Collection
from stix2 import CompositeDataSource, FileSystemSource, TAXIICollectionSource

# create FileSystemStore
fs = FileSystemSource("/tmp/stix2_source")

# create TAXIICollectionSource
colxn = Collection('http://127.0.0.1:5000/trustgroup1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/')
ts = TAXIICollectionSource(colxn)

# add them both to the CompositeDataSource
cs = CompositeDataSource()
cs.add_data_sources([fs,ts])

# get an object that is only in the filesystem
intrusion_set = cs.get('intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a')
print(intrusion_set)

# get an object that is only in the TAXII collection
ind = cs.get('indicator--02b90f02-a96a-43ee-88f1-1e87297941f2')
print(ind)
Out[4]:
{
    "type": "intrusion-set",
    "id": "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a",
    "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
    "created": "2017-05-31T21:31:53.197Z",
    "modified": "2017-05-31T21:31:53.197Z",
    "name": "DragonOK",
    "description": "DragonOK is a threat group that has targeted Japanese organizations with phishing emails. Due to overlapping TTPs, including similar custom tools, DragonOK is thought to have a direct or indirect relationship with the threat group Moafee. [[Citation: Operation Quantum Entanglement]][[Citation: Symbiotic APT Groups]] It is known to use a variety of malware, including Sysget/HelloBridge, PlugX, PoisonIvy, FormerFirstRat, NFlog, and NewCT. [[Citation: New DragonOK]]",
    "aliases": [
        "DragonOK"
    ],
    "external_references": [
        {
            "source_name": "mitre-attack",
            "url": "https://attack.mitre.org/wiki/Group/G0017",
            "external_id": "G0017"
        },
        {
            "source_name": "Operation Quantum Entanglement",
            "description": "Haq, T., Moran, N., Vashisht, S., Scott, M. (2014, September). OPERATION QUANTUM ENTANGLEMENT. Retrieved November 4, 2015.",
            "url": "https://www.fireeye.com/content/dam/fireeye-www/global/en/current-threats/pdfs/wp-operation-quantum-entanglement.pdf"
        },
        {
            "source_name": "Symbiotic APT Groups",
            "description": "Haq, T. (2014, October). An Insight into Symbiotic APT Groups. Retrieved November 4, 2015.",
            "url": "https://dl.mandiant.com/EE/library/MIRcon2014/MIRcon%202014%20R&D%20Track%20Insight%20into%20Symbiotic%20APT.pdf"
        },
        {
            "source_name": "New DragonOK",
            "description": "Miller-Osborn, J., Grunzweig, J.. (2015, April). Unit 42 Identifies New DragonOK Backdoor Malware Deployed Against Japanese Targets. Retrieved November 4, 2015.",
            "url": "http://researchcenter.paloaltonetworks.com/2015/04/unit-42-identifies-new-dragonok-backdoor-malware-deployed-against-japanese-targets/"
        }
    ],
    "object_marking_refs": [
        "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
    ]
}
Out[4]:
{
    "type": "indicator",
    "id": "indicator--02b90f02-a96a-43ee-88f1-1e87297941f2",
    "created": "2017-11-13T07:00:24.000Z",
    "modified": "2017-11-13T07:00:24.000Z",
    "name": "Ransomware IP Blocklist",
    "description": "IP Blocklist address from abuse.ch",
    "pattern": "[ ipv4-addr:value = '91.237.247.24' ]",
    "valid_from": "2017-11-13T07:00:24Z",
    "labels": [
        "malicious-activity",
        "Ransomware",
        "Botnet",
        "C&C"
    ],
    "external_references": [
        {
            "source_name": "abuse.ch",
            "url": "https://ransomwaretracker.abuse.ch/blocklist/"
        }
    ]
}

Filters

The stix2 DataStore suites - FileSystem, Memory, and TAXII - all use the Filters module to allow for the querying of STIX content. Filters can be used to explicitly include or exclude results with certain criteria. For example:

  • only trust content from a set of object creators
  • exclude content from certain (untrusted) object creators
  • only include content with a confidence above a certain threshold (once confidence is added to STIX 2)
  • only return content that can be shared with external parties (e.g. only content that has TLP:GREEN markings)

Filters can be created and supplied with every call to query(), and/or attached to a DataStore so that every future query placed to that DataStore is evaluated against the attached filters, supplemented with any further filters supplied with the query call. Attached filters can also be removed from DataStores.

Filters are very simple, as they consist of a field name, comparison operator and an object property value (i.e. value to compare to). All properties of STIX 2 objects can be filtered on. In addition, TAXII 2 Filtering parameters for fields can also be used in filters.

TAXII2 filter fields:

  • added_after
  • id
  • type
  • version

Supported operators:

  • =
  • !=
  • in
  • >
  • <
  • >=
  • <=

Value types of the property values must be one of these (Python) types:

  • bool
  • dict
  • float
  • int
  • list
  • str
  • tuple
Filter Examples
In [3]:
import sys
from stix2 import Filter

# create filter for STIX objects that have external references to MITRE ATT&CK framework
f = Filter("external_references.source_name", "=", "mitre-attack")

# create filter for STIX objects that are not of SDO type Attack-Pattnern
f1 = Filter("type", "!=", "attack-pattern")

# create filter for STIX objects that have the "threat-report" label
f2 = Filter("labels", "in", "threat-report")

# create filter for STIX objects that have been modified past the timestamp
f3 = Filter("modified", ">=", "2017-01-28T21:33:10.772474Z")

# create filter for STIX objects that have been revoked
f4 = Filter("revoked", "=", True)

For Filters to be applied to a query, they must be either supplied with the query call or attached to a DataStore, more specifically to a DataSource whether that DataSource is a part of a DataStore or stands by itself.

In [6]:
from stix2 import MemoryStore, FileSystemStore, FileSystemSource

fs = FileSystemStore("/tmp/stix2_store")
fs_source = FileSystemSource("/tmp/stix2_source")

# attach filter to FileSystemStore
fs.source.filters.add(f)

# attach multiple filters to FileSystemStore
fs.source.filters.add([f1,f2])

# can also attach filters to a Source
# attach multiple filters to FileSystemSource
fs_source.filters.add([f3, f4])


mem = MemoryStore()

# As it is impractical to only use MemorySink or MemorySource,
# attach a filter to a MemoryStore
mem.source.filters.add(f)

# attach multiple filters to a MemoryStore
mem.source.filters.add([f1,f2])

De-Referencing Relationships

Given a STIX object, there are several ways to find other STIX objects related to it. To illustrate this, let’s first create a DataStore and add some objects and relationships.

In [10]:
from stix2 import Campaign, Identity, Indicator, Malware, Relationship

mem = MemoryStore()
cam = Campaign(name='Charge', description='Attack!')
idy = Identity(name='John Doe', identity_class="individual")
ind = Indicator(labels=['malicious-activity'], pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']")
mal = Malware(labels=['ransomware'], name="Cryptolocker", created_by_ref=idy)
rel1 = Relationship(ind, 'indicates', mal,)
rel2 = Relationship(mal, 'targets', idy)
rel3 = Relationship(cam, 'uses', mal)
mem.add([cam, idy, ind, mal, rel1, rel2, rel3])

If a STIX object has a created_by_ref property, you can use the creator_of() method to retrieve the Identity object that created it.

In [11]:
print(mem.creator_of(mal))
Out[11]:
{
    "type": "identity",
    "id": "identity--b67cf8d4-cc1a-4bb7-9402-fffcff17c9a9",
    "created": "2018-04-05T20:43:54.117Z",
    "modified": "2018-04-05T20:43:54.117Z",
    "name": "John Doe",
    "identity_class": "individual"
}

Use the relationships() method to retrieve all the relationship objects that reference a STIX object.

In [12]:
rels = mem.relationships(mal)
len(rels)
Out[12]:
3

You can limit it to only specific relationship types:

In [13]:
mem.relationships(mal, relationship_type='indicates')
Out[13]:
[Relationship(type='relationship', id='relationship--3b9cb248-5c2c-425d-85d0-680bfef6e69d', created='2018-04-05T20:43:54.134Z', modified='2018-04-05T20:43:54.134Z', relationship_type='indicates', source_ref='indicator--61deb2a5-305a-490e-83b3-9839a9677368', target_ref='malware--9fe343d8-edf7-4f4a-bb6c-a221fb75142d')]

You can limit it to only relationships where the given object is the source:

In [14]:
mem.relationships(mal, source_only=True)
Out[14]:
[Relationship(type='relationship', id='relationship--8d322508-423b-4d51-be85-a95ad083f8af', created='2018-04-05T20:43:54.134Z', modified='2018-04-05T20:43:54.134Z', relationship_type='targets', source_ref='malware--9fe343d8-edf7-4f4a-bb6c-a221fb75142d', target_ref='identity--b67cf8d4-cc1a-4bb7-9402-fffcff17c9a9')]

And you can limit it to only relationships where the given object is the target:

In [15]:
mem.relationships(mal, target_only=True)
Out[15]:
[Relationship(type='relationship', id='relationship--3b9cb248-5c2c-425d-85d0-680bfef6e69d', created='2018-04-05T20:43:54.134Z', modified='2018-04-05T20:43:54.134Z', relationship_type='indicates', source_ref='indicator--61deb2a5-305a-490e-83b3-9839a9677368', target_ref='malware--9fe343d8-edf7-4f4a-bb6c-a221fb75142d'),
 Relationship(type='relationship', id='relationship--93e5afe0-d1fb-4315-8d08-10951f7a99b6', created='2018-04-05T20:43:54.134Z', modified='2018-04-05T20:43:54.134Z', relationship_type='uses', source_ref='campaign--edfd885c-bc31-4051-9bc2-08e057542d56', target_ref='malware--9fe343d8-edf7-4f4a-bb6c-a221fb75142d')]

Finally, you can retrieve all STIX objects related to a given STIX object using related_to(). This calls relationships() but then performs the extra step of getting the objects that these Relationships point to. related_to() takes all the same arguments that relationships() does.

In [16]:
mem.related_to(mal, target_only=True, relationship_type='uses')
Out[16]:
[Campaign(type='campaign', id='campaign--edfd885c-bc31-4051-9bc2-08e057542d56', created='2018-04-05T20:43:54.117Z', modified='2018-04-05T20:43:54.117Z', name='Charge', description='Attack!')]

Using Environments

An Environment object makes it easier to use STIX 2 content as part of a larger application or ecosystem. It allows you to abstract away the nasty details of sending and receiving STIX data, and to create STIX objects with default values for common properties.

Storing and Retrieving STIX Content

An Environment can be set up with a DataStore if you want to store and retrieve STIX content from the same place.

In [3]:
from stix2 import Environment, MemoryStore

env = Environment(store=MemoryStore())

If desired, you can instead set up an Environment with different data sources and sinks. In the following example we set up an environment that retrieves objects from memory and a directory on the filesystem, and stores objects in a different directory on the filesystem.

In [6]:
from stix2 import CompositeDataSource, FileSystemSink, FileSystemSource, MemorySource

src = CompositeDataSource()
src.add_data_sources([MemorySource(), FileSystemSource("/tmp/stix2_source")])
env2 = Environment(source=src,
                   sink=FileSystemSink("/tmp/stix2_sink"))

Once you have an Environment you can store some STIX content in its DataSinks with add():

In [7]:
from stix2 import Indicator

indicator = Indicator(id="indicator--01234567-89ab-cdef-0123-456789abcdef",
                      labels=["malicious-activity"],
                      pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
env.add(indicator)

You can retrieve STIX objects from the DataSources in the Environment with get(), query(), all_versions(), creator_of(), related_to(), and relationships() just as you would for a DataSource.

In [8]:
print(env.get("indicator--01234567-89ab-cdef-0123-456789abcdef"))
Out[8]:
{
    "type": "indicator",
    "id": "indicator--01234567-89ab-cdef-0123-456789abcdef",
    "created": "2018-04-05T19:27:53.923Z",
    "modified": "2018-04-05T19:27:53.923Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:27:53.923548Z",
    "labels": [
        "malicious-activity"
    ]
}

Creating STIX Objects With Defaults

To create STIX objects with default values for certain properties, use an ObjectFactory. For instance, say we want all objects we create to have a created_by_ref property pointing to the Identity object representing our organization.

In [13]:
from stix2 import Indicator, ObjectFactory

factory = ObjectFactory(created_by_ref="identity--311b2d2d-f010-5473-83ec-1edf84858f4c")

Once you’ve set up the ObjectFactory, use its create() method, passing in the class for the type of object you wish to create, followed by the other properties and their values for the object.

In [14]:
ind = factory.create(Indicator,
                     labels=["malicious-activity"],
                     pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
print(ind)
Out[14]:
{
    "type": "indicator",
    "id": "indicator--c1b421c0-9c6b-4276-9b73-1b8684a5a0d2",
    "created_by_ref": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
    "created": "2018-04-05T19:28:48.776Z",
    "modified": "2018-04-05T19:28:48.776Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:28:48.776442Z",
    "labels": [
        "malicious-activity"
    ]
}

All objects we create with that ObjectFactory will automatically get the default value for created_by_ref. These are the properties for which defaults can be set:

  • created_by_ref
  • created
  • external_references
  • object_marking_refs

These defaults can be bypassed. For example, say you have an Environment with multiple default values but want to create an object with a different value for created_by_ref, or none at all.

In [15]:
factory2 = ObjectFactory(created_by_ref="identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
                         created="2017-09-25T18:07:46.255472Z")
env2 = Environment(factory=factory2)

ind2 = env2.create(Indicator,
                   created_by_ref=None,
                   labels=["malicious-activity"],
                   pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
print(ind2)
Out[15]:
{
    "type": "indicator",
    "id": "indicator--30a3b39c-5f57-4e7f-9eaf-e1abcb643da4",
    "created": "2017-09-25T18:07:46.255Z",
    "modified": "2017-09-25T18:07:46.255Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:28:53.268567Z",
    "labels": [
        "malicious-activity"
    ]
}
In [16]:
ind3 = env2.create(Indicator,
                       created_by_ref="identity--962cabe5-f7f3-438a-9169-585a8c971d12",
                       labels=["malicious-activity"],
                       pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
print(ind3)
Out[16]:
{
    "type": "indicator",
    "id": "indicator--6c5bbaaf-6dac-44b0-a0df-86c27b3f6ecb",
    "created_by_ref": "identity--962cabe5-f7f3-438a-9169-585a8c971d12",
    "created": "2017-09-25T18:07:46.255Z",
    "modified": "2017-09-25T18:07:46.255Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:29:56.55129Z",
    "labels": [
        "malicious-activity"
    ]
}

For the full power of the Environment layer, create an Environment with both a DataStore/Source/Sink and an ObjectFactory:

In [17]:
environ = Environment(ObjectFactory(created_by_ref="identity--311b2d2d-f010-5473-83ec-1edf84858f4c"),
                      MemoryStore())

i = environ.create(Indicator,
                   labels=["malicious-activity"],
                   pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")
environ.add(i)
print(environ.get(i.id))
Out[17]:
{
    "type": "indicator",
    "id": "indicator--d1b8c3f6-1de1-44c1-b079-3df307224a0d",
    "created_by_ref": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
    "created": "2018-04-05T19:29:59.605Z",
    "modified": "2018-04-05T19:29:59.605Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:29:59.605463Z",
    "labels": [
        "malicious-activity"
    ]
}

FileSystem

The FileSystem suite contains FileSystemStore, FileSystemSource and FileSystemSink. Under the hood, all FileSystem objects point to a file directory (on disk) that contains STIX 2 content.

The directory and file structure of the intended STIX 2 content should be:

stix2_content/
    /STIX2 Domain Object type
        STIX2 Domain Object
        STIX2 Domain Object
            .
            .
            .
    /STIX2 Domain Object type
        STIX2 Domain Object
        STIX2 Domain Object
            .
            .
            .
        .
        .
        .
    /STIX2 Domain Object type

The master STIX 2 content directory contains subdirectories, each of which aligns to a STIX 2 domain object type (i.e. “attack-pattern”, “campaign”, “malware”, etc.). Within each STIX 2 domain object subdirectory are JSON files that are STIX 2 domain objects of the specified type. The name of the json files correspond to the ID of the STIX 2 domain object found within that file. A real example of the FileSystem directory structure:

stix2_content/
    /attack-pattern
        attack-pattern--00d0b012-8a03-410e-95de-5826bf542de6.json
        attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22.json
        attack-pattern--1b7ba276-eedc-4951-a762-0ceea2c030ec.json
    /campaign
    /course-of-action
        course-of-action--2a8de25c-f743-4348-b101-3ee33ab5871b.json
        course-of-action--2c3ce852-06a2-40ee-8fe6-086f6402a739.json
    /identity
        identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5.json
    /indicator
    /intrusion-set
    /malware
        malware--1d808f62-cf63-4063-9727-ff6132514c22.json
        malware--2eb9b131-d333-4a48-9eb4-d8dec46c19ee.json
    /observed-data
    /report
    /threat-actor
    /vulnerability

FileSystemStore is intended for use cases where STIX 2 content is retrieved and pushed to the same file directory. As FileSystemStore is just a wrapper around a paired FileSystemSource and FileSystemSink that point the same file directory.

For use cases where STIX 2 content will only be retrieved or pushed, then a FileSystemSource and FileSystemSink can be used individually. They can also be used individually when STIX 2 content will be retrieved from one distinct file directory and pushed to another.

FileSystem API

A note on get(), all_versions(), and query(): The format of the STIX2 content targeted by the FileSystem suite is JSON files. When the FileSystemStore retrieves STIX 2 content (in JSON) from disk, it will attempt to parse the content into full-featured python-stix2 objects and returned as such.

A note on add(): When STIX content is added (pushed) to the file system, the STIX content can be supplied in the following forms: Python STIX objects, Python dictionaries (of valid STIX objects or Bundles), JSON-encoded strings (of valid STIX objects or Bundles), or a (Python) list of any of the previously listed types. Any of the previous STIX content forms will be converted to a STIX JSON object (in a STIX Bundle) and written to disk.

FileSystem Examples

FileSystemStore

Use the FileSystemStore when you want to both retrieve STIX content from the file system and push STIX content to it, too.

In [4]:
from stix2 import FileSystemStore

# create FileSystemStore
fs = FileSystemStore("/tmp/stix2_store")

# retrieve STIX2 content from FileSystemStore
ap = fs.get("attack-pattern--00d0b012-8a03-410e-95de-5826bf542de6")
mal = fs.get("malware--00c3bfcb-99bd-4767-8c03-b08f585f5c8a")

# for visual purposes
print(mal)
Out[4]:
{
    "type": "malware",
    "id": "malware--00c3bfcb-99bd-4767-8c03-b08f585f5c8a",
    "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
    "created": "2017-05-31T21:33:19.746Z",
    "modified": "2017-05-31T21:33:19.746Z",
    "name": "PowerDuke",
    "description": "PowerDuke is a backdoor that was used by APT29 in 2016. It has primarily been delivered through Microsoft Word or Excel attachments containing malicious macros.[[Citation: Volexity PowerDuke November 2016]]",
    "labels": [
        "malware"
    ],
    "external_references": [
        {
            "source_name": "mitre-attack",
            "url": "https://attack.mitre.org/wiki/Software/S0139",
            "external_id": "S0139"
        },
        {
            "source_name": "Volexity PowerDuke November 2016",
            "description": "Adair, S.. (2016, November 9). PowerDuke: Widespread Post-Election Spear Phishing Campaigns Targeting Think Tanks and NGOs. Retrieved January 11, 2017.",
            "url": "https://www.volexity.com/blog/2016/11/09/powerduke-post-election-spear-phishing-campaigns-targeting-think-tanks-and-ngos/"
        }
    ],
    "object_marking_refs": [
        "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
    ]
}
In [2]:
from stix2 import ThreatActor, Indicator

# create new STIX threat-actor
ta = ThreatActor(name="Adjective Bear",
                labels=["nation-state"],
                sophistication="innovator",
                resource_level="government",
                goals=[
                    "compromising media outlets",
                    "water-hole attacks geared towards political, military targets",
                    "intelligence collection"
                ])

# create new indicators
ind = Indicator(description="Crusades C2 implant",
                labels=["malicious-activity"],
                pattern="[file:hashes.'SHA-256' = '54b7e05e39a59428743635242e4a867c932140a999f52a1e54fa7ee6a440c73b']")

ind1 = Indicator(description="Crusades C2 implant 2",
                 labels=["malicious-activity"],
                 pattern="[file:hashes.'SHA-256' = '64c7e05e40a59511743635242e4a867c932140a999f52a1e54fa7ee6a440c73b']")

# add STIX object (threat-actor) to FileSystemStore
fs.add(ta)

# can also add multiple STIX objects to FileSystemStore in one call
fs.add([ind, ind1])
FileSystemSource

Use the FileSystemSource when you only want to retrieve STIX content from the file system.

In [6]:
from stix2 import FileSystemSource

# create FileSystemSource
fs_source = FileSystemSource("/tmp/stix2_source")

# retrieve STIX 2 objects
ap = fs_source.get("attack-pattern--00d0b012-8a03-410e-95de-5826bf542de6")

# for visual purposes
print(ap)
Out[6]:
{
    "type": "attack-pattern",
    "id": "attack-pattern--00d0b012-8a03-410e-95de-5826bf542de6",
    "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
    "created": "2017-05-31T21:30:54.176Z",
    "modified": "2017-05-31T21:30:54.176Z",
    "name": "Indicator Removal from Tools",
    "description": "If a malicious...command-line parameters, Process monitoring",
    "kill_chain_phases": [
        {
            "kill_chain_name": "mitre-attack",
            "phase_name": "defense-evasion"
        }
    ],
    "external_references": [
        {
            "source_name": "mitre-attack",
            "url": "https://attack.mitre.org/wiki/Technique/T1066",
            "external_id": "T1066"
        }
    ],
    "object_marking_refs": [
        "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
    ]
}
In [7]:
from stix2 import Filter

# create filter for type=malware
query = [Filter("type", "=", "malware")]

# query on the filter
mals = fs_source.query(query)

for mal in mals:
    print(mal.id)
Out[7]:
malware--96b08451-b27a-4ff6-893f-790e26393a8e
Out[7]:
malware--b42378e0-f147-496f-992a-26a49705395b
Out[7]:
malware--6b616fc1-1505-48e3-8b2c-0d19337bff38
Out[7]:
malware--92ec0cbd-2c30-44a2-b270-73f4ec949841
In [8]:
# add more filters to the query
query.append(Filter("modified", ">" , "2017-05-31T21:33:10.772474Z"))

mals = fs_source.query(query)

# for visual purposes
for mal in mals:
    print(mal.id)
Out[8]:
malware--92ec0cbd-2c30-44a2-b270-73f4ec949841
FileSystemSink

Use the FileSystemSink when you only want to push STIX content to the file system.

In [10]:
from stix2 import FileSystemSink, Campaign, Indicator

# create FileSystemSink
fs_sink = FileSystemSink("/tmp/stix2_sink")

# create STIX objects and add to sink
camp = Campaign(name="The Crusades",
               objective="Infiltrating Israeli, Iranian and Palestinian digital infrastructure and government systems.",
               aliases=["Desert Moon"])

ind = Indicator(description="Crusades C2 implant",
                labels=["malicious-activity"],
                pattern="[file:hashes.'SHA-256' = '54b7e05e39a59428743635242e4a867c932140a999f52a1e54fa7ee6a440c73b']")

ind1 = Indicator(description="Crusades C2 implant",
                 labels=["malicious-activity"],
                 pattern="[file:hashes.'SHA-256' = '54b7e05e39a59428743635242e4a867c932140a999f52a1e54fa7ee6a440c73b']")

# add Campaign object to FileSystemSink
fs_sink.add(camp)

# can also add STIX objects to FileSystemSink in on call
fs_sink.add([ind, ind1])

Data Markings

Creating Objects With Data Markings

To create an object with a (predefined) TLP marking to an object, just provide it as a keyword argument to the constructor. The TLP markings can easily be imported from python-stix2.

In [7]:
from stix2 import Indicator, TLP_AMBER

indicator = Indicator(labels=["malicious-activity"],
                      pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
                      object_marking_refs=TLP_AMBER)
print(indicator)
Out[7]:
{
    "type": "indicator",
    "id": "indicator--95a71cff-fad0-4ffb-a641-8a6eaa642290",
    "created": "2018-04-05T19:49:47.924Z",
    "modified": "2018-04-05T19:49:47.924Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:49:47.924708Z",
    "labels": [
        "malicious-activity"
    ],
    "object_marking_refs": [
        "marking-definition--f88d31f6-486f-44da-b317-01333bde0b82"
    ]
}

If you’re creating your own marking (for example, a Statement marking), first create the statement marking:

In [8]:
from stix2 import MarkingDefinition, StatementMarking

marking_definition = MarkingDefinition(
    definition_type="statement",
    definition=StatementMarking(statement="Copyright 2017, Example Corp")
)
print(marking_definition)
Out[8]:
{
    "type": "marking-definition",
    "id": "marking-definition--13680b12-3d19-4b42-abe6-0d31effe5368",
    "created": "2018-04-05T19:49:53.98008Z",
    "definition_type": "statement",
    "definition": {
        "statement": "Copyright 2017, Example Corp"
    }
}

Then you can add it to an object as it’s being created (passing either full object or the the ID as a keyword argument, like with relationships).

In [9]:
indicator2 = Indicator(labels=["malicious-activity"],
                      pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
                      object_marking_refs=marking_definition)
print(indicator2)
Out[9]:
{
    "type": "indicator",
    "id": "indicator--7caeab49-2472-41bb-a988-2f990aea99bd",
    "created": "2018-04-05T19:49:55.763Z",
    "modified": "2018-04-05T19:49:55.763Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:49:55.763364Z",
    "labels": [
        "malicious-activity"
    ],
    "object_marking_refs": [
        "marking-definition--13680b12-3d19-4b42-abe6-0d31effe5368"
    ]
}
In [10]:
indicator3 = Indicator(labels=["malicious-activity"],
                      pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
                      object_marking_refs="marking-definition--f88d31f6-486f-44da-b317-01333bde0b82")
print(indicator3)
Out[10]:
{
    "type": "indicator",
    "id": "indicator--4eb21bbe-b8a9-4348-86cf-1ed52f9abdd7",
    "created": "2018-04-05T19:49:57.248Z",
    "modified": "2018-04-05T19:49:57.248Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:49:57.248658Z",
    "labels": [
        "malicious-activity"
    ],
    "object_marking_refs": [
        "marking-definition--f88d31f6-486f-44da-b317-01333bde0b82"
    ]
}

Granular markings work in the same way, except you also need to provide a full granular-marking object (including the selector).

In [11]:
from stix2 import Malware, TLP_WHITE

malware = Malware(name="Poison Ivy",
                  labels=['remote-access-trojan'],
                  description="A ransomware related to ...",
                  granular_markings=[
                      {
                          "selectors": ["description"],
                          "marking_ref": marking_definition
                      },
                      {
                          "selectors": ["name"],
                          "marking_ref": TLP_WHITE
                      }
                  ])
print(malware)
Out[11]:
{
    "type": "malware",
    "id": "malware--ef1eddbb-b5a5-47e0-b607-75b9870d8d91",
    "created": "2018-04-05T19:49:59.103Z",
    "modified": "2018-04-05T19:49:59.103Z",
    "name": "Poison Ivy",
    "description": "A ransomware related to ...",
    "labels": [
        "remote-access-trojan"
    ],
    "granular_markings": [
        {
            "marking_ref": "marking-definition--13680b12-3d19-4b42-abe6-0d31effe5368",
            "selectors": [
                "description"
            ]
        },
        {
            "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
            "selectors": [
                "name"
            ]
        }
    ]
}

Make sure that the selector is a field that exists and is populated on the object, otherwise this will cause an error:

In [12]:
Malware(name="Poison Ivy",
        labels=['remote-access-trojan'],
        description="A ransomware related to ...",
        granular_markings=[
            {
                "selectors": ["title"],
                "marking_ref": marking_definition
            }
        ])
InvalidSelectorError: Selector title in Malware is not valid!

Adding Data Markings To Existing Objects

Several functions exist to support working with data markings.

Both object markings and granular markings can be added to STIX objects which have already been created.

Note: Doing so will create a new version of the object (note the updated modified time).

In [13]:
indicator4 = indicator.add_markings(marking_definition)
print(indicator4)
Out[13]:
{
    "type": "indicator",
    "id": "indicator--95a71cff-fad0-4ffb-a641-8a6eaa642290",
    "created": "2018-04-05T19:49:47.924Z",
    "modified": "2018-04-05T19:50:03.387Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:49:47.924708Z",
    "labels": [
        "malicious-activity"
    ],
    "object_marking_refs": [
        "marking-definition--13680b12-3d19-4b42-abe6-0d31effe5368",
        "marking-definition--f88d31f6-486f-44da-b317-01333bde0b82"
    ]
}

You can also remove specific markings from STIX objects. This will also create a new version of the object.

In [14]:
indicator5 = indicator4.remove_markings(marking_definition)
print(indicator5)
Out[14]:
{
    "type": "indicator",
    "id": "indicator--95a71cff-fad0-4ffb-a641-8a6eaa642290",
    "created": "2018-04-05T19:49:47.924Z",
    "modified": "2018-04-05T19:50:05.109Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:49:47.924708Z",
    "labels": [
        "malicious-activity"
    ],
    "object_marking_refs": [
        "marking-definition--f88d31f6-486f-44da-b317-01333bde0b82"
    ]
}

The markings on an object can be replaced with a different set of markings:

In [15]:
from stix2 import TLP_GREEN

indicator6 = indicator5.set_markings([TLP_GREEN, marking_definition])
print(indicator6)
Out[15]:
{
    "type": "indicator",
    "id": "indicator--95a71cff-fad0-4ffb-a641-8a6eaa642290",
    "created": "2018-04-05T19:49:47.924Z",
    "modified": "2018-04-05T19:50:06.773Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:49:47.924708Z",
    "labels": [
        "malicious-activity"
    ],
    "object_marking_refs": [
        "marking-definition--13680b12-3d19-4b42-abe6-0d31effe5368",
        "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da"
    ]
}

STIX objects can also be cleared of all markings with clear_markings():

In [16]:
indicator7 = indicator5.clear_markings()
print(indicator7)
Out[16]:
{
    "type": "indicator",
    "id": "indicator--95a71cff-fad0-4ffb-a641-8a6eaa642290",
    "created": "2018-04-05T19:49:47.924Z",
    "modified": "2018-04-05T19:50:08.616Z",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T19:49:47.924708Z",
    "labels": [
        "malicious-activity"
    ]
}

All of these functions can be used for granular markings by passing in a list of selectors. Note that they will create new versions of the objects.

Evaluating Data Markings

You can get a list of the object markings on a STIX object:

In [17]:
indicator6.get_markings()
Out[17]:
['marking-definition--13680b12-3d19-4b42-abe6-0d31effe5368',
 'marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da']

To get a list of the granular markings on an object, pass the object and a list of selectors to get_markings():

In [18]:
from stix2 import get_markings

get_markings(malware, 'name')
Out[18]:
['marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9']

You can also call get_markings() as a method on the STIX object.

In [19]:
malware.get_markings('name')
Out[19]:
['marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9']

Finally, you may also check if an object is marked by a specific markings. Again, for granular markings, pass in the selector or list of selectors.

In [20]:
indicator.is_marked(TLP_AMBER.id)
Out[20]:
True
In [21]:
malware.is_marked(TLP_WHITE.id, 'name')
Out[21]:
True
In [22]:
malware.is_marked(TLP_WHITE.id, 'description')
Out[22]:
False

Memory

The Memory suite consists of MemoryStore, MemorySource, and MemorySink. Under the hood, the Memory suite points to an in-memory dictionary. Similarly, the MemoryStore is a just a wrapper around a paired MemorySource and MemorySink; as there is quite limited uses for just a MemorySource or a MemorySink, it is recommended to always use MemoryStore. The MemoryStore is intended for retrieving/searching and pushing STIX content to memory. It is important to note that all STIX content in memory is not backed up on the file system (disk), as that functionality is encompassed within the FileSystemStore. However, the Memory suite does provide some utility methods for saving and loading STIX content to disk. MemoryStore.save_to_file() allows for saving all the STIX content that is in memory to a json file. MemoryStore.load_from_file() allows for loading STIX content from a JSON-formatted file.

Memory API

A note on adding and retreiving STIX content to the Memory suite: As mentioned, under the hood the Memory suite is an internal, in-memory dictionary. STIX content that is to be added can be in the following forms: python-stix2 objects, (Python) dictionaries (of valid STIX objects or Bundles), JSON-encoded strings (of valid STIX objects or Bundles), or a (Python) list of any of the previously listed types. MemoryStore actually stores STIX content either as python-stix2 objects or as (Python) dictionaries, reducing and converting any of the aforementioned types to one of those. Additionally, whatever form the STIX object is stored as, is how it will be returned when retrieved. python-stix2 objects, and json-encoded strings (of STIX content) are stored as python-stix2 objects, while (Python) dictionaries (of STIX objects) are stored as (Python) dictionaries.

A note on load_from_file(): For load_from_file(), STIX content is assumed to be in JSON form within the file, as an individual STIX object or in a Bundle. When the JSON is loaded, the STIX objects are parsed into python-stix2 objects before being stored in the in-memory dictionary.

A note on save_to_file(): This method dumps all STIX content that is in the MemoryStore to the specified file. The file format will be JSON, and the STIX content will be within a STIX Bundle. Note also that the output form will be a JSON STIX Bundle regardless of the form that the individual STIX objects are stored in (i.e. supplied to) the MemoryStore.

Memory Examples

MemoryStore
In [3]:
from stix2 import MemoryStore, Indicator

# create default MemoryStore
mem = MemoryStore()

# insert newly created indicator into memory
ind = Indicator(description="Crusades C2 implant",
                labels=["malicious-activity"],
                pattern="[file:hashes.'SHA-256' = '54b7e05e39a59428743635242e4a867c932140a999f52a1e54fa7ee6a440c73b']")

mem.add(ind)

# for visual purposes
print(mem.get(ind.id))

Out[3]:
{
    "type": "indicator",
    "id": "indicator--41a960c7-a6d4-406d-9156-0069cb3bd40d",
    "created": "2018-04-05T19:50:41.222Z",
    "modified": "2018-04-05T19:50:41.222Z",
    "description": "Crusades C2 implant",
    "pattern": "[file:hashes.'SHA-256' = '54b7e05e39a59428743635242e4a867c932140a999f52a1e54fa7ee6a440c73b']",
    "valid_from": "2018-04-05T19:50:41.222522Z",
    "labels": [
        "malicious-activity"
    ]
}
In [4]:
from stix2 import Malware

# add multiple STIX objects into memory
ind2 = Indicator(description="Crusades stage 2 implant",
                 labels=["malicious-activity"],
                 pattern="[file:hashes.'SHA-256' = '70fa62fb218dd9d936ee570dbe531dfa4e7c128ff37e6af7a6a6b2485487e50a']")
ind3 = Indicator(description="Crusades stage 2 implant variant",
                 labels=["malicious-activity"],
                 pattern="[file:hashes.'SHA-256' = '31a45e777e4d58b97f4c43e38006f8cd6580ddabc4037905b2fad734712b582c']")
mal = Malware(labels=["rootkit"], name= "Alexios")

mem.add([ind2,ind3, mal])

# for visual purposes
print(mem.get(ind3.id))
Out[4]:
{
    "type": "indicator",
    "id": "indicator--ba2a7acb-a3ac-420b-9288-09988aa99408",
    "created": "2018-04-05T19:50:43.343Z",
    "modified": "2018-04-05T19:50:43.343Z",
    "description": "Crusades stage 2 implant variant",
    "pattern": "[file:hashes.'SHA-256' = '31a45e777e4d58b97f4c43e38006f8cd6580ddabc4037905b2fad734712b582c']",
    "valid_from": "2018-04-05T19:50:43.343298Z",
    "labels": [
        "malicious-activity"
    ]
}
In [5]:
from stix2 import Filter

mal = mem.query([Filter("labels","=", "rootkit")])[0]
print(mal)
Out[5]:
{
    "type": "malware",
    "id": "malware--9e9b87ce-2b2b-455a-8d5b-26384ccc8d52",
    "created": "2018-04-05T19:50:43.346Z",
    "modified": "2018-04-05T19:50:43.346Z",
    "name": "Alexios",
    "labels": [
        "rootkit"
    ]
}

load_from_file() and save_to_file()

In [8]:
mem_2 = MemoryStore()

# save (dump) all STIX content in MemoryStore to json file
mem.save_to_file("path_to_target_file.json")

# load(add) STIX content from json file into MemoryStore
mem_2.load_from_file("path_to_target_file.json")

report = mem_2.get("malware--9e9b87ce-2b2b-455a-8d5b-26384ccc8d52")

# for visual purposes
print(report)
Out[8]:
{
    "type": "malware",
    "id": "malware--9e9b87ce-2b2b-455a-8d5b-26384ccc8d52",
    "created": "2018-04-05T19:50:43.346Z",
    "modified": "2018-04-05T19:50:43.346Z",
    "name": "Alexios",
    "labels": [
        "rootkit"
    ]
}

Parsing STIX Content

Parsing STIX content is as easy as calling the parse() function on a JSON string, dictionary, or file-like object. It will automatically determine the type of the object. The STIX objects within bundle objects, and the cyber observables contained within observed-data objects will be parsed as well.

Parsing a string

In [3]:
from stix2 import parse

input_string = """{
    "type": "observed-data",
    "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
    "created": "2016-04-06T19:58:16.000Z",
    "modified": "2016-04-06T19:58:16.000Z",
    "first_observed": "2015-12-21T19:00:00Z",
    "last_observed": "2015-12-21T19:00:00Z",
    "number_observed": 50,
    "objects": {
        "0": {
            "type": "file",
            "hashes": {
                "SHA-256": "0969de02ecf8a5f003e3f6d063d848c8a193aada092623f8ce408c15bcb5f038"
            }
        }
    }
}"""

obj = parse(input_string)
print(type(obj))
print(obj)
Out[3]:
<class 'stix2.v20.sdo.ObservedData'>
Out[3]:
{
    "type": "observed-data",
    "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
    "created": "2016-04-06T19:58:16.000Z",
    "modified": "2016-04-06T19:58:16.000Z",
    "first_observed": "2015-12-21T19:00:00Z",
    "last_observed": "2015-12-21T19:00:00Z",
    "number_observed": 50,
    "objects": {
        "0": {
            "type": "file",
            "hashes": {
                "SHA-256": "0969de02ecf8a5f003e3f6d063d848c8a193aada092623f8ce408c15bcb5f038"
            }
        }
    }
}

Parsing a dictionary

In [4]:
input_dict = {
    "type": "identity",
    "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
    "created": "2015-12-21T19:59:11Z",
    "modified": "2015-12-21T19:59:11Z",
    "name": "Cole Powers",
    "identity_class": "individual"
}

obj = parse(input_dict)
print(type(obj))
print(obj)
Out[4]:
<class 'stix2.v20.sdo.Identity'>
Out[4]:
{
    "type": "identity",
    "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
    "created": "2015-12-21T19:59:11.000Z",
    "modified": "2015-12-21T19:59:11.000Z",
    "name": "Cole Powers",
    "identity_class": "individual"
}

Parsing a file-like object

In [5]:
file_handle = open("/tmp/stix2_store/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd.json")

obj = parse(file_handle)
print(type(obj))
print(obj)
Out[5]:
<class 'stix2.v20.sdo.CourseOfAction'>
Out[5]:
{
    "type": "course-of-action",
    "id": "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd",
    "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5",
    "created": "2017-05-31T21:30:41.022Z",
    "modified": "2017-05-31T21:30:41.022Z",
    "name": "Data from Network Shared Drive Mitigation",
    "description": "Identify unnecessary system utilities or potentially malicious software that may be used to collect data from a network share, and audit and/or block them by using whitelisting[[CiteRef::Beechey 2010]] tools, like AppLocker,[[CiteRef::Windows Commands JPCERT]][[CiteRef::NSA MS AppLocker]] or Software Restriction Policies[[CiteRef::Corio 2008]] where appropriate.[[CiteRef::TechNet Applocker vs SRP]]"
}

Parsing Custom STIX Content

Parsing custom STIX objects and/or STIX objects with custom properties is also completed easily with parse(). Just supply the keyword argument allow_custom=True. When allow_custom is specified, parse() will attempt to convert the supplied STIX content to known STIX 2 domain objects and/or previously defined custom STIX 2 objects. If the conversion cannot be completed (and allow_custom is specified), parse() will treat the supplied STIX 2 content as valid STIX 2 objects and return them. Warning: Specifying allow_custom may lead to critical errors if further processing (searching, filtering, modifying etc...) of the custom content occurs where the custom content supplied is not valid STIX 2. This is an axiomatic possibility as the stix2 library cannot guarantee proper processing of unknown custom STIX 2 objects that were explicitly flagged to be allowed, and thus may not be valid.

For examples of parsing STIX 2 objects with custom STIX properties, see Custom STIX Content: Custom Properties

For examples of parsing defined custom STIX 2 objects, see Custom STIX Content: Custom STIX Object Types

For retrieving STIX 2 content from a source (e.g. file system, TAXII) that may possibly have custom STIX 2 content unknown to the user, the user can create a STIX 2 DataStore/Source with the flag allow_custom=True. As mentioned, this will configure the DataStore/Source to allow for unknown STIX 2 content to be returned (albeit not converted to full STIX 2 domain objects and properties); the stix2 library may preclude processing the unknown content, if the content is not valid or actual STIX 2 domain objects and properties.

In [ ]:
from taxii2client import Collection
from stix2 import CompositeDataSource, FileSystemSource, TAXIICollectionSource

# to allow for the retrieval of unknown custom STIX2 content,
# just create *Stores/*Sources with the 'allow_custom' flag

# create FileSystemStore
fs = FileSystemSource("/path/to/stix2_data/", allow_custom=True)

# create TAXIICollectionSource
colxn = Collection('http://taxii_url')
ts = TAXIICollectionSource(colxn, allow_custom=True)

Serializing STIX Objects

The string representation of all STIX classes is a valid STIX JSON object.

In [3]:
from stix2 import Indicator

indicator = Indicator(name="File hash for malware variant",
                      labels=["malicious-activity"],
                      pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")

print(str(indicator))
Out[3]:
{
    "type": "indicator",
    "id": "indicator--4336ace8-d985-413a-8e32-f749ba268dc3",
    "created": "2018-04-05T20:01:20.012Z",
    "modified": "2018-04-05T20:01:20.012Z",
    "name": "File hash for malware variant",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T20:01:20.012209Z",
    "labels": [
        "malicious-activity"
    ]
}

However, the string representation can be slow, as it sorts properties to be in a more readable order. If you need performance and don’t care about the human-readability of the output, use the object’s serialize() function:

In [4]:
print(indicator.serialize())
Out[4]:
{"name": "File hash for malware variant", "labels": ["malicious-activity"], "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']", "type": "indicator", "id": "indicator--4336ace8-d985-413a-8e32-f749ba268dc3", "created": "2018-04-05T20:01:20.012Z", "modified": "2018-04-05T20:01:20.012Z", "valid_from": "2018-04-05T20:01:20.012209Z"}

If you need performance but also need human-readable output, you can pass the indent keyword argument to serialize():

In [5]:
print(indicator.serialize(indent=4))
Out[5]:
{
    "name": "File hash for malware variant",
    "labels": [
        "malicious-activity"
    ],
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "type": "indicator",
    "id": "indicator--4336ace8-d985-413a-8e32-f749ba268dc3",
    "created": "2018-04-05T20:01:20.012Z",
    "modified": "2018-04-05T20:01:20.012Z",
    "valid_from": "2018-04-05T20:01:20.012209Z"
}

The only difference between this and the string representation from using str() is that this will not sort the keys. This works because the keyword arguments are passed to json.dumps() internally.

TAXIICollection

The TAXIICollection suite contains TAXIICollectionStore, TAXIICollectionSource, and TAXIICollectionSink. TAXIICollectionStore pushes and retrieves STIX content to local/remote TAXII Collection(s). TAXIICollectionSource retrieves STIX content from local/remote TAXII Collection(s). TAXIICollectionSink pushes STIX content to local/remote TAXII Collection(s). Each of the interfaces is designed to be bound to a Collection from the taxii2client library (taxii2client.Collection), where all TAXIICollection API calls will be executed through that Collection instance.

A note on TAXII2 searching/filtering of STIX content: TAXII2 server implementations natively support searching on the STIX2 object properties: id, type and version; API requests made to TAXII2 can contain filter arguments for those 3 properties. However, the TAXIICollection suite supports searching on all STIX2 common object properties (see Filters documentation for full listing). This works simply by augmenting the filtering that is done remotely at the TAXII2 server instance. TAXIICollection will seperate any supplied queries into TAXII supported filters and non-supported filters. During a TAXIICollection API call, TAXII2 supported filters get inserted into the TAXII2 server request (to be evaluated at the server). The rest of the filters are kept locally and then applied to the STIX2 content that is returned from the TAXII2 server, before being returned from the TAXIICollection API call.

TAXIICollection API

TAXIICollection Examples

TAXIICollectionSource
In [18]:
from stix2 import TAXIICollectionSource
from taxii2client import Collection

# establish TAXII2 Collection instance
collection = Collection("http://127.0.0.1:5000/trustgroup1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/", user="admin", password="Password0")
# supply the TAXII2 collection to TAXIICollection
tc_source = TAXIICollectionSource(collection)

#retrieve STIX objects by id
stix_obj = tc_source.get("malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111")
stix_obj_versions = tc_source.all_versions("indicator--a932fcc6-e032-176c-126f-cb970a5a1ade")

#for visual purposes
print(stix_obj)
print("-------")
for so in stix_obj_versions:
    print(so)

{
    "type": "malware",
    "id": "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111",
    "created": "2017-01-27T13:49:53.997Z",
    "modified": "2017-01-27T13:49:53.997Z",
    "name": "Poison Ivy",
    "description": "Poison Ivy",
    "labels": [
        "remote-access-trojan"
    ]
}
-------
{
    "type": "indicator",
    "id": "indicator--a932fcc6-e032-176c-126f-cb970a5a1ade",
    "created": "2014-05-08T09:00:00.000Z",
    "modified": "2014-05-08T09:00:00.000Z",
    "name": "File hash for Poison Ivy variant",
    "pattern": "[file:hashes.'SHA-256' = 'ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c']",
    "valid_from": "2014-05-08T09:00:00Z",
    "labels": [
        "file-hash-watchlist"
    ]
}
In [20]:
from stix2 import Filter

# retrieve multiple object from TAXIICollectionSource
# by using filters
f1 = Filter("type","=", "indicator")

indicators = tc_source.query([f1])

#for visual purposes
for indicator in indicators:
    print(indicator)
{
    "type": "indicator",
    "id": "indicator--a932fcc6-e032-176c-126f-cb970a5a1ade",
    "created": "2014-05-08T09:00:00.000Z",
    "modified": "2014-05-08T09:00:00.000Z",
    "name": "File hash for Poison Ivy variant",
    "pattern": "[file:hashes.'SHA-256' = 'ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c']",
    "valid_from": "2014-05-08T09:00:00Z",
    "labels": [
        "file-hash-watchlist"
    ]
}
TAXIICollectionSink
In [ ]:
from stix2 import TAXIICollectionSink, ThreatActor

#create TAXIICollectionSINK and push STIX content to it
tc_sink = TAXIICollectionSink(collection)

# create new STIX threat-actor
ta = ThreatActor(name="Teddy Bear",
                labels=["nation-state"],
                sophistication="innovator",
                resource_level="government",
                goals=[
                    "compromising environment NGOs",
                    "water-hole attacks geared towards energy sector",
                ])

tc_sink.add(ta)



TAXIICollectionStore
In [19]:
from stix2 import TAXIICollectionStore

# create TAXIICollectionStore - note the same collection instance can
# be used for the store
tc_store = TAXIICollectionStore(collection)

# retrieve STIX object by id from TAXII Collection through
# TAXIICollectionStore
stix_obj2 = tc_source.get("malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111")

print(stix_obj2)
{
    "type": "malware",
    "id": "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111",
    "created": "2017-01-27T13:49:53.997Z",
    "modified": "2017-01-27T13:49:53.997Z",
    "name": "Poison Ivy",
    "description": "Poison Ivy",
    "labels": [
        "remote-access-trojan"
    ]
}
In [ ]:
from stix2 import indicator

# add STIX object to TAXIICollectionStore
ind = Indicator(description="Smokey Bear implant",
                labels=["malicious-activity"],
                pattern="[file:hashes.'SHA-256' = '09c7e05a39a59428743635242e4a867c932140a909f12a1e54fa7ee6a440c73b']")

tc_store.add(ind)

Bug and Workaround

You may get an error similar to the following when adding STIX objects to a TAXIICollectionStore or TAXIICollectionSink:

TypeError: Object of type ThreatActor is not JSON serializable

This is a known bug and we are working to fix it. For more information, see this GitHub issue In the meantime, try this workaround:

In [ ]:
tc_sink.add(json.loads(Bundle(ta).serialize()))

Or bypass the TAXIICollection altogether and interact with the collection itself:

In [ ]:
collection.add_objects(json.loads(Bundle(ta).serialize()))

Technical Specification Support

How imports work

Imports can be used in different ways depending on the use case and support levels.

People who want to support the latest version of STIX 2.X without having to make changes, can implicitly use the latest version:

In [ ]:
import stix2

stix2.Indicator()

or,

In [ ]:
from stix2 import Indicator

Indicator()

People who want to use an explicit version:

In [ ]:
import stix2.v20

stix2.v20.Indicator()

or,

In [ ]:
from stix2.v20 import Indicator

Indicator()

or even,

In [ ]:
import stix2.v20 as stix2

stix2.Indicator()

The last option makes it easy to update to a new version in one place per file, once you’ve made the deliberate action to do this.

People who want to use multiple versions in a single file:

In [ ]:
import stix2

stix2.v20.Indicator()
stix2.v21.Indicator()

or,

In [ ]:
from stix2 import v20, v21

v20.Indicator()
v21.Indicator()

or (less preferred):

In [ ]:
from stix2.v20 import Indicator as Indicator_v20
from stix2.v21 import Indicator as Indicator_v21

Indicator_v20()
Indicator_v21()

How parsing works

If the version positional argument is not provided. The data will be parsed using the latest version of STIX 2.X supported by the stix2 library.

You can lock your parse() method to a specific STIX version by:

In [2]:
from stix2 import parse

indicator = parse("""{
    "type": "indicator",
    "id": "indicator--dbcbd659-c927-4f9a-994f-0a2632274394",
    "created": "2017-09-26T23:33:39.829Z",
    "modified": "2017-09-26T23:33:39.829Z",
    "labels": [
        "malicious-activity"
    ],
    "name": "File hash for malware variant",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2017-09-26T23:33:39.829952Z"
}""", version="2.0")
print(indicator)
Out[2]:
{
    "type": "indicator",
    "id": "indicator--dbcbd659-c927-4f9a-994f-0a2632274394",
    "created": "2017-09-26T23:33:39.829Z",
    "modified": "2017-09-26T23:33:39.829Z",
    "name": "File hash for malware variant",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2017-09-26T23:33:39.829952Z",
    "labels": [
        "malicious-activity"
    ]
}

Keep in mind that if a 2.1 or higher object is parsed, the operation will fail.

How custom content works

CustomObject, CustomObservable, CustomMarking and CustomExtension must be registered explicitly by STIX version. This is a design decision since properties or requirements may change as the STIX Technical Specification advances.

You can perform this by:

In [ ]:
import stix2

# Make my custom observable available in STIX 2.0
@stix2.v20.CustomObservable('x-new-object-type',
                            (("prop", stix2.properties.BooleanProperty())))
class NewObject2(object):
    pass


# Make my custom observable available in STIX 2.1
@stix2.v21.CustomObservable('x-new-object-type',
                            (("prop", stix2.properties.BooleanProperty())))
class NewObject2(object):
    pass

Versioning

To create a new version of an existing object, specify the property(ies) you want to change and their new values:

In [4]:
from stix2 import Indicator

indicator = Indicator(created="2016-01-01T08:00:00.000Z",
                      name="File hash for suspicious file",
                      labels=["anomalous-activity"],
                      pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']")

indicator2 = indicator.new_version(name="File hash for Foobar malware",
                                   labels=["malicious-activity"])
print(indicator2)
Out[4]:
{
    "type": "indicator",
    "id": "indicator--dd052ff6-e404-444b-beb9-eae96d1e79ea",
    "created": "2016-01-01T08:00:00.000Z",
    "modified": "2018-04-05T20:02:51.161Z",
    "name": "File hash for Foobar malware",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T20:02:51.138312Z",
    "labels": [
        "malicious-activity"
    ]
}

The modified time will be updated to the current time unless you provide a specific value as a keyword argument. Note that you can’t change the type, id, or created properties.

In [5]:
indicator.new_version(id="indicator--cc42e358-8b9b-493c-9646-6ecd73b41c21")
UnmodifiablePropertyError: These properties cannot be changed when making a new version: id.

To revoke an object:

In [6]:
indicator2 = indicator2.revoke()
print(indicator2)
Out[6]:
{
    "type": "indicator",
    "id": "indicator--dd052ff6-e404-444b-beb9-eae96d1e79ea",
    "created": "2016-01-01T08:00:00.000Z",
    "modified": "2018-04-05T20:02:54.704Z",
    "name": "File hash for Foobar malware",
    "pattern": "[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']",
    "valid_from": "2018-04-05T20:02:51.138312Z",
    "revoked": true,
    "labels": [
        "malicious-activity"
    ]
}

Using The Workbench

The Workbench API hides most of the complexity of the rest of the library to make it easy to interact with STIX data. To use it, just import everything from stix2.workbench:

In [3]:
from stix2.workbench import *

Retrieving STIX Data

To get some STIX data to work with, let’s set up a DataSource and add it to our workbench.

In [4]:
from taxii2client import Collection

collection = Collection("http://127.0.0.1:5000/trustgroup1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/", user="admin", password="Password0")
tc_source = TAXIICollectionSource(collection)
add_data_source(tc_source)

Now we can get all of the indicators from the data source.

In [5]:
response = indicators()

Similar functions are available for the other STIX Object types. See the full list here.

If you want to only retrieve some indicators, you can pass in one or more Filters. This example finds all the indicators created by a specific identity:

In [6]:
response = indicators(filters=Filter('created_by_ref', '=', 'identity--adede3e8-bf44-4e6f-b3c9-1958cbc3b188'))

The objects returned let you easily traverse their relationships. Get all Relationship objects involving that object with .relationships(), all other objects related to this object with .related(), and the Identity object for the creator of the object (if one exists) with .created_by(). For full details on these methods and their arguments, see the Workbench API documentation.

In [7]:
for i in indicators():
    for rel in i.relationships():
        print(rel.source_ref)
        print(rel.relationship_type)
        print(rel.target_ref)
Out[7]:
indicator--a932fcc6-e032-176c-126f-cb970a5a1ade
Out[7]:
indicates
Out[7]:
malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111
In [8]:
for i in indicators():
    for obj in i.related():
        print(obj)
Out[8]:
{
    "type": "malware",
    "id": "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111",
    "created": "2017-01-27T13:49:53.997Z",
    "modified": "2017-01-27T13:49:53.997Z",
    "name": "Poison Ivy",
    "description": "Poison Ivy",
    "labels": [
        "remote-access-trojan"
    ]
}

If there are a lot of related objects, you can narrow it down by passing in one or more Filters just as before. For example, if we want to get only the indicators related to a specific piece of malware (and not any entities that use it or are targeted by it):

In [9]:
malware = get('malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111')
indicator = malware.related(filters=Filter('type', '=', 'indicator'))
print(indicator[0])
Out[9]:
{
    "type": "indicator",
    "id": "indicator--a932fcc6-e032-176c-126f-cb970a5a1ade",
    "created": "2014-05-08T09:00:00.000Z",
    "modified": "2014-05-08T09:00:00.000Z",
    "name": "File hash for Poison Ivy variant",
    "pattern": "[file:hashes.'SHA-256' = 'ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c']",
    "valid_from": "2014-05-08T09:00:00Z",
    "labels": [
        "file-hash-watchlist"
    ]
}

Creating STIX Data

To create a STIX object, just use that object’s class constructor. Once it’s created, add it to the workbench with save().

In [10]:
identity = Identity(name="ACME Threat Intel Co.", identity_class="organization")
save(identity)

You can also set defaults for certain properties when creating objects. For example, let’s set the default creator to be the identity object we just created:

In [11]:
set_default_creator(identity)

Now when we create an indicator (or any other STIX Domain Object), it will automatically have the right create_by_ref value.

In [12]:
indicator = Indicator(labels=["malicious-activity"], pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']")
save(indicator)

indicator_creator = get(indicator.created_by_ref)
print(indicator_creator.name)
Out[12]:
ACME Threat Intel Co.

Defaults can also be set for the created timestamp, external references and object marking references.

Warning:

The workbench layer replaces STIX Object classes with special versions of them that use “wrappers” to provide extra functionality. Because of this, we recommend that you either use the workbench layer or the rest of the library, but not both. In other words, don’t import from both stix2.workbench and any other submodules of stix2.

API Reference

This section of documentation contains information on all of the classes and functions in the stix2 API, as given by the package’s docstrings.

Note

All the classes and functions detailed in the pages below are importable directly from stix2. See also: How imports work.

Python APIs for STIX 2.

core STIX 2.0 Objects that are neither SDOs nor SROs.
datastore Python STIX 2.0 DataStore API.
environment Python STIX 2.0 Environment API.
exceptions STIX 2 error classes.
markings Functions for working with STIX 2 Data Markings.
patterns Classes to aid in working with the STIX 2 patterning language.
properties Classes for representing properties of STIX Objects and Cyber Observables.
utils Utility functions and classes for the stix2 library.
workbench Functions and class wrappers for interacting with STIX data at a high level.
v20.common STIX 2 Common Data Types and Properties.
v20.observables STIX 2.0 Cyber Observable Objects.
v20.sdo STIX 2.0 Domain Objects.
v20.sro STIX 2.0 Relationship Objects.

Contributing

We’re thrilled that you’re interested in contributing to python-stix2! Here are some things you should know:

  • contribution-guide.org has great ideas for contributing to any open-source project (not just python-stix2).
  • All contributors must sign a Contributor License Agreement. See CONTRIBUTING.md in the project repository for specifics.
  • If you are planning to implement a major feature (vs. fixing a bug), please discuss with a project maintainer first to ensure you aren’t duplicating the work of someone else, and that the feature is likely to be accepted.

Now, let’s get started!

Setting up a development environment

We recommend using a virtualenv.

1. Clone the repository. If you’re planning to make pull request, you should fork the repository on GitHub and clone your fork instead of the main repo:

git clone https://github.com/yourusername/cti-python-stix2.git
  1. Install develoment-related dependencies:
cd cti-python-stix2
pip install -r requirements.txt
  1. Install pre-commit git hooks:
pre-commit install

At this point you should be able to make changes to the code.

Code style

All code should follow PEP 8. We allow for line lengths up to 160 characters, but any lines over 80 characters should be the exception rather than the rule. PEP 8 conformance will be tested automatically by Tox and Travis-CI (see below).

Testing

Note

All of the tools mentioned in this section are installed when you run pip install -r requirements.txt.

python-stix2 uses pytest for testing. We encourage the use of test-driven development (TDD), where you write (failing) tests that demonstrate a bug or proposed new feature before writing code that fixes the bug or implements the features. Any code contributions to python-stix2 should come with new or updated tests.

To run the tests in your current Python environment, use the pytest command from the root project directory:

pytest

This should show all of the tests that ran, along with their status.

You can run a specific test file by passing it on the command line:

pytest stix2/test/test_<xxx>.py

To ensure that the test you wrote is running, you can deliberately add an assert False statement at the beginning of the test. This is another benefit of TDD, since you should be able to see the test failing (and ensure it’s being run) before making it pass.

tox allows you to test a package across multiple versions of Python. Setting up multiple Python environments is beyond the scope of this guide, but feel free to ask for help setting them up. Tox should be run from the root directory of the project:

tox

We aim for high test coverage, using the coverage.py library. Though it’s not an absolute requirement to maintain 100% coverage, all code contributions must be accompanied by tests. To run coverage and look for untested lines of code, run:

pytest --cov=stix2
coverage html

then look at the resulting report in htmlcov/index.html.

All commits pushed to the master branch or submitted as a pull request are tested with Travis-CI automatically.

Indices and tables