Merge existing 'master' branch
commit
fe61b00d78
|
@ -0,0 +1,59 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
152
README.md
152
README.md
|
@ -4,6 +4,158 @@
|
|||
|
||||
This repository provides Python APIs for serializing and de-serializing STIX 2 JSON content, along with higher-level APIs for common tasks, including data markings, versioning, and for resolving STIX IDs across multiple data sources.
|
||||
|
||||
## Installation
|
||||
|
||||
Install with [`pip`](https://pip.pypa.io/en/stable/):
|
||||
|
||||
```
|
||||
pip install stix2
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating STIX Domain Objects
|
||||
|
||||
To create a STIX object, provide keyword arguments to the type's constructor:
|
||||
|
||||
```python
|
||||
from stix2 import Indicator
|
||||
|
||||
indicator = Indicator(name="File hash for malware variant",
|
||||
labels=['malicious-activity'],
|
||||
pattern='file:hashes.md5 = "d41d8cd98f00b204e9800998ecf8427e"')
|
||||
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
indicator = Indicator(type='indicator', ...)
|
||||
```
|
||||
|
||||
Passing a value for `type` that does not match the class being constructed
|
||||
will cause an error:
|
||||
|
||||
```python
|
||||
>>> indicator = Indicator(type='xxx', ...)
|
||||
ValueError: Indicators must have type='indicator'
|
||||
```
|
||||
|
||||
- If not provided, `id` will be generated randomly. If you provide an `id`
|
||||
argument, it must begin with the correct prefix:
|
||||
|
||||
```python
|
||||
>>> indicator = Indicator(id="campaign--63ce9068-b5ab-47fa-a2cf-a602ea01f21a")
|
||||
ValueError: Indicator id values must begin with 'indicator--'
|
||||
```
|
||||
|
||||
- If not provided, `created` and `modified` will be set to the (same) current
|
||||
time.
|
||||
|
||||
For indicators, `labels` and `pattern` are required and cannot be set
|
||||
automatically. Trying to create an indicator that is missing one of these fields
|
||||
will result in an error:
|
||||
|
||||
```python
|
||||
>>> indicator = Indicator()
|
||||
ValueError: Missing required field(s) 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:
|
||||
|
||||
```python
|
||||
>>> indicator['name']
|
||||
'File hash for malware variant'
|
||||
```
|
||||
|
||||
TBD: Should we allow property access using the standard Python attribute syntax?
|
||||
|
||||
```python
|
||||
>>> indicator.name
|
||||
'File hash for malware variant'
|
||||
```
|
||||
|
||||
Attempting to modify any attributes will raise an error:
|
||||
|
||||
```python
|
||||
>>>indicator['name'] = "This is a revised name"
|
||||
ValueError: Cannot modify properties after creation.
|
||||
```
|
||||
|
||||
To update the properties of an object, see [Versioning](#versioning) below.
|
||||
|
||||
Creating a Malware object follows the same pattern:
|
||||
|
||||
```python
|
||||
from stix2 import Malware
|
||||
|
||||
malware = Malware(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.
|
||||
|
||||
### 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.
|
||||
|
||||
```python
|
||||
from stix2 import Relationship
|
||||
|
||||
relationship = Relationship(relationship_type='indicates',
|
||||
source_ref=indicator.id,
|
||||
target_ref=malware.id)
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
relationship = Relationship(indicator, 'indicates', malware)
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```python
|
||||
from stix2 import bundle
|
||||
|
||||
bundle = Bundle(indicator, malware, relationship)
|
||||
```
|
||||
|
||||
### Serializing STIX objects
|
||||
|
||||
The string representation of all STIX classes is a valid STIX JSON object.
|
||||
|
||||
```python
|
||||
indicator = Indicator(...)
|
||||
|
||||
print(str(indicator))
|
||||
```
|
||||
|
||||
### Versioning
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
## Governance
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
pytest
|
||||
tox
|
||||
pytest-cov
|
||||
|
||||
-e .
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
install_requires = [
|
||||
'pytz',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='stix2',
|
||||
description="Produce and consume STIX 2 JSON content",
|
||||
version='0.0.1',
|
||||
packages=find_packages(),
|
||||
install_requires=install_requires,
|
||||
keywords="stix stix2 json cti cyber threat intelligence",
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
"""Python APIs for STIX 2."""
|
||||
|
||||
from .bundle import Bundle
|
||||
from .common import ExternalReference
|
||||
from .sdo import Indicator, Malware
|
||||
from .sro import Relationship
|
|
@ -0,0 +1,109 @@
|
|||
"""Base class for type definitions in the stix2 library."""
|
||||
|
||||
import collections
|
||||
import datetime as dt
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from .utils import format_datetime, get_timestamp, NOW
|
||||
|
||||
__all__ = ['STIXJSONEncoder', '_STIXBase']
|
||||
|
||||
DEFAULT_ERROR = "{type} must have {field}='{expected}'."
|
||||
|
||||
|
||||
class STIXJSONEncoder(json.JSONEncoder):
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, (dt.date, dt.datetime)):
|
||||
return format_datetime(obj)
|
||||
elif isinstance(obj, _STIXBase):
|
||||
return dict(obj)
|
||||
else:
|
||||
return super(STIXJSONEncoder, self).default(obj)
|
||||
|
||||
|
||||
class _STIXBase(collections.Mapping):
|
||||
"""Base class for STIX object types"""
|
||||
|
||||
@classmethod
|
||||
def _make_id(cls):
|
||||
return cls._type + "--" + str(uuid.uuid4())
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
cls = self.__class__
|
||||
class_name = cls.__name__
|
||||
|
||||
# Use the same timestamp for any auto-generated datetimes
|
||||
now = get_timestamp()
|
||||
|
||||
# Detect any keyword arguments not allowed for a specific type
|
||||
extra_kwargs = list(set(kwargs) - set(cls._properties))
|
||||
if extra_kwargs:
|
||||
raise TypeError("unexpected keyword arguments: " + str(extra_kwargs))
|
||||
|
||||
required_fields = [k for k, v in cls._properties.items() if v.get('required')]
|
||||
missing_kwargs = set(required_fields) - set(kwargs)
|
||||
if missing_kwargs:
|
||||
msg = "Missing required field(s) for {type}: ({fields})."
|
||||
field_list = ", ".join(x for x in sorted(list(missing_kwargs)))
|
||||
raise ValueError(msg.format(type=class_name, fields=field_list))
|
||||
|
||||
for prop_name, prop_metadata in cls._properties.items():
|
||||
if prop_name not in kwargs:
|
||||
if prop_metadata.get('default'):
|
||||
default = prop_metadata['default']
|
||||
if default == NOW:
|
||||
kwargs[prop_name] = now
|
||||
else:
|
||||
kwargs[prop_name] = default(cls)
|
||||
elif prop_metadata.get('fixed'):
|
||||
kwargs[prop_name] = prop_metadata['fixed']
|
||||
|
||||
if prop_metadata.get('validate'):
|
||||
if not prop_metadata['validate'](cls, kwargs[prop_name]):
|
||||
msg = prop_metadata.get('error_msg', DEFAULT_ERROR).format(
|
||||
type=class_name,
|
||||
field=prop_name,
|
||||
expected=prop_metadata.get('expected',
|
||||
prop_metadata['default'])(cls),
|
||||
)
|
||||
raise ValueError(msg)
|
||||
elif prop_metadata.get('fixed'):
|
||||
if kwargs[prop_name] != prop_metadata['fixed']:
|
||||
msg = prop_metadata.get('error_msg', DEFAULT_ERROR).format(
|
||||
type=class_name,
|
||||
field=prop_name,
|
||||
expected=prop_metadata['fixed']
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
self._inner = kwargs
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._inner[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._inner)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._inner)
|
||||
|
||||
# Handle attribute access just like key access
|
||||
def __getattr__(self, name):
|
||||
return self.get(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name != '_inner':
|
||||
raise ValueError("Cannot modify properties after creation.")
|
||||
super(_STIXBase, self).__setattr__(name, value)
|
||||
|
||||
def __str__(self):
|
||||
# TODO: put keys in specific order. Probably need custom JSON encoder.
|
||||
return json.dumps(self, indent=4, sort_keys=True, cls=STIXJSONEncoder,
|
||||
separators=(",", ": ")) # Don't include spaces after commas.
|
||||
|
||||
def __repr__(self):
|
||||
props = [(k, self[k]) for k in sorted(self._properties) if self.get(k)]
|
||||
return "{0}({1})".format(self.__class__.__name__,
|
||||
", ".join(["{0!s}={1!r}".format(k, v) for k, v in props]))
|
|
@ -0,0 +1,24 @@
|
|||
"""STIX 2 Bundle object"""
|
||||
|
||||
from .base import _STIXBase
|
||||
from .common import TYPE_PROPERTY, ID_PROPERTY
|
||||
|
||||
|
||||
class Bundle(_STIXBase):
|
||||
|
||||
_type = 'bundle'
|
||||
_properties = {
|
||||
'type': TYPE_PROPERTY,
|
||||
'id': ID_PROPERTY,
|
||||
'spec_version': {
|
||||
'fixed': "2.0",
|
||||
},
|
||||
'objects': {},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Add any positional arguments to the 'objects' kwarg.
|
||||
if args:
|
||||
kwargs['objects'] = kwargs.get('objects', []) + list(args)
|
||||
|
||||
super(Bundle, self).__init__(**kwargs)
|
|
@ -0,0 +1,38 @@
|
|||
"""STIX 2 Common Data Types and Properties"""
|
||||
|
||||
from .base import _STIXBase
|
||||
from .utils import NOW
|
||||
|
||||
TYPE_PROPERTY = {
|
||||
'default': (lambda x: x._type),
|
||||
'validate': (lambda x, val: val == x._type)
|
||||
}
|
||||
|
||||
ID_PROPERTY = {
|
||||
'default': (lambda x: x._make_id()),
|
||||
'validate': (lambda x, val: val.startswith(x._type + "--")),
|
||||
'expected': (lambda x: x._type + "--"),
|
||||
'error_msg': "{type} {field} values must begin with '{expected}'."
|
||||
}
|
||||
|
||||
COMMON_PROPERTIES = {
|
||||
'type': TYPE_PROPERTY,
|
||||
'id': ID_PROPERTY,
|
||||
'created': {
|
||||
'default': NOW,
|
||||
},
|
||||
'modified': {
|
||||
'default': NOW,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ExternalReference(_STIXBase):
|
||||
_properties = {
|
||||
'source_name': {
|
||||
'required': True,
|
||||
},
|
||||
'description': {},
|
||||
'url': {},
|
||||
'external_id': {},
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
"""STIX 2.0 Domain Objects"""
|
||||
|
||||
from .base import _STIXBase
|
||||
from .common import COMMON_PROPERTIES
|
||||
from .utils import NOW
|
||||
|
||||
|
||||
class Indicator(_STIXBase):
|
||||
|
||||
_type = 'indicator'
|
||||
_properties = COMMON_PROPERTIES.copy()
|
||||
_properties.update({
|
||||
'labels': {
|
||||
'required': True,
|
||||
},
|
||||
'pattern': {
|
||||
'required': True,
|
||||
},
|
||||
'valid_from': {
|
||||
'default': NOW,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# TODO:
|
||||
# - created_by_ref
|
||||
# - revoked
|
||||
# - external_references
|
||||
# - object_marking_refs
|
||||
# - granular_markings
|
||||
|
||||
# - name
|
||||
# - description
|
||||
# - valid_until
|
||||
# - kill_chain_phases
|
||||
|
||||
super(Indicator, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class Malware(_STIXBase):
|
||||
|
||||
_type = 'malware'
|
||||
_properties = COMMON_PROPERTIES.copy()
|
||||
_properties.update({
|
||||
'labels': {
|
||||
'required': True,
|
||||
},
|
||||
'name': {
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# TODO:
|
||||
# - created_by_ref
|
||||
# - revoked
|
||||
# - external_references
|
||||
# - object_marking_refs
|
||||
# - granular_markings
|
||||
|
||||
# - description
|
||||
# - kill_chain_phases
|
||||
|
||||
super(Malware, self).__init__(**kwargs)
|
|
@ -0,0 +1,51 @@
|
|||
"""STIX 2.0 Relationship Objects."""
|
||||
|
||||
from .base import _STIXBase
|
||||
from .common import COMMON_PROPERTIES
|
||||
|
||||
|
||||
class Relationship(_STIXBase):
|
||||
|
||||
_type = 'relationship'
|
||||
_properties = COMMON_PROPERTIES.copy()
|
||||
_properties.update({
|
||||
'relationship_type': {
|
||||
'required': True,
|
||||
},
|
||||
'source_ref': {
|
||||
'required': True,
|
||||
},
|
||||
'target_ref': {
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Explicitly define the first three kwargs to make readable Relationship declarations.
|
||||
def __init__(self, source_ref=None, relationship_type=None, target_ref=None,
|
||||
**kwargs):
|
||||
# TODO:
|
||||
# - created_by_ref
|
||||
# - revoked
|
||||
# - external_references
|
||||
# - object_marking_refs
|
||||
# - granular_markings
|
||||
|
||||
# - description
|
||||
|
||||
# Allow (source_ref, relationship_type, target_ref) as positional args.
|
||||
if source_ref and not kwargs.get('source_ref'):
|
||||
kwargs['source_ref'] = source_ref
|
||||
if relationship_type and not kwargs.get('relationship_type'):
|
||||
kwargs['relationship_type'] = relationship_type
|
||||
if target_ref and not kwargs.get('target_ref'):
|
||||
kwargs['target_ref'] = target_ref
|
||||
|
||||
# If actual STIX objects (vs. just the IDs) are passed in, extract the
|
||||
# ID values to use in the Relationship object.
|
||||
if kwargs.get('source_ref') and isinstance(kwargs['source_ref'], _STIXBase):
|
||||
kwargs['source_ref'] = kwargs['source_ref'].id
|
||||
|
||||
if kwargs.get('target_ref') and isinstance(kwargs['target_ref'], _STIXBase):
|
||||
kwargs['target_ref'] = kwargs['target_ref'].id
|
||||
|
||||
super(Relationship, self).__init__(**kwargs)
|
|
@ -0,0 +1,112 @@
|
|||
"""Tests for stix.ExternalReference"""
|
||||
|
||||
import pytest
|
||||
|
||||
import stix2
|
||||
|
||||
VERIS = """{
|
||||
"external_id": "0001AA7F-C601-424A-B2B8-BE6C9F5164E7",
|
||||
"source_name": "veris",
|
||||
"url": "https://github.com/vz-risk/VCDB/blob/master/data/json/0001AA7F-C601-424A-B2B8-BE6C9F5164E7.json"
|
||||
}"""
|
||||
|
||||
|
||||
def test_external_reference_veris():
|
||||
ref = stix2.ExternalReference(
|
||||
source_name="veris",
|
||||
external_id="0001AA7F-C601-424A-B2B8-BE6C9F5164E7",
|
||||
url="https://github.com/vz-risk/VCDB/blob/master/data/json/0001AA7F-C601-424A-B2B8-BE6C9F5164E7.json",
|
||||
)
|
||||
|
||||
assert str(ref) == VERIS
|
||||
|
||||
|
||||
CAPEC = """{
|
||||
"external_id": "CAPEC-550",
|
||||
"source_name": "capec"
|
||||
}"""
|
||||
|
||||
|
||||
def test_external_reference_capec():
|
||||
ref = stix2.ExternalReference(
|
||||
source_name="capec",
|
||||
external_id="CAPEC-550",
|
||||
)
|
||||
|
||||
assert str(ref) == CAPEC
|
||||
assert repr(ref) == "ExternalReference(external_id='CAPEC-550', source_name='capec')"
|
||||
|
||||
|
||||
CAPEC_URL = """{
|
||||
"external_id": "CAPEC-550",
|
||||
"source_name": "capec",
|
||||
"url": "http://capec.mitre.org/data/definitions/550.html"
|
||||
}"""
|
||||
|
||||
|
||||
def test_external_reference_capec_url():
|
||||
ref = stix2.ExternalReference(
|
||||
source_name="capec",
|
||||
external_id="CAPEC-550",
|
||||
url="http://capec.mitre.org/data/definitions/550.html",
|
||||
)
|
||||
|
||||
assert str(ref) == CAPEC_URL
|
||||
|
||||
|
||||
THREAT_REPORT = """{
|
||||
"description": "Threat report",
|
||||
"source_name": "ACME Threat Intel",
|
||||
"url": "http://www.example.com/threat-report.pdf"
|
||||
}"""
|
||||
|
||||
|
||||
def test_external_reference_threat_report():
|
||||
ref = stix2.ExternalReference(
|
||||
source_name="ACME Threat Intel",
|
||||
description="Threat report",
|
||||
url="http://www.example.com/threat-report.pdf",
|
||||
)
|
||||
|
||||
assert str(ref) == THREAT_REPORT
|
||||
|
||||
|
||||
BUGZILLA = """{
|
||||
"external_id": "1370",
|
||||
"source_name": "ACME Bugzilla",
|
||||
"url": "https://www.example.com/bugs/1370"
|
||||
}"""
|
||||
|
||||
|
||||
def test_external_reference_bugzilla():
|
||||
ref = stix2.ExternalReference(
|
||||
source_name="ACME Bugzilla",
|
||||
external_id="1370",
|
||||
url="https://www.example.com/bugs/1370",
|
||||
)
|
||||
|
||||
assert str(ref) == BUGZILLA
|
||||
|
||||
|
||||
OFFLINE = """{
|
||||
"description": "Threat report",
|
||||
"source_name": "ACME Threat Intel"
|
||||
}"""
|
||||
|
||||
|
||||
def test_external_reference_offline():
|
||||
ref = stix2.ExternalReference(
|
||||
source_name="ACME Threat Intel",
|
||||
description="Threat report",
|
||||
)
|
||||
|
||||
assert str(ref) == OFFLINE
|
||||
assert repr(ref) == "ExternalReference(description='Threat report', source_name='ACME Threat Intel')"
|
||||
# Yikes! This works
|
||||
assert eval("stix2." + repr(ref)) == ref
|
||||
|
||||
|
||||
def test_external_reference_source_required():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
ref = stix2.ExternalReference()
|
||||
assert str(excinfo.value) == "Missing required field(s) for ExternalReference: (source_name)."
|
|
@ -0,0 +1,473 @@
|
|||
"""Tests for the stix2 library"""
|
||||
|
||||
import datetime as dt
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
|
||||
import stix2
|
||||
import stix2.utils
|
||||
|
||||
|
||||
FAKE_TIME = dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
|
||||
|
||||
|
||||
# Inspired by: http://stackoverflow.com/a/24006251
|
||||
@pytest.fixture
|
||||
def clock(monkeypatch):
|
||||
|
||||
class mydatetime(dt.datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
return FAKE_TIME
|
||||
|
||||
monkeypatch.setattr(dt, 'datetime', mydatetime)
|
||||
|
||||
|
||||
def test_clock(clock):
|
||||
assert dt.datetime.now() == FAKE_TIME
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def uuid4(monkeypatch):
|
||||
def wrapper():
|
||||
data = [0]
|
||||
|
||||
def wrapped():
|
||||
data[0] += 1
|
||||
return "00000000-0000-0000-0000-00000000%04x" % data[0]
|
||||
|
||||
return wrapped
|
||||
monkeypatch.setattr(uuid, "uuid4", wrapper())
|
||||
|
||||
|
||||
def test_my_uuid4_fixture(uuid4):
|
||||
assert uuid.uuid4() == "00000000-0000-0000-0000-000000000001"
|
||||
assert uuid.uuid4() == "00000000-0000-0000-0000-000000000002"
|
||||
assert uuid.uuid4() == "00000000-0000-0000-0000-000000000003"
|
||||
for _ in range(256):
|
||||
uuid.uuid4()
|
||||
assert uuid.uuid4() == "00000000-0000-0000-0000-000000000104"
|
||||
|
||||
|
||||
INDICATOR_ID = "indicator--01234567-89ab-cdef-0123-456789abcdef"
|
||||
MALWARE_ID = "malware--fedcba98-7654-3210-fedc-ba9876543210"
|
||||
RELATIONSHIP_ID = "relationship--00000000-1111-2222-3333-444444444444"
|
||||
|
||||
# Minimum required args for an Indicator instance
|
||||
INDICATOR_KWARGS = dict(
|
||||
labels=['malicious-activity'],
|
||||
pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
|
||||
)
|
||||
|
||||
# Minimum required args for a Malware instance
|
||||
MALWARE_KWARGS = dict(
|
||||
labels=['ransomware'],
|
||||
name="Cryptolocker",
|
||||
)
|
||||
|
||||
# Minimum required args for a Relationship instance
|
||||
RELATIONSHIP_KWARGS = dict(
|
||||
relationship_type="indicates",
|
||||
source_ref=INDICATOR_ID,
|
||||
target_ref=MALWARE_ID,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def indicator(uuid4, clock):
|
||||
return stix2.Indicator(**INDICATOR_KWARGS)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def malware(uuid4, clock):
|
||||
return stix2.Malware(**MALWARE_KWARGS)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def relationship(uuid4, clock):
|
||||
return stix2.Relationship(**RELATIONSHIP_KWARGS)
|
||||
|
||||
|
||||
EXPECTED_INDICATOR = """{
|
||||
"created": "2017-01-01T00:00:01Z",
|
||||
"id": "indicator--01234567-89ab-cdef-0123-456789abcdef",
|
||||
"labels": [
|
||||
"malicious-activity"
|
||||
],
|
||||
"modified": "2017-01-01T00:00:01Z",
|
||||
"pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
|
||||
"type": "indicator",
|
||||
"valid_from": "1970-01-01T00:00:01Z"
|
||||
}"""
|
||||
|
||||
EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join("""
|
||||
created=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
|
||||
id='indicator--01234567-89ab-cdef-0123-456789abcdef',
|
||||
labels=['malicious-activity'],
|
||||
modified=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
|
||||
pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
|
||||
type='indicator',
|
||||
valid_from=datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=<UTC>)
|
||||
""".split()) + ")"
|
||||
|
||||
|
||||
def test_indicator_with_all_required_fields():
|
||||
now = dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
|
||||
epoch = dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
|
||||
|
||||
indicator = stix2.Indicator(
|
||||
type="indicator",
|
||||
id=INDICATOR_ID,
|
||||
labels=['malicious-activity'],
|
||||
pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
|
||||
created=now,
|
||||
modified=now,
|
||||
valid_from=epoch,
|
||||
)
|
||||
|
||||
assert str(indicator) == EXPECTED_INDICATOR
|
||||
assert repr(indicator) == EXPECTED_INDICATOR_REPR
|
||||
|
||||
|
||||
def test_indicator_autogenerated_fields(indicator):
|
||||
assert indicator.type == 'indicator'
|
||||
assert indicator.id == 'indicator--00000000-0000-0000-0000-000000000001'
|
||||
assert indicator.created == FAKE_TIME
|
||||
assert indicator.modified == FAKE_TIME
|
||||
assert indicator.labels == ['malicious-activity']
|
||||
assert indicator.pattern == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']"
|
||||
assert indicator.valid_from == FAKE_TIME
|
||||
|
||||
assert indicator['type'] == 'indicator'
|
||||
assert indicator['id'] == 'indicator--00000000-0000-0000-0000-000000000001'
|
||||
assert indicator['created'] == FAKE_TIME
|
||||
assert indicator['modified'] == FAKE_TIME
|
||||
assert indicator['labels'] == ['malicious-activity']
|
||||
assert indicator['pattern'] == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']"
|
||||
assert indicator['valid_from'] == FAKE_TIME
|
||||
|
||||
|
||||
def test_indicator_type_must_be_indicator():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
indicator = stix2.Indicator(type='xxx', **INDICATOR_KWARGS)
|
||||
|
||||
assert str(excinfo.value) == "Indicator must have type='indicator'."
|
||||
|
||||
|
||||
def test_indicator_id_must_start_with_indicator():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
indicator = stix2.Indicator(id='my-prefix--', **INDICATOR_KWARGS)
|
||||
|
||||
assert str(excinfo.value) == "Indicator id values must begin with 'indicator--'."
|
||||
|
||||
|
||||
def test_indicator_required_fields():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
indicator = stix2.Indicator()
|
||||
assert str(excinfo.value) == "Missing required field(s) for Indicator: (labels, pattern)."
|
||||
|
||||
|
||||
def test_indicator_required_field_pattern():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
indicator = stix2.Indicator(labels=['malicious-activity'])
|
||||
assert str(excinfo.value) == "Missing required field(s) for Indicator: (pattern)."
|
||||
|
||||
|
||||
def test_cannot_assign_to_indicator_attributes(indicator):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
indicator.valid_from = dt.datetime.now()
|
||||
|
||||
assert str(excinfo.value) == "Cannot modify properties after creation."
|
||||
|
||||
|
||||
def test_invalid_kwarg_to_indicator():
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
indicator = stix2.Indicator(my_custom_property="foo", **INDICATOR_KWARGS)
|
||||
assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']"
|
||||
|
||||
|
||||
def test_created_modified_time_are_identical_by_default():
|
||||
"""By default, the created and modified times should be the same."""
|
||||
indicator = stix2.Indicator(**INDICATOR_KWARGS)
|
||||
|
||||
assert indicator.created == indicator.modified
|
||||
|
||||
|
||||
EXPECTED_MALWARE = """{
|
||||
"created": "2016-05-12T08:17:27Z",
|
||||
"id": "malware--fedcba98-7654-3210-fedc-ba9876543210",
|
||||
"labels": [
|
||||
"ransomware"
|
||||
],
|
||||
"modified": "2016-05-12T08:17:27Z",
|
||||
"name": "Cryptolocker",
|
||||
"type": "malware"
|
||||
}"""
|
||||
|
||||
|
||||
def test_malware_with_all_required_fields():
|
||||
now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
|
||||
|
||||
malware = stix2.Malware(
|
||||
type="malware",
|
||||
id=MALWARE_ID,
|
||||
created=now,
|
||||
modified=now,
|
||||
labels=["ransomware"],
|
||||
name="Cryptolocker",
|
||||
)
|
||||
|
||||
assert str(malware) == EXPECTED_MALWARE
|
||||
|
||||
|
||||
def test_malware_autogenerated_fields(malware):
|
||||
assert malware.type == 'malware'
|
||||
assert malware.id == 'malware--00000000-0000-0000-0000-000000000001'
|
||||
assert malware.created == FAKE_TIME
|
||||
assert malware.modified == FAKE_TIME
|
||||
assert malware.labels == ['ransomware']
|
||||
assert malware.name == "Cryptolocker"
|
||||
|
||||
assert malware['type'] == 'malware'
|
||||
assert malware['id'] == 'malware--00000000-0000-0000-0000-000000000001'
|
||||
assert malware['created'] == FAKE_TIME
|
||||
assert malware['modified'] == FAKE_TIME
|
||||
assert malware['labels'] == ['ransomware']
|
||||
assert malware['name'] == "Cryptolocker"
|
||||
|
||||
|
||||
def test_malware_type_must_be_malware():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
malware = stix2.Malware(type='xxx', **MALWARE_KWARGS)
|
||||
|
||||
assert str(excinfo.value) == "Malware must have type='malware'."
|
||||
|
||||
|
||||
def test_malware_id_must_start_with_malware():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
malware = stix2.Malware(id='my-prefix--', **MALWARE_KWARGS)
|
||||
|
||||
assert str(excinfo.value) == "Malware id values must begin with 'malware--'."
|
||||
|
||||
|
||||
def test_malware_required_fields():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
malware = stix2.Malware()
|
||||
assert str(excinfo.value) == "Missing required field(s) for Malware: (labels, name)."
|
||||
|
||||
|
||||
def test_malware_required_field_name():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
malware = stix2.Malware(labels=['ransomware'])
|
||||
assert str(excinfo.value) == "Missing required field(s) for Malware: (name)."
|
||||
|
||||
|
||||
def test_cannot_assign_to_malware_attributes(malware):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
malware.name = "Cryptolocker II"
|
||||
|
||||
assert str(excinfo.value) == "Cannot modify properties after creation."
|
||||
|
||||
|
||||
def test_invalid_kwarg_to_malware():
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
malware = stix2.Malware(my_custom_property="foo", **MALWARE_KWARGS)
|
||||
assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']"
|
||||
|
||||
|
||||
EXPECTED_RELATIONSHIP = """{
|
||||
"created": "2016-04-06T20:06:37Z",
|
||||
"id": "relationship--00000000-1111-2222-3333-444444444444",
|
||||
"modified": "2016-04-06T20:06:37Z",
|
||||
"relationship_type": "indicates",
|
||||
"source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
|
||||
"target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210",
|
||||
"type": "relationship"
|
||||
}"""
|
||||
|
||||
|
||||
def test_relationship_all_required_fields():
|
||||
now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc)
|
||||
|
||||
relationship = stix2.Relationship(
|
||||
type='relationship',
|
||||
id=RELATIONSHIP_ID,
|
||||
created=now,
|
||||
modified=now,
|
||||
relationship_type='indicates',
|
||||
source_ref=INDICATOR_ID,
|
||||
target_ref=MALWARE_ID,
|
||||
)
|
||||
assert str(relationship) == EXPECTED_RELATIONSHIP
|
||||
|
||||
|
||||
def test_relationship_autogenerated_fields(relationship):
|
||||
assert relationship.type == 'relationship'
|
||||
assert relationship.id == 'relationship--00000000-0000-0000-0000-000000000001'
|
||||
assert relationship.created == FAKE_TIME
|
||||
assert relationship.modified == FAKE_TIME
|
||||
assert relationship.relationship_type == 'indicates'
|
||||
assert relationship.source_ref == INDICATOR_ID
|
||||
assert relationship.target_ref == MALWARE_ID
|
||||
|
||||
assert relationship['type'] == 'relationship'
|
||||
assert relationship['id'] == 'relationship--00000000-0000-0000-0000-000000000001'
|
||||
assert relationship['created'] == FAKE_TIME
|
||||
assert relationship['modified'] == FAKE_TIME
|
||||
assert relationship['relationship_type'] == 'indicates'
|
||||
assert relationship['source_ref'] == INDICATOR_ID
|
||||
assert relationship['target_ref'] == MALWARE_ID
|
||||
|
||||
|
||||
def test_relationship_type_must_be_relationship():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
relationship = stix2.Relationship(type='xxx', **RELATIONSHIP_KWARGS)
|
||||
|
||||
assert str(excinfo.value) == "Relationship must have type='relationship'."
|
||||
|
||||
|
||||
def test_relationship_id_must_start_with_relationship():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
relationship = stix2.Relationship(id='my-prefix--', **RELATIONSHIP_KWARGS)
|
||||
|
||||
assert str(excinfo.value) == "Relationship id values must begin with 'relationship--'."
|
||||
|
||||
|
||||
def test_relationship_required_field_relationship_type():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
relationship = stix2.Relationship()
|
||||
assert str(excinfo.value) == "Missing required field(s) for Relationship: (relationship_type, source_ref, target_ref)."
|
||||
|
||||
|
||||
def test_relationship_missing_some_required_fields():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
# relationship_type is checked first, so make sure that is provided
|
||||
relationship = stix2.Relationship(relationship_type='indicates')
|
||||
assert str(excinfo.value) == "Missing required field(s) for Relationship: (source_ref, target_ref)."
|
||||
|
||||
|
||||
def test_relationship_required_field_target_ref():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
relationship = stix2.Relationship(
|
||||
relationship_type='indicates',
|
||||
source_ref=INDICATOR_ID
|
||||
)
|
||||
assert str(excinfo.value) == "Missing required field(s) for Relationship: (target_ref)."
|
||||
|
||||
|
||||
def test_cannot_assign_to_relationship_attributes(relationship):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
relationship.relationship_type = "derived-from"
|
||||
|
||||
assert str(excinfo.value) == "Cannot modify properties after creation."
|
||||
|
||||
|
||||
def test_invalid_kwarg_to_relationship():
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
relationship = stix2.Relationship(my_custom_property="foo", **RELATIONSHIP_KWARGS)
|
||||
assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']" in str(excinfo)
|
||||
|
||||
|
||||
def test_create_relationship_from_objects_rather_than_ids(indicator, malware):
|
||||
relationship = stix2.Relationship(
|
||||
relationship_type="indicates",
|
||||
source_ref=indicator,
|
||||
target_ref=malware,
|
||||
)
|
||||
|
||||
assert relationship.relationship_type == 'indicates'
|
||||
assert relationship.source_ref == 'indicator--00000000-0000-0000-0000-000000000001'
|
||||
assert relationship.target_ref == 'malware--00000000-0000-0000-0000-000000000002'
|
||||
assert relationship.id == 'relationship--00000000-0000-0000-0000-000000000003'
|
||||
|
||||
|
||||
def test_create_relationship_with_positional_args(indicator, malware):
|
||||
relationship = stix2.Relationship(indicator, 'indicates', malware)
|
||||
|
||||
assert relationship.relationship_type == 'indicates'
|
||||
assert relationship.source_ref == 'indicator--00000000-0000-0000-0000-000000000001'
|
||||
assert relationship.target_ref == 'malware--00000000-0000-0000-0000-000000000002'
|
||||
assert relationship.id == 'relationship--00000000-0000-0000-0000-000000000003'
|
||||
|
||||
|
||||
EXPECTED_BUNDLE = """{
|
||||
"id": "bundle--00000000-0000-0000-0000-000000000004",
|
||||
"objects": [
|
||||
{
|
||||
"created": "2017-01-01T12:34:56Z",
|
||||
"id": "indicator--00000000-0000-0000-0000-000000000001",
|
||||
"labels": [
|
||||
"malicious-activity"
|
||||
],
|
||||
"modified": "2017-01-01T12:34:56Z",
|
||||
"pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
|
||||
"type": "indicator",
|
||||
"valid_from": "2017-01-01T12:34:56Z"
|
||||
},
|
||||
{
|
||||
"created": "2017-01-01T12:34:56Z",
|
||||
"id": "malware--00000000-0000-0000-0000-000000000002",
|
||||
"labels": [
|
||||
"ransomware"
|
||||
],
|
||||
"modified": "2017-01-01T12:34:56Z",
|
||||
"name": "Cryptolocker",
|
||||
"type": "malware"
|
||||
},
|
||||
{
|
||||
"created": "2017-01-01T12:34:56Z",
|
||||
"id": "relationship--00000000-0000-0000-0000-000000000003",
|
||||
"modified": "2017-01-01T12:34:56Z",
|
||||
"relationship_type": "indicates",
|
||||
"source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
|
||||
"target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210",
|
||||
"type": "relationship"
|
||||
}
|
||||
],
|
||||
"spec_version": "2.0",
|
||||
"type": "bundle"
|
||||
}"""
|
||||
|
||||
|
||||
def test_empty_bundle():
|
||||
bundle = stix2.Bundle()
|
||||
|
||||
assert bundle.type == "bundle"
|
||||
assert bundle.id.startswith("bundle--")
|
||||
assert bundle.spec_version == "2.0"
|
||||
assert bundle.objects is None
|
||||
|
||||
|
||||
def test_bundle_with_wrong_type():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bundle = stix2.Bundle(type="not-a-bundle")
|
||||
|
||||
assert str(excinfo.value) == "Bundle must have type='bundle'."
|
||||
|
||||
|
||||
def test_bundle_id_must_start_with_bundle():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bundle = stix2.Bundle(id='my-prefix--')
|
||||
|
||||
assert str(excinfo.value) == "Bundle id values must begin with 'bundle--'."
|
||||
|
||||
|
||||
def test_bundle_with_wrong_spec_version():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
bundle = stix2.Bundle(spec_version="1.2")
|
||||
|
||||
assert str(excinfo.value) == "Bundle must have spec_version='2.0'."
|
||||
|
||||
|
||||
def test_create_bundle(indicator, malware, relationship):
|
||||
bundle = stix2.Bundle(objects=[indicator, malware, relationship])
|
||||
|
||||
assert str(bundle) == EXPECTED_BUNDLE
|
||||
|
||||
|
||||
def test_create_bundle_with_positional_args(indicator, malware, relationship):
|
||||
bundle = stix2.Bundle(indicator, malware, relationship)
|
||||
|
||||
assert str(bundle) == EXPECTED_BUNDLE
|
|
@ -0,0 +1,19 @@
|
|||
import datetime as dt
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
|
||||
import stix2.utils
|
||||
|
||||
amsterdam = pytz.timezone('Europe/Amsterdam')
|
||||
eastern = pytz.timezone('US/Eastern')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('dttm,timestamp', [
|
||||
(dt.datetime(2017, 1, 1, tzinfo=pytz.utc), '2017-01-01T00:00:00Z'),
|
||||
(amsterdam.localize(dt.datetime(2017, 1, 1)), '2016-12-31T23:00:00Z'),
|
||||
(eastern.localize(dt.datetime(2017, 1, 1, 12, 34, 56)), '2017-01-01T17:34:56Z'),
|
||||
(eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'),
|
||||
])
|
||||
def test_timestamp_formatting(dttm, timestamp):
|
||||
assert stix2.utils.format_datetime(dttm) == timestamp
|
|
@ -0,0 +1,26 @@
|
|||
"""Utility functions and classes for the stix2 library."""
|
||||
|
||||
import datetime as dt
|
||||
|
||||
import pytz
|
||||
|
||||
# Sentinel value for fields that should be set to the current time.
|
||||
# We can't use the standard 'default' approach, since if there are multiple
|
||||
# timestamps in a single object, the timestamps will vary by a few microseconds.
|
||||
NOW = object()
|
||||
|
||||
|
||||
def get_timestamp():
|
||||
return dt.datetime.now(tz=pytz.UTC)
|
||||
|
||||
|
||||
def format_datetime(dttm):
|
||||
# TODO: how to handle naive datetime
|
||||
|
||||
# 1. Convert to UTC
|
||||
# 2. Format in ISO format
|
||||
# 3. Strip off "+00:00"
|
||||
# 4. Add "Z"
|
||||
|
||||
# TODO: how to handle timestamps with subsecond 0's
|
||||
return dttm.astimezone(pytz.utc).isoformat()[:-6] + "Z"
|
|
@ -0,0 +1,23 @@
|
|||
[tox]
|
||||
envlist = py26,py27,py33,py34,py35,py36,pycodestyle
|
||||
|
||||
[testenv]
|
||||
deps = pytest
|
||||
commands = pytest
|
||||
|
||||
[testenv:pycodestyle]
|
||||
deps = pycodestyle
|
||||
commands = pycodestyle ./stix2
|
||||
|
||||
[pycodestyle]
|
||||
ignore=
|
||||
max-line-length=160
|
||||
|
||||
[travis]
|
||||
python =
|
||||
2.6: py26
|
||||
2.7: py27, pycodestyle
|
||||
3.3: py33
|
||||
3.4: py34
|
||||
3.5: py35
|
||||
3.6: py36
|
Loading…
Reference in New Issue