Merge pull request #3 from oasis-open/parsing

Parsing
stix2.1
Greg Back 2017-04-25 09:40:59 -05:00 committed by GitHub
commit 7a8e6341b2
31 changed files with 1114 additions and 322 deletions

5
.gitignore vendored
View File

@ -57,6 +57,9 @@ docs/_build/
# PyBuilder # PyBuilder
target/ target/
#pycharm stuff # Vim
*.swp
#
# PyCharm
.idea/ .idea/

View File

@ -1,8 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
from setuptools import setup, find_packages from setuptools import find_packages, setup
install_requires = [ install_requires = [
'pytz', 'pytz',
'six',
'python-dateutil',
] ]
setup( setup(

View File

@ -3,11 +3,49 @@
# flake8: noqa # flake8: noqa
from .bundle import Bundle from .bundle import Bundle
from .common import ExternalReference, KillChainPhase from .other import ExternalReference, KillChainPhase, MarkingDefinition, \
GranularMarking, StatementMarking, TLPMarking
from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \ from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \
IntrusionSet, Malware, ObservedData, Report, ThreatActor, Tool, \ IntrusionSet, Malware, ObservedData, Report, ThreatActor, Tool, \
Vulnerability Vulnerability
from .sro import Relationship, Sighting from .sro import Relationship, Sighting
from .markings import MarkingDefinition, GranularMarking, StatementMarking, TLPMarking from .utils import get_dict
from . import exceptions from . import exceptions
OBJ_MAP = {
'attack-pattern': AttackPattern,
'campaign': Campaign,
'course-of-action': CourseOfAction,
'identity': Identity,
'indicator': Indicator,
'intrusion-set': IntrusionSet,
'malware': Malware,
'marking-definition': MarkingDefinition,
'observed-data': ObservedData,
'report': Report,
'relationship': Relationship,
'threat-actor': ThreatActor,
'tool': Tool,
'sighting': Sighting,
'vulnerability': Vulnerability,
}
def parse(data):
"""Deserialize a string or file-like object into a STIX object"""
obj = get_dict(data)
if 'type' not in obj:
# TODO parse external references, kill chain phases, and granular markings
pass
else:
try:
obj_class = OBJ_MAP[obj['type']]
return obj_class(**obj)
except KeyError:
# TODO handle custom objects
raise ValueError("Can't parse unknown object type!")
return obj

View File

@ -41,7 +41,7 @@ class _STIXBase(collections.Mapping):
if prop_name in kwargs: if prop_name in kwargs:
try: try:
kwargs[prop_name] = prop.validate(kwargs[prop_name]) kwargs[prop_name] = prop.clean(kwargs[prop_name])
except ValueError as exc: except ValueError as exc:
raise InvalidValueError(self.__class__, prop_name, reason=str(exc)) raise InvalidValueError(self.__class__, prop_name, reason=str(exc))
@ -56,6 +56,7 @@ class _STIXBase(collections.Mapping):
if extra_kwargs: if extra_kwargs:
raise ExtraFieldsError(cls, extra_kwargs) raise ExtraFieldsError(cls, extra_kwargs)
# Detect any missing required fields
required_fields = get_required_properties(cls._properties) required_fields = get_required_properties(cls._properties)
missing_kwargs = set(required_fields) - set(kwargs) missing_kwargs = set(required_fields) - set(kwargs)
if missing_kwargs: if missing_kwargs:

View File

@ -1,7 +1,7 @@
"""STIX 2 Bundle object""" """STIX 2 Bundle object"""
from .base import _STIXBase from .base import _STIXBase
from .properties import IDProperty, TypeProperty, Property from .properties import IDProperty, Property, TypeProperty
class Bundle(_STIXBase): class Bundle(_STIXBase):

View File

@ -1,32 +1,17 @@
"""STIX 2 Common Data Types and Properties""" """STIX 2 Common Data Types and Properties"""
from .base import _STIXBase from .other import ExternalReference, GranularMarking
from .properties import Property, BooleanProperty, ReferenceProperty, ListProperty from .properties import (BooleanProperty, ListProperty, ReferenceProperty,
TimestampProperty)
from .utils import NOW from .utils import NOW
COMMON_PROPERTIES = { COMMON_PROPERTIES = {
# 'type' and 'id' should be defined on each individual type # 'type' and 'id' should be defined on each individual type
'created': Property(default=lambda: NOW), 'created': TimestampProperty(default=lambda: NOW),
'modified': Property(default=lambda: NOW), 'modified': TimestampProperty(default=lambda: NOW),
'external_references': Property(), 'external_references': ListProperty(ExternalReference),
'revoked': BooleanProperty(), 'revoked': BooleanProperty(),
'created_by_ref': ReferenceProperty(type="identity"), 'created_by_ref': ReferenceProperty(type="identity"),
'object_marking_refs': ListProperty(ReferenceProperty, element_type="marking-definition"), 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")),
'granular_markings': ListProperty(Property) 'granular_markings': ListProperty(GranularMarking),
} }
class ExternalReference(_STIXBase):
_properties = {
'source_name': Property(required=True),
'description': Property(),
'url': Property(),
'external_id': Property(),
}
class KillChainPhase(_STIXBase):
_properties = {
'kill_chain_name': Property(required=True),
'phase_name': Property(required=True),
}

View File

@ -1,76 +0,0 @@
"""STIX 2.0 Marking Objects"""
from .base import _STIXBase
from .properties import IDProperty, TypeProperty, ListProperty, ReferenceProperty, Property, SelectorProperty
from .utils import NOW
class MarkingDefinition(_STIXBase):
_type = 'marking-definition'
_properties = {
'created': Property(default=lambda: NOW),
'external_references': Property(),
'created_by_ref': ReferenceProperty(type="identity"),
'object_marking_refs': ListProperty(ReferenceProperty, element_type="marking-definition"),
'granular_marking': ListProperty(Property, element_type="granular-marking"),
'type': TypeProperty(_type),
'id': IDProperty(_type),
'definition_type': Property(),
'definition': Property(),
}
class GranularMarking(_STIXBase):
_properties = {
'marking_ref': ReferenceProperty(required=True, type="marking-definition"),
'selectors': ListProperty(SelectorProperty, required=True),
}
class TLPMarking(_STIXBase):
# TODO: don't allow the creation of any other TLPMarkings than the ones below
_properties = {
'tlp': Property(required=True)
}
class StatementMarking(_STIXBase):
_properties = {
'statement': Property(required=True)
}
def __init__(self, statement=None, **kwargs):
# Allow statement as positional args.
if statement and not kwargs.get('statement'):
kwargs['statement'] = statement
super(StatementMarking, self).__init__(**kwargs)
TLP_WHITE = MarkingDefinition(
id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="white")
)
TLP_GREEN = MarkingDefinition(
id="marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="green")
)
TLP_AMBER = MarkingDefinition(
id="marking-definition--f88d31f6-486f-44da-b317-01333bde0b82",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="amber")
)
TLP_RED = MarkingDefinition(
id="marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="red")
)

124
stix2/other.py Normal file
View File

