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.
|
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
|
## 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