@ -0,0 +1,124 @@
"""STIX 2.0 Objects that are neither SDOs nor SROs"""
from .base import _STIXBase
from .properties import (IDProperty, ListProperty, Property, ReferenceProperty,
SelectorProperty, StringProperty, TimestampProperty,
TypeProperty)
from .utils import get_dict, NOW
class ExternalReference(_STIXBase):
_properties = {
'source_name': StringProperty(required=True),
'description': StringProperty(),
'url': StringProperty(),
'external_id': StringProperty(),
}
class KillChainPhase(_STIXBase):
_properties = {
'kill_chain_name': StringProperty(required=True),
'phase_name': StringProperty(required=True),
}
class GranularMarking(_STIXBase):
_properties = {
'marking_ref': ReferenceProperty(required=True, type="marking-definition"),
'selectors': ListProperty(SelectorProperty, required=True),
}
class TLPMarking(_STIXBase):
# TODO: don't allow the creation of any other TLPMarkings than the ones below
_properties = {
'tlp': Property(required=True)
}
class StatementMarking(_STIXBase):
_properties = {
'statement': StringProperty(required=True)
}
def __init__(self, statement=None, **kwargs):
# Allow statement as positional args.
if statement and not kwargs.get('statement'):
kwargs['statement'] = statement
super(StatementMarking, self).__init__(**kwargs)
class MarkingProperty(Property):
"""Represent the marking objects in the `definition` property of
marking-definition objects.
"""
def clean(self, value):
if type(value) in [TLPMarking, StatementMarking]:
return value
else:
raise ValueError("must be a Statement or TLP Marking.")
class MarkingDefinition(_STIXBase):
_type = 'marking-definition'
_properties = {
'created': TimestampProperty(default=lambda: NOW, required=True),
'external_references': ListProperty(ExternalReference),
'created_by_ref': ReferenceProperty(type="identity"),
'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")),
'granular_markings': ListProperty(GranularMarking),
'type': TypeProperty(_type),
'id': IDProperty(_type),
'definition_type': StringProperty(required=True),
'definition': MarkingProperty(required=True),
}
marking_map = {
'tlp': TLPMarking,
'statement': StatementMarking,
}
def __init__(self, **kwargs):
if set(('definition_type', 'definition')).issubset(kwargs.keys()):
# Create correct marking type object
try:
marking_type = self.marking_map[kwargs['definition_type']]
except KeyError:
raise ValueError("definition_type must be a valid marking type")
if not isinstance(kwargs['definition'], marking_type):
defn = get_dict(kwargs['definition'])
kwargs['definition'] = marking_type(**defn)
super(MarkingDefinition, self).__init__(**kwargs)
TLP_WHITE = MarkingDefinition(
id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="white")
)
TLP_GREEN = MarkingDefinition(
id="marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="green")
)
TLP_AMBER = MarkingDefinition(
id="marking-definition--f88d31f6-486f-44da-b317-01333bde0b82",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="amber")
)
TLP_RED = MarkingDefinition(
id="marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed",
created="2017-01-20T00:00:00.000Z",
definition_type="tlp",
definition=TLPMarking(tlp="red")
)

View File

@ -1,5 +1,13 @@
import collections
import datetime as dt
import inspect
import re import re
import uuid import uuid
from dateutil import parser
import pytz
from six import text_type
from .base import _STIXBase from .base import _STIXBase
@ -17,7 +25,7 @@ class Property(object):
you to copy *all* values from an existing object to a new object), but you to copy *all* values from an existing object to a new object), but
if the user provides a value other than the `fixed` value, it will raise if the user provides a value other than the `fixed` value, it will raise
an error. This is semantically equivalent to defining both: an error. This is semantically equivalent to defining both:
- a `validate()` function that checks if the value matches the fixed - a `clean()` function that checks if the value matches the fixed
value, and value, and
- a `default()` function that returns the fixed value. - a `default()` function that returns the fixed value.
(Default: `None`) (Default: `None`)
@ -25,15 +33,10 @@ class Property(object):
Subclasses can also define the following functions. Subclasses can also define the following functions.
- `def clean(self, value) -> any:` - `def clean(self, value) -> any:`
- Transform `value` into a valid value for this property. This should - Return a value that is valid for this property. If `value` is not
raise a ValueError if such no such transformation is possible. valid for this property, this will attempt to transform it first. If
- `def validate(self, value) -> any:` `value` is not valid and no such transformation is possible, it should
- check that `value` is valid for this property. This should return raise a ValueError.
a valid value (possibly modified) for this property, or raise a
ValueError if the value is not valid.
(Default: if `clean` is defined, it will attempt to call `clean` and
return the result or pass on a ValueError that `clean` raises. If
`clean` is not defined, this will return `value` unmodified).
- `def default(self):` - `def default(self):`
- provide a default value for this property. - provide a default value for this property.
- `default()` can return the special value `NOW` to use the current - `default()` can return the special value `NOW` to use the current
@ -41,58 +44,94 @@ class Property(object):
to use the same default value, so calling now() for each field-- to use the same default value, so calling now() for each field--
likely several microseconds apart-- does not work. likely several microseconds apart-- does not work.
Subclasses can instead provide lambda functions for `clean`, and `default` Subclasses can instead provide a lambda function for `default` as a keyword
as keyword arguments. `validate` should not be provided as a lambda since argument. `clean` should not be provided as a lambda since lambdas cannot
lambdas cannot raise their own exceptions. raise their own exceptions.
When instantiating Properties, `required` and `default` should not be used
together. `default` implies that the field is required in the specification
so this function will be used to supply a value if none is provided.
`required` means that the user must provide this; it is required in the
specification and we can't or don't want to create a default value.
""" """
def _default_validate(self, value): def _default_clean(self, value):
if value != self._fixed_value: if value != self._fixed_value:
raise ValueError("must equal '{0}'.".format(self._fixed_value)) raise ValueError("must equal '{0}'.".format(self._fixed_value))
return value return value
def __init__(self, required=False, fixed=None, clean=None, default=None, type=None): def __init__(self, required=False, fixed=None, default=None, type=None):
self.required = required self.required = required
self.type = type self.type = type
if fixed: if fixed:
self._fixed_value = fixed self._fixed_value = fixed
self.validate = self._default_validate self.clean = self._default_clean
self.default = lambda: fixed self.default = lambda: fixed
if clean:
self.clean = clean
if default: if default:
self.default = default self.default = default
def clean(self, value): def clean(self, value):
raise NotImplementedError return value
def validate(self, value): def __call__(self, value=None):
try: """Used by ListProperty to handle lists that have been defined with
value = self.clean(value) either a class or an instance.
except NotImplementedError: """
pass
return value return value
class ListProperty(Property): class ListProperty(Property):
def __init__(self, contained, required=False, element_type=None): def __init__(self, contained, **kwargs):
""" """
contained should be a type whose constructor creates an object from the value Contained should be a function which returns an object from the value.
""" """
self.contained = contained if inspect.isclass(contained) and issubclass(contained, Property):
self.element_type = element_type # If it's a class and not an instance, instantiate it so that
super(ListProperty, self).__init__(required) # clean() can be called on it, and ListProperty.clean() will
# use __call__ when it appends the item.
def validate(self, value): self.contained = contained()
# TODO: ensure iterable else:
result = [] self.contained = contained
for item in value: super(ListProperty, self).__init__(**kwargs)
result.append(self.contained(type=self.element_type).validate(item))
return result
def clean(self, value): def clean(self, value):
return [self.contained(x) for x in value] try:
iter(value)
except TypeError:
raise ValueError("must be an iterable.")
result = []
for item in value:
try:
valid = self.contained.clean(item)
except ValueError:
raise
except AttributeError:
# type of list has no clean() function (eg. built in Python types)
# TODO Should we raise an error here?
valid = item
if isinstance(valid, collections.Mapping):
result.append(self.contained(**valid))
else:
result.append(self.contained(valid))
# STIX spec forbids empty lists
if len(result) < 1:
raise ValueError("must not be empty.")
return result
class StringProperty(Property):
def __init__(self, **kwargs):
self.string_type = text_type
super(StringProperty, self).__init__(**kwargs)
def clean(self, value):
return self.string_type(value)
class TypeProperty(Property): class TypeProperty(Property):
@ -106,23 +145,72 @@ class IDProperty(Property):
self.required_prefix = type + "--" self.required_prefix = type + "--"
super(IDProperty, self).__init__() super(IDProperty, self).__init__()
def validate(self, value): def clean(self, value):
# TODO: validate GUID as well
if not value.startswith(self.required_prefix): if not value.startswith(self.required_prefix):
raise ValueError("must start with '{0}'.".format(self.required_prefix)) raise ValueError("must start with '{0}'.".format(self.required_prefix))
try:
uuid.UUID(value.split('--', 1)[1])
except Exception:
raise ValueError("must have a valid UUID after the prefix.")
return value return value
def default(self): def default(self):
return self.required_prefix + str(uuid.uuid4()) return self.required_prefix + str(uuid.uuid4())
class BooleanProperty(Property): class IntegerProperty(Property):
# TODO: Consider coercing some values (like the strings "true" and "false")
def validate(self, value): def clean(self, value):
if not isinstance(value, bool): try:
raise ValueError("must be a boolean value.") return int(value)
return value except Exception:
raise ValueError("must be an integer.")
class BooleanProperty(Property):
def clean(self, value):
if isinstance(value, bool):
return value
trues = ['true', 't']
falses = ['false', 'f']
try:
if value.lower() in trues:
return True
if value.lower() in falses:
return False
except AttributeError:
if value == 1:
return True
if value == 0:
return False
raise ValueError("must be a boolean value.")
class TimestampProperty(Property):
def clean(self, value):
if isinstance(value, dt.date):
if hasattr(value, 'hour'):
return value
else:
# Add a time component
return dt.datetime.combine(value, dt.time(), tzinfo=pytz.utc)
# value isn't a date or datetime object so assume it's a string
try:
parsed = parser.parse(value)
except TypeError:
# Unknown format
raise ValueError("must be a datetime object, date object, or "
"timestamp string in a recognizable format.")
if parsed.tzinfo:
return parsed.astimezone(pytz.utc)
else:
# Doesn't have timezone info in the string; assume UTC
return pytz.utc.localize(parsed)
REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}"
@ -137,7 +225,7 @@ class ReferenceProperty(Property):
self.type = type self.type = type
super(ReferenceProperty, self).__init__(required, type=type) super(ReferenceProperty, self).__init__(required, type=type)
def validate(self, value): def clean(self, value):
if isinstance(value, _STIXBase): if isinstance(value, _STIXBase):
value = value.id value = value.id
if self.type: if self.type:
@ -156,7 +244,7 @@ class SelectorProperty(Property):
# ignore type # ignore type
super(SelectorProperty, self).__init__() super(SelectorProperty, self).__init__()
def validate(self, value): def clean(self, value):
if not SELECTOR_REGEX.match(value): if not SELECTOR_REGEX.match(value):
raise ValueError("must adhere to selector syntax.") raise ValueError("must adhere to selector syntax.")
return value return value

View File

@ -2,7 +2,10 @@
from .base import _STIXBase from .base import _STIXBase
from .common import COMMON_PROPERTIES from .common import COMMON_PROPERTIES
from .properties import IDProperty, TypeProperty, ListProperty, ReferenceProperty, Property from .other import KillChainPhase
from .properties import (IDProperty, IntegerProperty, ListProperty, Property,
ReferenceProperty, StringProperty, TimestampProperty,
TypeProperty)
from .utils import NOW from .utils import NOW
@ -13,9 +16,9 @@ class AttackPattern(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'kill_chain_phases': Property(), 'kill_chain_phases': ListProperty(KillChainPhase),
}) })
@ -26,12 +29,12 @@ class Campaign(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'aliases': Property(), 'aliases': ListProperty(StringProperty),
'first_seen': Property(), 'first_seen': TimestampProperty(),
'last_seen': Property(), 'last_seen': TimestampProperty(),
'objective': Property(), 'objective': StringProperty(),
}) })
@ -42,8 +45,8 @@ class CourseOfAction(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
}) })
@ -54,11 +57,12 @@ class Identity(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'name': Property(required=True), 'labels': ListProperty(StringProperty),
'description': Property(), 'name': StringProperty(required=True),
'identity_class': Property(required=True), 'description': StringProperty(),
'sectors': Property(), 'identity_class': StringProperty(required=True),
'contact_information': Property(), 'sectors': ListProperty(StringProperty),
'contact_information': StringProperty(),
}) })
@ -69,13 +73,13 @@ class Indicator(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'labels': Property(required=True), 'labels': ListProperty(StringProperty, required=True),
'name': Property(), 'name': StringProperty(),
'description': Property(), 'description': StringProperty(),
'pattern': Property(required=True), 'pattern': StringProperty(required=True),
'valid_from': Property(default=lambda: NOW), 'valid_from': TimestampProperty(default=lambda: NOW),
'valid_until': Property(), 'valid_until': TimestampProperty(),
'kill_chain_phases': Property(), 'kill_chain_phases': ListProperty(KillChainPhase),
}) })
@ -86,15 +90,15 @@ class IntrusionSet(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'aliases': Property(), 'aliases': ListProperty(StringProperty),
'first_seen': Property(), 'first_seen': TimestampProperty(),
'last_seen ': Property(), 'last_seen ': TimestampProperty(),
'goals': Property(), 'goals': ListProperty(StringProperty),
'resource_level': Property(), 'resource_level': StringProperty(),
'primary_motivation': Property(), 'primary_motivation': StringProperty(),
'secondary_motivations': Property(), 'secondary_motivations': ListProperty(StringProperty),
}) })
@ -105,10 +109,10 @@ class Malware(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'labels': Property(required=True), 'labels': ListProperty(StringProperty, required=True),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'kill_chain_phases': Property(), 'kill_chain_phases': ListProperty(KillChainPhase),
}) })
@ -119,9 +123,9 @@ class ObservedData(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'first_observed': Property(), 'first_observed': TimestampProperty(required=True),
'last_observed': Property(), 'last_observed': TimestampProperty(required=True),
'number_observed': Property(), 'number_observed': IntegerProperty(required=True),
'objects': Property(), 'objects': Property(),
}) })
@ -133,10 +137,10 @@ class Report(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'labels': Property(required=True), 'labels': ListProperty(StringProperty, required=True),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'published': Property(), 'published': TimestampProperty(),
'object_refs': ListProperty(ReferenceProperty), 'object_refs': ListProperty(ReferenceProperty),
}) })
@ -148,17 +152,17 @@ class ThreatActor(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'labels': Property(required=True), 'labels': ListProperty(StringProperty, required=True),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'aliases': Property(), 'aliases': ListProperty(StringProperty),
'roles': Property(), 'roles': ListProperty(StringProperty),
'goals': Property(), 'goals': ListProperty(StringProperty),
'sophistication': Property(), 'sophistication': StringProperty(),
'resource_level': Property(), 'resource_level': StringProperty(),
'primary_motivation': Property(), 'primary_motivation': StringProperty(),
'secondary_motivations': Property(), 'secondary_motivations': ListProperty(StringProperty),
'personal_motivations': Property(), 'personal_motivations': ListProperty(StringProperty),
}) })
@ -169,11 +173,11 @@ class Tool(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'labels': Property(required=True), 'labels': ListProperty(StringProperty, required=True),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'kill_chain_phases': Property(), 'kill_chain_phases': ListProperty(KillChainPhase),
'tool_version': Property(), 'tool_version': StringProperty(),
}) })
@ -184,6 +188,6 @@ class Vulnerability(_STIXBase):
_properties.update({ _properties.update({
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'id': IDProperty(_type), 'id': IDProperty(_type),
'name': Property(required=True), 'name': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
}) })

View File

@ -2,7 +2,9 @@
from .base import _STIXBase from .base import _STIXBase
from .common import COMMON_PROPERTIES from .common import COMMON_PROPERTIES
from .properties import IDProperty, TypeProperty, ReferenceProperty, ListProperty, Property from .properties import (IDProperty, IntegerProperty, ListProperty,
ReferenceProperty, StringProperty, TimestampProperty,
TypeProperty)
class Relationship(_STIXBase): class Relationship(_STIXBase):
@ -12,8 +14,8 @@ class Relationship(_STIXBase):
_properties.update({ _properties.update({
'id': IDProperty(_type), 'id': IDProperty(_type),
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'relationship_type': Property(required=True), 'relationship_type': StringProperty(required=True),
'description': Property(), 'description': StringProperty(),
'source_ref': ReferenceProperty(required=True), 'source_ref': ReferenceProperty(required=True),
'target_ref': ReferenceProperty(required=True), 'target_ref': ReferenceProperty(required=True),
}) })
@ -41,13 +43,13 @@ class Sighting(_STIXBase):
_properties.update({ _properties.update({
'id': IDProperty(_type), 'id': IDProperty(_type),
'type': TypeProperty(_type), 'type': TypeProperty(_type),
'first_seen': Property(), 'first_seen': TimestampProperty(),
'last_seen': Property(), 'last_seen': TimestampProperty(),
'count': Property(), 'count': IntegerProperty(),
'sighting_of_ref': ReferenceProperty(required=True), 'sighting_of_ref': ReferenceProperty(required=True),
'observed_data_refs': ListProperty(ReferenceProperty, element_type="observed-data"), 'observed_data_refs': ListProperty(ReferenceProperty(type="observed-data")),
'where_sighted_refs': ListProperty(ReferenceProperty, element_type="identity"), 'where_sighted_refs': ListProperty(ReferenceProperty(type="identity")),
'summary': Property(), 'summary': StringProperty(),
}) })
# Explicitly define the first kwargs to make readable Sighting declarations. # Explicitly define the first kwargs to make readable Sighting declarations.

View File

@ -4,11 +4,21 @@ import pytz
FAKE_TIME = dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc) FAKE_TIME = dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
ATTACK_PATTERN_ID = "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061"
CAMPAIGN_ID = "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
COURSE_OF_ACTION_ID = "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
IDENTITY_ID = "identity--311b2d2d-f010-5473-83ec-1edf84858f4c"
INDICATOR_ID = "indicator--01234567-89ab-cdef-0123-456789abcdef" INDICATOR_ID = "indicator--01234567-89ab-cdef-0123-456789abcdef"
INTRUSION_SET_ID = "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29"
MALWARE_ID = "malware--fedcba98-7654-3210-fedc-ba9876543210" MALWARE_ID = "malware--fedcba98-7654-3210-fedc-ba9876543210"
MARKING_DEFINITION_ID = "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"
OBSERVED_DATA_ID = "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf"
REPORT_ID = "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3"
RELATIONSHIP_ID = "relationship--00000000-1111-2222-3333-444444444444" RELATIONSHIP_ID = "relationship--00000000-1111-2222-3333-444444444444"
IDENTITY_ID = "identity--d4d765ce-cff7-40e8-b7a6-e205d005ac2c" THREAT_ACTOR_ID = "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
TOOL_ID = "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
SIGHTING_ID = "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb" SIGHTING_ID = "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb"
VULNERABILITY_ID = "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061"
# Minimum required args for an Indicator instance # Minimum required args for an Indicator instance
INDICATOR_KWARGS = dict( INDICATOR_KWARGS = dict(

View File

@ -1,16 +1,22 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import ATTACK_PATTERN_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-05-12T08:17:27.000Z", "created": "2016-05-12T08:17:27Z",
"description": "...", "description": "...",
"external_references": [ "external_references": [
{ {
"id": "CAPEC-163", "external_id": "CAPEC-163",
"source_name": "capec" "source_name": "capec"
} }
], ],
"id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
"modified": "2016-05-12T08:17:27.000Z", "modified": "2016-05-12T08:17:27Z",
"name": "Spear Phishing", "name": "Spear Phishing",
"type": "attack-pattern" "type": "attack-pattern"
}""" }"""
@ -19,12 +25,12 @@ EXPECTED = """{
def test_attack_pattern_example(): def test_attack_pattern_example():
ap = stix2.AttackPattern( ap = stix2.AttackPattern(
id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
created="2016-05-12T08:17:27.000Z", created="2016-05-12T08:17:27Z",
modified="2016-05-12T08:17:27.000Z", modified="2016-05-12T08:17:27Z",
name="Spear Phishing", name="Spear Phishing",
external_references=[{ external_references=[{
"source_name": "capec", "source_name": "capec",
"id": "CAPEC-163" "external_id": "CAPEC-163"
}], }],
description="...", description="...",
) )
@ -32,4 +38,33 @@ def test_attack_pattern_example():
assert str(ap) == EXPECTED assert str(ap) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"type": "attack-pattern",
"id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
"created": "2016-05-12T08:17:27Z",
"modified": "2016-05-12T08:17:27Z",
"description": "...",
"external_references": [
{
"external_id": "CAPEC-163",
"source_name": "capec"
}
],
"name": "Spear Phishing",
},
])
def test_parse_attack_pattern(data):
ap = stix2.parse(data)
assert ap.type == 'attack-pattern'
assert ap.id == ATTACK_PATTERN_ID
assert ap.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
assert ap.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
assert ap.description == "..."
assert ap.external_references[0].external_id == 'CAPEC-163'
assert ap.external_references[0].source_name == 'capec'
assert ap.name == "Spear Phishing"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,11 +1,17 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import CAMPAIGN_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:00.000Z", "created": "2016-04-06T20:03:00Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Campaign by Green Group against a series of targets in the financial services sector.", "description": "Campaign by Green Group against a series of targets in the financial services sector.",
"id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"modified": "2016-04-06T20:03:00.000Z", "modified": "2016-04-06T20:03:00Z",
"name": "Green Group Attacks Against Finance", "name": "Green Group Attacks Against Finance",
"type": "campaign" "type": "campaign"
}""" }"""
@ -15,12 +21,36 @@ def test_campaign_example():
campaign = stix2.Campaign( campaign = stix2.Campaign(
id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:00.000Z", created="2016-04-06T20:03:00Z",
modified="2016-04-06T20:03:00.000Z", modified="2016-04-06T20:03:00Z",
name="Green Group Attacks Against Finance", name="Green Group Attacks Against Finance",
description="Campaign by Green Group against a series of targets in the financial services sector." description="Campaign by Green Group against a series of targets in the financial services sector."
) )
assert str(campaign) == EXPECTED assert str(campaign) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"type": "campaign",
"id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"created": "2016-04-06T20:03:00Z",
"modified": "2016-04-06T20:03:00Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Campaign by Green Group against a series of targets in the financial services sector.",
"name": "Green Group Attacks Against Finance",
},
])
def test_parse_campaign(data):
cmpn = stix2.parse(data)
assert cmpn.type == 'campaign'
assert cmpn.id == CAMPAIGN_ID
assert cmpn.created == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc)
assert cmpn.modified == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc)
assert cmpn.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff"
assert cmpn.description == "Campaign by Green Group against a series of targets in the financial services sector."
assert cmpn.name == "Green Group Attacks Against Finance"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,11 +1,17 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import COURSE_OF_ACTION_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:48.000Z", "created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...",
"id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"modified": "2016-04-06T20:03:48.000Z", "modified": "2016-04-06T20:03:48Z",
"name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
"type": "course-of-action" "type": "course-of-action"
}""" }"""
@ -15,12 +21,36 @@ def test_course_of_action_example():
coa = stix2.CourseOfAction( coa = stix2.CourseOfAction(
id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:48.000Z", created="2016-04-06T20:03:48Z",
modified="2016-04-06T20:03:48.000Z", modified="2016-04-06T20:03:48Z",
name="Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", name="Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..."
) )
assert str(coa) == EXPECTED assert str(coa) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...",
"id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"modified": "2016-04-06T20:03:48Z",
"name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
"type": "course-of-action"
},
])
def test_parse_course_of_action(data):
coa = stix2.parse(data)
assert coa.type == 'course-of-action'
assert coa.id == COURSE_OF_ACTION_ID
assert coa.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert coa.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert coa.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff"
assert coa.description == "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..."
assert coa.name == "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,7 +1,8 @@
"""Tests for stix.ExternalReference""" """Tests for stix.ExternalReference"""
import pytest import re
import pytest
import stix2 import stix2
VERIS = """{ VERIS = """{
@ -34,7 +35,7 @@ def test_external_reference_capec():
) )
assert str(ref) == CAPEC assert str(ref) == CAPEC
assert repr(ref) == "ExternalReference(external_id='CAPEC-550', source_name='capec')" assert re.match("ExternalReference\(external_id=u?'CAPEC-550', source_name=u?'capec'\)", repr(ref))
CAPEC_URL = """{ CAPEC_URL = """{
@ -101,7 +102,7 @@ def test_external_reference_offline():
) )
assert str(ref) == OFFLINE assert str(ref) == OFFLINE
assert repr(ref) == "ExternalReference(description='Threat report', source_name='ACME Threat Intel')" assert re.match("ExternalReference\(description=u?'Threat report', source_name=u?'ACME Threat Intel'\)", repr(ref))
# Yikes! This works # Yikes! This works
assert eval("stix2." + repr(ref)) == ref assert eval("stix2." + repr(ref)) == ref

View File

@ -1,10 +1,16 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import IDENTITY_ID
EXPECTED = """{ EXPECTED = """{
"created": "2015-12-21T19:59:11.000Z", "created": "2015-12-21T19:59:11Z",
"id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
"identity_class": "individual", "identity_class": "individual",
"modified": "2015-12-21T19:59:11.000Z", "modified": "2015-12-21T19:59:11Z",
"name": "John Smith", "name": "John Smith",
"type": "identity" "type": "identity"
}""" }"""
@ -13,12 +19,33 @@ EXPECTED = """{
def test_identity_example(): def test_identity_example():
report = stix2.Identity( report = stix2.Identity(
id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
created="2015-12-21T19:59:11.000Z", created="2015-12-21T19:59:11Z",
modified="2015-12-21T19:59:11.000Z", modified="2015-12-21T19:59:11Z",
name="John Smith", name="John Smith",
identity_class="individual", identity_class="individual",
) )
assert str(report) == EXPECTED assert str(report) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"created": "2015-12-21T19:59:11Z",
"id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
"identity_class": "individual",
"modified": "2015-12-21T19:59:11Z",
"name": "John Smith",
"type": "identity"
},
])
def test_parse_identity(data):
identity = stix2.parse(data)
assert identity.type == 'identity'
assert identity.id == IDENTITY_ID
assert identity.created == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc)
assert identity.modified == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc)
assert identity.name == "John Smith"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,8 +1,8 @@
import datetime as dt import datetime as dt
import re
import pytest import pytest
import pytz import pytz
import stix2 import stix2
from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS
@ -45,7 +45,8 @@ def test_indicator_with_all_required_fields():
) )
assert str(ind) == EXPECTED_INDICATOR assert str(ind) == EXPECTED_INDICATOR
assert repr(ind) == EXPECTED_INDICATOR_REPR rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(ind))
assert rep == EXPECTED_INDICATOR_REPR
def test_indicator_autogenerated_fields(indicator): def test_indicator_autogenerated_fields(indicator):
@ -116,12 +117,11 @@ def test_indicator_created_ref_invalid_format():
def test_indicator_revoked_invalid(): def test_indicator_revoked_invalid():
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Indicator(revoked='false', **INDICATOR_KWARGS) stix2.Indicator(revoked='no', **INDICATOR_KWARGS)
assert excinfo.value.cls == stix2.Indicator assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.prop_name == "revoked" assert excinfo.value.prop_name == "revoked"
assert excinfo.value.reason == "must be a boolean value." assert excinfo.value.reason == "must be a boolean value."
assert str(excinfo.value) == "Invalid value for Indicator 'revoked': must be a boolean value."
def test_cannot_assign_to_indicator_attributes(indicator): def test_cannot_assign_to_indicator_attributes(indicator):
@ -145,3 +145,29 @@ def test_created_modified_time_are_identical_by_default():
ind = stix2.Indicator(**INDICATOR_KWARGS) ind = stix2.Indicator(**INDICATOR_KWARGS)
assert ind.created == ind.modified assert ind.created == ind.modified
@pytest.mark.parametrize("data", [
EXPECTED_INDICATOR,
{
"type": "indicator",
"id": "indicator--01234567-89ab-cdef-0123-456789abcdef",
"created": "2017-01-01T00:00:01Z",
"modified": "2017-01-01T00:00:01Z",
"labels": [
"malicious-activity"
],
"pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
"valid_from": "1970-01-01T00:00:01Z"
},
])
def test_parse_indicator(data):
idctr = stix2.parse(data)
assert idctr.type == 'indicator'
assert idctr.id == INDICATOR_ID
assert idctr.created == dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
assert idctr.modified == dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
assert idctr.valid_from == dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc)
assert idctr.labels[0] == "malicious-activity"
assert idctr.pattern == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']"

View File

@ -1,10 +1,16 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import INTRUSION_SET_ID
EXPECTED = """{ EXPECTED = """{
"aliases": [ "aliases": [
"Zookeeper" "Zookeeper"
], ],
"created": "2016-04-06T20:03:48.000Z", "created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Incidents usually feature a shared TTP of a bobcat being released...", "description": "Incidents usually feature a shared TTP of a bobcat being released...",
"goals": [ "goals": [
@ -13,7 +19,7 @@ EXPECTED = """{
"damage" "damage"
], ],
"id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
"modified": "2016-04-06T20:03:48.000Z", "modified": "2016-04-06T20:03:48Z",
"name": "Bobcat Breakin", "name": "Bobcat Breakin",
"type": "intrusion-set" "type": "intrusion-set"
}""" }"""
@ -23,8 +29,8 @@ def test_intrusion_set_example():
intrusion_set = stix2.IntrusionSet( intrusion_set = stix2.IntrusionSet(
id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:48.000Z", created="2016-04-06T20:03:48Z",
modified="2016-04-06T20:03:48.000Z", modified="2016-04-06T20:03:48Z",
name="Bobcat Breakin", name="Bobcat Breakin",
description="Incidents usually feature a shared TTP of a bobcat being released...", description="Incidents usually feature a shared TTP of a bobcat being released...",
aliases=["Zookeeper"], aliases=["Zookeeper"],
@ -33,4 +39,37 @@ def test_intrusion_set_example():
assert str(intrusion_set) == EXPECTED assert str(intrusion_set) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"aliases": [
"Zookeeper"
],
"created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Incidents usually feature a shared TTP of a bobcat being released...",
"goals": [
"acquisition-theft",
"harassment",
"damage"
],
"id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
"modified": "2016-04-06T20:03:48Z",
"name": "Bobcat Breakin",
"type": "intrusion-set"
},
])
def test_parse_intrusion_set(data):
intset = stix2.parse(data)
assert intset.type == "intrusion-set"
assert intset.id == INTRUSION_SET_ID
assert intset.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert intset.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert intset.goals == ["acquisition-theft", "harassment", "damage"]
assert intset.aliases == ["Zookeeper"]
assert intset.description == "Incidents usually feature a shared TTP of a bobcat being released..."
assert intset.name == "Bobcat Breakin"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,8 +1,8 @@
import datetime as dt import datetime as dt
import re
import pytest import pytest
import pytz import pytz
import stix2 import stix2
from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS
@ -102,3 +102,61 @@ def test_invalid_kwarg_to_malware():
assert excinfo.value.cls == stix2.Malware assert excinfo.value.cls == stix2.Malware
assert excinfo.value.fields == ['my_custom_property'] assert excinfo.value.fields == ['my_custom_property']
assert str(excinfo.value) == "Unexpected field(s) for Malware: (my_custom_property)." assert str(excinfo.value) == "Unexpected field(s) for Malware: (my_custom_property)."
@pytest.mark.parametrize("data", [
EXPECTED_MALWARE,
{
"type": "malware",
"id": "malware--fedcba98-7654-3210-fedc-ba9876543210",
"created": "2016-05-12T08:17:27Z",
"modified": "2016-05-12T08:17:27Z",
"labels": ["ransomware"],
"name": "Cryptolocker",
},
])
def test_parse_malware(data):
mal = stix2.parse(data)
assert mal.type == 'malware'
assert mal.id == MALWARE_ID
assert mal.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
assert mal.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
assert mal.labels == ['ransomware']
assert mal.name == "Cryptolocker"
def test_parse_malware_invalid_labels():
data = re.compile('\[.+\]', re.DOTALL).sub('1', EXPECTED_MALWARE)
with pytest.raises(ValueError) as excinfo:
stix2.parse(data)
assert "Invalid value for Malware 'labels'" in str(excinfo.value)
def test_parse_malware_kill_chain_phases():
kill_chain = """
"kill_chain_phases": [
{
"kill_chain_name": "lockheed-martin-cyber-kill-chain",
"phase_name": "reconnaissance"
}
]"""
data = EXPECTED_MALWARE.replace('malware"', 'malware",%s' % kill_chain)
mal = stix2.parse(data)
assert mal.kill_chain_phases[0].kill_chain_name == "lockheed-martin-cyber-kill-chain"
assert mal.kill_chain_phases[0].phase_name == "reconnaissance"
assert mal['kill_chain_phases'][0]['kill_chain_name'] == "lockheed-martin-cyber-kill-chain"
assert mal['kill_chain_phases'][0]['phase_name'] == "reconnaissance"
def test_parse_malware_clean_kill_chain_phases():
kill_chain = """
"kill_chain_phases": [
{
"kill_chain_name": "lockheed-martin-cyber-kill-chain",
"phase_name": 1
}
]"""
data = EXPECTED_MALWARE.replace('malware"', 'malware",%s' % kill_chain)
mal = stix2.parse(data)
assert mal['kill_chain_phases'][0]['phase_name'] == "1"

View File

@ -1,9 +1,14 @@
import stix2 import datetime as dt
from stix2.markings import TLP_WHITE
import pytest import pytest
import pytz
import stix2
from stix2.other import TLP_WHITE
from .constants import MARKING_DEFINITION_ID
EXPECTED_TLP_MARKING_DEFINITION = """{ EXPECTED_TLP_MARKING_DEFINITION = """{
"created": "2017-01-20T00:00:00.000Z", "created": "2017-01-20T00:00:00Z",
"definition": { "definition": {
"tlp": "white" "tlp": "white"
}, },
@ -13,7 +18,7 @@ EXPECTED_TLP_MARKING_DEFINITION = """{
}""" }"""
EXPECTED_STATEMENT_MARKING_DEFINITION = """{ EXPECTED_STATEMENT_MARKING_DEFINITION = """{
"created": "2017-01-20T00:00:00.000Z", "created": "2017-01-20T00:00:00Z",
"definition": { "definition": {
"statement": "Copyright 2016, Example Corp" "statement": "Copyright 2016, Example Corp"
}, },
@ -33,7 +38,7 @@ EXPECTED_GRANULAR_MARKING = """{
}""" }"""
EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{
"created": "2016-04-06T20:03:00.000Z", "created": "2016-04-06T20:03:00Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Campaign by Green Group against a series of targets in the financial services sector.", "description": "Campaign by Green Group against a series of targets in the financial services sector.",
"granular_markings": [ "granular_markings": [
@ -45,7 +50,7 @@ EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{
} }
], ],
"id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"modified": "2016-04-06T20:03:00.000Z", "modified": "2016-04-06T20:03:00Z",
"name": "Green Group Attacks Against Finance", "name": "Green Group Attacks Against Finance",
"type": "campaign" "type": "campaign"
}""" }"""
@ -103,8 +108,8 @@ def test_campaign_with_granular_markings_example():
campaign = stix2.Campaign( campaign = stix2.Campaign(
id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:00.000Z", created="2016-04-06T20:03:00Z",
modified="2016-04-06T20:03:00.000Z", modified="2016-04-06T20:03:00Z",
name="Green Group Attacks Against Finance", name="Green Group Attacks Against Finance",
description="Campaign by Green Group against a series of targets in the financial services sector.", description="Campaign by Green Group against a series of targets in the financial services sector.",
granular_markings=[ granular_markings=[
@ -115,4 +120,27 @@ def test_campaign_with_granular_markings_example():
print(str(campaign)) print(str(campaign))
assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS
@pytest.mark.parametrize("data", [
EXPECTED_TLP_MARKING_DEFINITION,
{
"id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
"type": "marking-definition",
"created": "2017-01-20T00:00:00Z",
"definition": {
"tlp": "white"
},
"definition_type": "tlp",
},
])
def test_parse_marking_definition(data):
gm = stix2.parse(data)
assert gm.type == 'marking-definition'
assert gm.id == MARKING_DEFINITION_ID
assert gm.created == dt.datetime(2017, 1, 20, 0, 0, 0, tzinfo=pytz.utc)
assert gm.definition.tlp == "white"
assert gm.definition_type == "tlp"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,12 +1,18 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import OBSERVED_DATA_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T19:58:16.000Z", "created": "2016-04-06T19:58:16Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"first_observed": "2015-12-21T19:00:00Z", "first_observed": "2015-12-21T19:00:00Z",
"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
"last_observed": "2015-12-21T19:00:00Z", "last_observed": "2015-12-21T19:00:00Z",
"modified": "2016-04-06T19:58:16.000Z", "modified": "2016-04-06T19:58:16Z",
"number_observed": 50, "number_observed": 50,
"objects": { "objects": {
"0": { "0": {
@ -21,8 +27,8 @@ def test_observed_data_example():
observed_data = stix2.ObservedData( observed_data = stix2.ObservedData(
id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T19:58:16.000Z", created="2016-04-06T19:58:16Z",
modified="2016-04-06T19:58:16.000Z", modified="2016-04-06T19:58:16Z",
first_observed="2015-12-21T19:00:00Z", first_observed="2015-12-21T19:00:00Z",
last_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z",
number_observed=50, number_observed=50,
@ -35,4 +41,35 @@ def test_observed_data_example():
assert str(observed_data) == EXPECTED assert str(observed_data) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"type": "observed-data",
"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
"created": "2016-04-06T19:58:16Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"first_observed": "2015-12-21T19:00:00Z",
"last_observed": "2015-12-21T19:00:00Z",
"modified": "2016-04-06T19:58:16Z",
"number_observed": 50,
"objects": {
"0": {
"type": "file"
}
}
},
])
def test_parse_observed_data(data):
odata = stix2.parse(data)
assert odata.type == 'observed-data'
assert odata.id == OBSERVED_DATA_ID
assert odata.created == dt.datetime(2016, 4, 6, 19, 58, 16, tzinfo=pytz.utc)
assert odata.modified == dt.datetime(2016, 4, 6, 19, 58, 16, tzinfo=pytz.utc)
assert odata.first_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc)
assert odata.last_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc)
assert odata.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff"
# assert odata.objects["0"].type == "file" # TODO
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,7 +1,9 @@
import pytest import pytest
from stix2.properties import (Property, BooleanProperty, IDProperty, from stix2.properties import (BooleanProperty, IDProperty, IntegerProperty,
ReferenceProperty, TypeProperty) ListProperty, Property, ReferenceProperty,
StringProperty, TimestampProperty, TypeProperty)
from .constants import FAKE_TIME
def test_property(): def test_property():
@ -10,10 +12,10 @@ def test_property():
assert p.required is False assert p.required is False
def test_basic_validate(): def test_basic_clean():
class Prop(Property): class Prop(Property):
def validate(self, value): def clean(self, value):
if value == 42: if value == 42:
return value return value
else: else:
@ -21,9 +23,9 @@ def test_basic_validate():
p = Prop() p = Prop()
assert p.validate(42) == 42 assert p.clean(42) == 42
with pytest.raises(ValueError): with pytest.raises(ValueError):
p.validate(41) p.clean(41)
def test_default_field(): def test_default_field():
@ -40,48 +42,132 @@ def test_default_field():
def test_fixed_property(): def test_fixed_property():
p = Property(fixed="2.0") p = Property(fixed="2.0")
assert p.validate("2.0") assert p.clean("2.0")
with pytest.raises(ValueError): with pytest.raises(ValueError):
assert p.validate("x") is False assert p.clean("x") is False
with pytest.raises(ValueError): with pytest.raises(ValueError):
assert p.validate(2.0) is False assert p.clean(2.0) is False
assert p.default() == "2.0" assert p.default() == "2.0"
assert p.validate(p.default()) assert p.clean(p.default())
def test_list_property():
p = ListProperty(StringProperty)
assert p.clean(['abc', 'xyz'])
with pytest.raises(ValueError):
p.clean([])
def test_string_property():
prop = StringProperty()
assert prop.clean('foobar')
assert prop.clean(1)
assert prop.clean([1, 2, 3])
def test_type_property(): def test_type_property():
prop = TypeProperty('my-type') prop = TypeProperty('my-type')
assert prop.validate('my-type') assert prop.clean('my-type')
with pytest.raises(ValueError): with pytest.raises(ValueError):
prop.validate('not-my-type') prop.clean('not-my-type')
assert prop.validate(prop.default()) assert prop.clean(prop.default())
def test_id_property(): def test_id_property():
idprop = IDProperty('my-type') idprop = IDProperty('my-type')
assert idprop.validate('my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') assert idprop.clean('my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c')
with pytest.raises(ValueError) as excinfo:
idprop.clean('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c')
assert str(excinfo.value) == "must start with 'my-type--'."
with pytest.raises(ValueError) as excinfo:
idprop.clean('my-type--foo')
assert str(excinfo.value) == "must have a valid UUID after the prefix."
assert idprop.clean(idprop.default())
@pytest.mark.parametrize("value", [
2,
-1,
3.14,
False,
])
def test_integer_property_valid(value):
int_prop = IntegerProperty()
assert int_prop.clean(value) is not None
@pytest.mark.parametrize("value", [
"something",
StringProperty(),
])
def test_integer_property_invalid(value):
int_prop = IntegerProperty()
with pytest.raises(ValueError): with pytest.raises(ValueError):
idprop.validate('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') int_prop.clean(value)
assert idprop.validate(idprop.default())
def test_boolean_property(): @pytest.mark.parametrize("value", [
True,
False,
'True',
'False',
'true',
'false',
'TRUE',
'FALSE',
'T',
'F',
't',
'f',
1,
0,
])
def test_boolean_property_valid(value):
bool_prop = BooleanProperty() bool_prop = BooleanProperty()
assert bool_prop.validate(True) is not None assert bool_prop.clean(value) is not None
assert bool_prop.validate(False) is not None
for invalid in ('true', 'false', "T", "F", 1, 0):
print(invalid) @pytest.mark.parametrize("value", [
with pytest.raises(ValueError): 'abc',
bool_prop.validate(invalid) ['false'],
{'true': 'true'},
2,
-1,
])
def test_boolean_property_invalid(value):
bool_prop = BooleanProperty()
with pytest.raises(ValueError):
bool_prop.clean(value)
def test_reference_property(): def test_reference_property():
ref_prop = ReferenceProperty() ref_prop = ReferenceProperty()
assert ref_prop.validate("my-type--3a331bfe-0566-55e1-a4a0-9a2cd355a300") assert ref_prop.clean("my-type--3a331bfe-0566-55e1-a4a0-9a2cd355a300")
with pytest.raises(ValueError): with pytest.raises(ValueError):
ref_prop.validate("foo") ref_prop.clean("foo")
@pytest.mark.parametrize("value", [
'2017-01-01T12:34:56Z',
'2017-01-01 12:34:56',
'Jan 1 2017 12:34:56',
])
def test_timestamp_property_valid(value):
ts_prop = TimestampProperty()
assert ts_prop.clean(value) == FAKE_TIME
def test_timestamp_property_invalid():
ts_prop = TimestampProperty()
with pytest.raises(ValueError):
ts_prop.clean(1)
with pytest.raises(ValueError):
ts_prop.clean("someday sometime")

View File

@ -133,3 +133,27 @@ def test_create_relationship_with_positional_args(indicator, malware):
assert rel.source_ref == 'indicator--00000000-0000-0000-0000-000000000001' assert rel.source_ref == 'indicator--00000000-0000-0000-0000-000000000001'
assert rel.target_ref == 'malware--00000000-0000-0000-0000-000000000002' assert rel.target_ref == 'malware--00000000-0000-0000-0000-000000000002'
assert rel.id == 'relationship--00000000-0000-0000-0000-000000000003' assert rel.id == 'relationship--00000000-0000-0000-0000-000000000003'
@pytest.mark.parametrize("data", [
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_parse_relationship(data):
rel = stix2.parse(data)
assert rel.type == 'relationship'
assert rel.id == RELATIONSHIP_ID
assert rel.created == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc)
assert rel.modified == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc)
assert rel.relationship_type == "indicates"
assert rel.source_ref == "indicator--01234567-89ab-cdef-0123-456789abcdef"
assert rel.target_ref == "malware--fedcba98-7654-3210-fedc-ba9876543210"

View File

@ -1,23 +1,27 @@
import stix2 import datetime as dt
import pytest import pytest
from .constants import INDICATOR_KWARGS import pytz
import stix2
from .constants import INDICATOR_KWARGS, REPORT_ID
EXPECTED = """{ EXPECTED = """{
"created": "2015-12-21T19:59:11.000Z", "created": "2015-12-21T19:59:11Z",
"created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
"description": "A simple report with an indicator and campaign", "description": "A simple report with an indicator and campaign",
"id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
"labels": [ "labels": [
"campaign" "campaign"
], ],
"modified": "2015-12-21T19:59:11.000Z", "modified": "2015-12-21T19:59:11Z",
"name": "The Black Vine Cyberespionage Group", "name": "The Black Vine Cyberespionage Group",
"object_refs": [ "object_refs": [
"indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",
"campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c",
"relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a"
], ],
"published": "2016-01-201T17:00:00Z", "published": "2016-01-20T17:00:00Z",
"type": "report" "type": "report"
}""" }"""
@ -26,11 +30,11 @@ def test_report_example():
report = stix2.Report( report = stix2.Report(
id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
created="2015-12-21T19:59:11.000Z", created="2015-12-21T19:59:11Z",
modified="2015-12-21T19:59:11.000Z", modified="2015-12-21T19:59:11Z",
name="The Black Vine Cyberespionage Group", name="The Black Vine Cyberespionage Group",
description="A simple report with an indicator and campaign", description="A simple report with an indicator and campaign",
published="2016-01-201T17:00:00Z", published="2016-01-20T17:00:00Z",
labels=["campaign"], labels=["campaign"],
object_refs=[ object_refs=[
"indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",
@ -50,7 +54,7 @@ def test_report_example_objects_in_object_refs():
modified="2015-12-21T19:59:11.000Z", modified="2015-12-21T19:59:11.000Z",
name="The Black Vine Cyberespionage Group", name="The Black Vine Cyberespionage Group",
description="A simple report with an indicator and campaign", description="A simple report with an indicator and campaign",
published="2016-01-201T17:00:00Z", published="2016-01-20T17:00:00Z",
labels=["campaign"], labels=["campaign"],
object_refs=[ object_refs=[
stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS),
@ -71,7 +75,7 @@ def test_report_example_objects_in_object_refs_with_bad_id():
modified="2015-12-21T19:59:11.000Z", modified="2015-12-21T19:59:11.000Z",
name="The Black Vine Cyberespionage Group", name="The Black Vine Cyberespionage Group",
description="A simple report with an indicator and campaign", description="A simple report with an indicator and campaign",
published="2016-01-201T17:00:00Z", published="2016-01-20T17:00:00Z",
labels=["campaign"], labels=["campaign"],
object_refs=[ object_refs=[
stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS),
@ -85,4 +89,41 @@ def test_report_example_objects_in_object_refs_with_bad_id():
assert excinfo.value.reason == "must match <object-type>--<guid>." assert excinfo.value.reason == "must match <object-type>--<guid>."
assert str(excinfo.value) == "Invalid value for Report 'object_refs': must match <object-type>--<guid>." assert str(excinfo.value) == "Invalid value for Report 'object_refs': must match <object-type>--<guid>."
@pytest.mark.parametrize("data", [
EXPECTED,
{
"created": "2015-12-21T19:59:11Z",
"created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
"description": "A simple report with an indicator and campaign",
"id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
"labels": [
"campaign"
],
"modified": "2015-12-21T19:59:11Z",
"name": "The Black Vine Cyberespionage Group",
"object_refs": [
"indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",
"campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c",
"relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a"
],
"published": "2016-01-20T17:00:00Z",
"type": "report"
},
])
def test_parse_report(data):
rept = stix2.parse(data)
assert rept.type == 'report'
assert rept.id == REPORT_ID
assert rept.created == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc)
assert rept.modified == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc)
assert rept.created_by_ref == "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283"
assert rept.object_refs == ["indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",
"campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c",
"relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a"]
assert rept.description == "A simple report with an indicator and campaign"
assert rept.labels == ["campaign"]
assert rept.name == "The Black Vine Cyberespionage Group"
# TODO: Add other examples # TODO: Add other examples

View File

@ -88,3 +88,27 @@ def test_create_sighting_from_objects_rather_than_ids(malware): # noqa: F811
assert rel.sighting_of_ref == 'malware--00000000-0000-0000-0000-000000000001' assert rel.sighting_of_ref == 'malware--00000000-0000-0000-0000-000000000001'
assert rel.id == 'sighting--00000000-0000-0000-0000-000000000002' assert rel.id == 'sighting--00000000-0000-0000-0000-000000000002'
@pytest.mark.parametrize("data", [
EXPECTED_SIGHTING,
{
"created": "2016-04-06T20:06:37Z",
"id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb",
"modified": "2016-04-06T20:06:37Z",
"sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
"type": "sighting",
"where_sighted_refs": [
"identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"
]
},
])
def test_parse_sighting(data):
sighting = stix2.parse(data)
assert sighting.type == 'sighting'
assert sighting.id == SIGHTING_ID
assert sighting.created == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc)
assert sighting.modified == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc)
assert sighting.sighting_of_ref == "indicator--01234567-89ab-cdef-0123-456789abcdef"
assert sighting.where_sighted_refs == ["identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"]

View File

@ -1,14 +1,20 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import THREAT_ACTOR_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:48.000Z", "created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "The Evil Org threat actor group", "description": "The Evil Org threat actor group",
"id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"labels": [ "labels": [
"crime-syndicate" "crime-syndicate"
], ],
"modified": "2016-04-06T20:03:48.000Z", "modified": "2016-04-06T20:03:48Z",
"name": "Evil Org", "name": "Evil Org",
"type": "threat-actor" "type": "threat-actor"
}""" }"""
@ -18,8 +24,8 @@ def test_threat_actor_example():
threat_actor = stix2.ThreatActor( threat_actor = stix2.ThreatActor(
id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:48.000Z", created="2016-04-06T20:03:48Z",
modified="2016-04-06T20:03:48.000Z", modified="2016-04-06T20:03:48Z",
name="Evil Org", name="Evil Org",
description="The Evil Org threat actor group", description="The Evil Org threat actor group",
labels=["crime-syndicate"], labels=["crime-syndicate"],
@ -27,4 +33,32 @@ def test_threat_actor_example():
assert str(threat_actor) == EXPECTED assert str(threat_actor) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "The Evil Org threat actor group",
"id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"labels": [
"crime-syndicate"
],
"modified": "2016-04-06T20:03:48Z",
"name": "Evil Org",
"type": "threat-actor"
},
])
def test_parse_threat_actor(data):
actor = stix2.parse(data)
assert actor.type == 'threat-actor'
assert actor.id == THREAT_ACTOR_ID
assert actor.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert actor.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert actor.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff"
assert actor.description == "The Evil Org threat actor group"
assert actor.name == "Evil Org"
assert actor.labels == ["crime-syndicate"]
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,13 +1,19 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import TOOL_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:48.000Z", "created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"labels": [ "labels": [
"remote-access" "remote-access"
], ],
"modified": "2016-04-06T20:03:48.000Z", "modified": "2016-04-06T20:03:48Z",
"name": "VNC", "name": "VNC",
"type": "tool" "type": "tool"
}""" }"""
@ -17,12 +23,38 @@ def test_tool_example():
tool = stix2.Tool( tool = stix2.Tool(
id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:48.000Z", created="2016-04-06T20:03:48Z",
modified="2016-04-06T20:03:48.000Z", modified="2016-04-06T20:03:48Z",
name="VNC", name="VNC",
labels=["remote-access"], labels=["remote-access"],
) )
assert str(tool) == EXPECTED assert str(tool) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"created": "2016-04-06T20:03:48Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"labels": [
"remote-access"
],
"modified": "2016-04-06T20:03:48Z",
"name": "VNC",
"type": "tool"
},
])
def test_parse_tool(data):
tool = stix2.parse(data)
assert tool.type == 'tool'
assert tool.id == TOOL_ID
assert tool.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert tool.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc)
assert tool.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff"
assert tool.labels == ["remote-access"]
assert tool.name == "VNC"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,7 +1,13 @@
import datetime as dt
import pytest
import pytz
import stix2 import stix2
from .constants import VULNERABILITY_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-05-12T08:17:27.000Z", "created": "2016-05-12T08:17:27Z",
"external_references": [ "external_references": [
{ {
"external_id": "CVE-2016-1234", "external_id": "CVE-2016-1234",
@ -9,7 +15,7 @@ EXPECTED = """{
} }
], ],
"id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
"modified": "2016-05-12T08:17:27.000Z", "modified": "2016-05-12T08:17:27Z",
"name": "CVE-2016-1234", "name": "CVE-2016-1234",
"type": "vulnerability" "type": "vulnerability"
}""" }"""
@ -18,8 +24,8 @@ EXPECTED = """{
def test_vulnerability_example(): def test_vulnerability_example():
vulnerability = stix2.Vulnerability( vulnerability = stix2.Vulnerability(
id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
created="2016-05-12T08:17:27.000Z", created="2016-05-12T08:17:27Z",
modified="2016-05-12T08:17:27.000Z", modified="2016-05-12T08:17:27Z",
name="CVE-2016-1234", name="CVE-2016-1234",
external_references=[ external_references=[
stix2.ExternalReference(source_name='cve', stix2.ExternalReference(source_name='cve',
@ -29,4 +35,32 @@ def test_vulnerability_example():
assert str(vulnerability) == EXPECTED assert str(vulnerability) == EXPECTED
@pytest.mark.parametrize("data", [
EXPECTED,
{
"created": "2016-05-12T08:17:27Z",
"external_references": [
{
"external_id": "CVE-2016-1234",
"source_name": "cve"
}
],
"id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
"modified": "2016-05-12T08:17:27Z",
"name": "CVE-2016-1234",
"type": "vulnerability"
},
])
def test_parse_vulnerability(data):
vuln = stix2.parse(data)
assert vuln.type == 'vulnerability'
assert vuln.id == VULNERABILITY_ID
assert vuln.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
assert vuln.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
assert vuln.name == "CVE-2016-1234"
assert vuln.external_references[0].external_id == "CVE-2016-1234"
assert vuln.external_references[0].source_name == "cve"
# TODO: Add other examples # TODO: Add other examples

View File

@ -1,6 +1,7 @@
"""Utility functions and classes for the stix2 library.""" """Utility functions and classes for the stix2 library."""
import datetime as dt import datetime as dt
import json
import pytz import pytz
@ -15,12 +16,35 @@ def get_timestamp():
def format_datetime(dttm): def format_datetime(dttm):
# TODO: how to handle naive datetime # 1. Convert to timezone-aware
# 2. Convert to UTC
# 3. Format in ISO format
# 4. Add subsecond value if non-zero
# 5. Add "Z"
# 1. Convert to UTC try:
# 2. Format in ISO format zoned = dttm.astimezone(pytz.utc)
# 3. Strip off "+00:00" except ValueError:
# 4. Add "Z" # dttm is timezone-naive; assume UTC
pytz.utc.localize(dttm)
ts = zoned.strftime("%Y-%m-%dT%H:%M:%S")
if zoned.microsecond > 0:
ms = zoned.strftime("%f")
ts = ts + '.' + ms.rstrip("0")
return ts + "Z"
# TODO: how to handle timestamps with subsecond 0's
return dttm.astimezone(pytz.utc).isoformat()[:-6] + "Z" def get_dict(data):
"""Return data as a dictionary.
Input can be a dictionary, string, or file-like object.
"""
if type(data) is dict:
obj = data
else:
try:
obj = json.loads(data)
except TypeError:
obj = json.load(data)
return obj

View File

@ -8,10 +8,11 @@ commands = pytest
[testenv:pycodestyle] [testenv:pycodestyle]
deps = deps =
flake8 flake8
flake8-import-order
pycodestyle pycodestyle
commands = commands =
pycodestyle ./stix2 pycodestyle ./stix2
flake8 --max-line-length=160 flake8 --max-line-length=160 --import-order-style='google'
[pycodestyle] [pycodestyle]
ignore= ignore=