diff --git a/.gitignore b/.gitignore index 1824e34..3b9971a 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,9 @@ docs/_build/ # PyBuilder target/ -#pycharm stuff +# Vim +*.swp +# +# PyCharm .idea/ diff --git a/setup.py b/setup.py index 22ee0ac..0fbad77 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,10 @@ #!/usr/bin/env python -from setuptools import setup, find_packages +from setuptools import find_packages, setup install_requires = [ 'pytz', + 'six', + 'python-dateutil', ] setup( diff --git a/stix2/__init__.py b/stix2/__init__.py index cef4356..187d18a 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -3,11 +3,49 @@ # flake8: noqa 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, \ IntrusionSet, Malware, ObservedData, Report, ThreatActor, Tool, \ Vulnerability from .sro import Relationship, Sighting -from .markings import MarkingDefinition, GranularMarking, StatementMarking, TLPMarking - +from .utils import get_dict 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 diff --git a/stix2/base.py b/stix2/base.py index 130d8d3..05bf545 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -41,7 +41,7 @@ class _STIXBase(collections.Mapping): if prop_name in kwargs: try: - kwargs[prop_name] = prop.validate(kwargs[prop_name]) + kwargs[prop_name] = prop.clean(kwargs[prop_name]) except ValueError as exc: raise InvalidValueError(self.__class__, prop_name, reason=str(exc)) @@ -56,6 +56,7 @@ class _STIXBase(collections.Mapping): if extra_kwargs: raise ExtraFieldsError(cls, extra_kwargs) + # Detect any missing required fields required_fields = get_required_properties(cls._properties) missing_kwargs = set(required_fields) - set(kwargs) if missing_kwargs: diff --git a/stix2/bundle.py b/stix2/bundle.py index 5785d43..85be3e1 100644 --- a/stix2/bundle.py +++ b/stix2/bundle.py @@ -1,7 +1,7 @@ """STIX 2 Bundle object""" from .base import _STIXBase -from .properties import IDProperty, TypeProperty, Property +from .properties import IDProperty, Property, TypeProperty class Bundle(_STIXBase): diff --git a/stix2/common.py b/stix2/common.py index 6ad7052..29cbf62 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -1,32 +1,17 @@ """STIX 2 Common Data Types and Properties""" -from .base import _STIXBase -from .properties import Property, BooleanProperty, ReferenceProperty, ListProperty +from .other import ExternalReference, GranularMarking +from .properties import (BooleanProperty, ListProperty, ReferenceProperty, + TimestampProperty) from .utils import NOW COMMON_PROPERTIES = { # 'type' and 'id' should be defined on each individual type - 'created': Property(default=lambda: NOW), - 'modified': Property(default=lambda: NOW), - 'external_references': Property(), + 'created': TimestampProperty(default=lambda: NOW), + 'modified': TimestampProperty(default=lambda: NOW), + 'external_references': ListProperty(ExternalReference), 'revoked': BooleanProperty(), 'created_by_ref': ReferenceProperty(type="identity"), - 'object_marking_refs': ListProperty(ReferenceProperty, element_type="marking-definition"), - 'granular_markings': ListProperty(Property) + 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")), + '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), - } diff --git a/stix2/markings.py b/stix2/markings.py deleted file mode 100644 index aea0caa..0000000 --- a/stix2/markings.py +++ /dev/null @@ -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") -) diff --git a/stix2/other.py b/stix2/other.py new file mode 100644 index 0000000..9b7f03a --- /dev/null +++ b/stix2/other.py @@ -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") +) diff --git a/stix2/properties.py b/stix2/properties.py index 88aa126..76fe31b 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,5 +1,13 @@ +import collections +import datetime as dt +import inspect import re import uuid + +from dateutil import parser +import pytz +from six import text_type + 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 if the user provides a value other than the `fixed` value, it will raise 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 - a `default()` function that returns the fixed value. (Default: `None`) @@ -25,15 +33,10 @@ class Property(object): Subclasses can also define the following functions. - `def clean(self, value) -> any:` - - Transform `value` into a valid value for this property. This should - raise a ValueError if such no such transformation is possible. - - `def validate(self, value) -> any:` - - check that `value` is valid for this property. This should return - 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). + - Return a value that is valid for this property. If `value` is not + valid for this property, this will attempt to transform it first. If + `value` is not valid and no such transformation is possible, it should + raise a ValueError. - `def default(self):` - provide a default value for this property. - `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-- likely several microseconds apart-- does not work. - Subclasses can instead provide lambda functions for `clean`, and `default` - as keyword arguments. `validate` should not be provided as a lambda since - lambdas cannot raise their own exceptions. + Subclasses can instead provide a lambda function for `default` as a keyword + argument. `clean` should not be provided as a lambda since lambdas cannot + 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: raise ValueError("must equal '{0}'.".format(self._fixed_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.type = type if fixed: self._fixed_value = fixed - self.validate = self._default_validate + self.clean = self._default_clean self.default = lambda: fixed - if clean: - self.clean = clean if default: self.default = default def clean(self, value): - raise NotImplementedError + return value - def validate(self, value): - try: - value = self.clean(value) - except NotImplementedError: - pass + def __call__(self, value=None): + """Used by ListProperty to handle lists that have been defined with + either a class or an instance. + """ return value 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 - self.element_type = element_type - super(ListProperty, self).__init__(required) - - def validate(self, value): - # TODO: ensure iterable - result = [] - for item in value: - result.append(self.contained(type=self.element_type).validate(item)) - return result + if inspect.isclass(contained) and issubclass(contained, Property): + # If it's a class and not an instance, instantiate it so that + # clean() can be called on it, and ListProperty.clean() will + # use __call__ when it appends the item. + self.contained = contained() + else: + self.contained = contained + super(ListProperty, self).__init__(**kwargs) 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): @@ -106,23 +145,72 @@ class IDProperty(Property): self.required_prefix = type + "--" super(IDProperty, self).__init__() - def validate(self, value): - # TODO: validate GUID as well + def clean(self, value): if not value.startswith(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 def default(self): return self.required_prefix + str(uuid.uuid4()) -class BooleanProperty(Property): - # TODO: Consider coercing some values (like the strings "true" and "false") +class IntegerProperty(Property): - def validate(self, value): - if not isinstance(value, bool): - raise ValueError("must be a boolean value.") - return value + def clean(self, value): + try: + return int(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}" @@ -137,7 +225,7 @@ class ReferenceProperty(Property): self.type = type super(ReferenceProperty, self).__init__(required, type=type) - def validate(self, value): + def clean(self, value): if isinstance(value, _STIXBase): value = value.id if self.type: @@ -156,7 +244,7 @@ class SelectorProperty(Property): # ignore type super(SelectorProperty, self).__init__() - def validate(self, value): + def clean(self, value): if not SELECTOR_REGEX.match(value): raise ValueError("must adhere to selector syntax.") return value diff --git a/stix2/sdo.py b/stix2/sdo.py index 978840f..693b750 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -2,7 +2,10 @@ from .base import _STIXBase 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 @@ -13,9 +16,9 @@ class AttackPattern(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'name': Property(required=True), - 'description': Property(), - 'kill_chain_phases': Property(), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'kill_chain_phases': ListProperty(KillChainPhase), }) @@ -26,12 +29,12 @@ class Campaign(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'name': Property(required=True), - 'description': Property(), - 'aliases': Property(), - 'first_seen': Property(), - 'last_seen': Property(), - 'objective': Property(), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'aliases': ListProperty(StringProperty), + 'first_seen': TimestampProperty(), + 'last_seen': TimestampProperty(), + 'objective': StringProperty(), }) @@ -42,8 +45,8 @@ class CourseOfAction(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'name': Property(required=True), - 'description': Property(), + 'name': StringProperty(required=True), + 'description': StringProperty(), }) @@ -54,11 +57,12 @@ class Identity(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'name': Property(required=True), - 'description': Property(), - 'identity_class': Property(required=True), - 'sectors': Property(), - 'contact_information': Property(), + 'labels': ListProperty(StringProperty), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'identity_class': StringProperty(required=True), + 'sectors': ListProperty(StringProperty), + 'contact_information': StringProperty(), }) @@ -69,13 +73,13 @@ class Indicator(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'labels': Property(required=True), - 'name': Property(), - 'description': Property(), - 'pattern': Property(required=True), - 'valid_from': Property(default=lambda: NOW), - 'valid_until': Property(), - 'kill_chain_phases': Property(), + 'labels': ListProperty(StringProperty, required=True), + 'name': StringProperty(), + 'description': StringProperty(), + 'pattern': StringProperty(required=True), + 'valid_from': TimestampProperty(default=lambda: NOW), + 'valid_until': TimestampProperty(), + 'kill_chain_phases': ListProperty(KillChainPhase), }) @@ -86,15 +90,15 @@ class IntrusionSet(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'name': Property(required=True), - 'description': Property(), - 'aliases': Property(), - 'first_seen': Property(), - 'last_seen ': Property(), - 'goals': Property(), - 'resource_level': Property(), - 'primary_motivation': Property(), - 'secondary_motivations': Property(), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'aliases': ListProperty(StringProperty), + 'first_seen': TimestampProperty(), + 'last_seen ': TimestampProperty(), + 'goals': ListProperty(StringProperty), + 'resource_level': StringProperty(), + 'primary_motivation': StringProperty(), + 'secondary_motivations': ListProperty(StringProperty), }) @@ -105,10 +109,10 @@ class Malware(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'labels': Property(required=True), - 'name': Property(required=True), - 'description': Property(), - 'kill_chain_phases': Property(), + 'labels': ListProperty(StringProperty, required=True), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'kill_chain_phases': ListProperty(KillChainPhase), }) @@ -119,9 +123,9 @@ class ObservedData(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'first_observed': Property(), - 'last_observed': Property(), - 'number_observed': Property(), + 'first_observed': TimestampProperty(required=True), + 'last_observed': TimestampProperty(required=True), + 'number_observed': IntegerProperty(required=True), 'objects': Property(), }) @@ -133,10 +137,10 @@ class Report(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'labels': Property(required=True), - 'name': Property(required=True), - 'description': Property(), - 'published': Property(), + 'labels': ListProperty(StringProperty, required=True), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'published': TimestampProperty(), 'object_refs': ListProperty(ReferenceProperty), }) @@ -148,17 +152,17 @@ class ThreatActor(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'labels': Property(required=True), - 'name': Property(required=True), - 'description': Property(), - 'aliases': Property(), - 'roles': Property(), - 'goals': Property(), - 'sophistication': Property(), - 'resource_level': Property(), - 'primary_motivation': Property(), - 'secondary_motivations': Property(), - 'personal_motivations': Property(), + 'labels': ListProperty(StringProperty, required=True), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'aliases': ListProperty(StringProperty), + 'roles': ListProperty(StringProperty), + 'goals': ListProperty(StringProperty), + 'sophistication': StringProperty(), + 'resource_level': StringProperty(), + 'primary_motivation': StringProperty(), + 'secondary_motivations': ListProperty(StringProperty), + 'personal_motivations': ListProperty(StringProperty), }) @@ -169,11 +173,11 @@ class Tool(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'labels': Property(required=True), - 'name': Property(required=True), - 'description': Property(), - 'kill_chain_phases': Property(), - 'tool_version': Property(), + 'labels': ListProperty(StringProperty, required=True), + 'name': StringProperty(required=True), + 'description': StringProperty(), + 'kill_chain_phases': ListProperty(KillChainPhase), + 'tool_version': StringProperty(), }) @@ -184,6 +188,6 @@ class Vulnerability(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'name': Property(required=True), - 'description': Property(), + 'name': StringProperty(required=True), + 'description': StringProperty(), }) diff --git a/stix2/sro.py b/stix2/sro.py index 4fcae6c..c13fff3 100644 --- a/stix2/sro.py +++ b/stix2/sro.py @@ -2,7 +2,9 @@ from .base import _STIXBase 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): @@ -12,8 +14,8 @@ class Relationship(_STIXBase): _properties.update({ 'id': IDProperty(_type), 'type': TypeProperty(_type), - 'relationship_type': Property(required=True), - 'description': Property(), + 'relationship_type': StringProperty(required=True), + 'description': StringProperty(), 'source_ref': ReferenceProperty(required=True), 'target_ref': ReferenceProperty(required=True), }) @@ -41,13 +43,13 @@ class Sighting(_STIXBase): _properties.update({ 'id': IDProperty(_type), 'type': TypeProperty(_type), - 'first_seen': Property(), - 'last_seen': Property(), - 'count': Property(), + 'first_seen': TimestampProperty(), + 'last_seen': TimestampProperty(), + 'count': IntegerProperty(), 'sighting_of_ref': ReferenceProperty(required=True), - 'observed_data_refs': ListProperty(ReferenceProperty, element_type="observed-data"), - 'where_sighted_refs': ListProperty(ReferenceProperty, element_type="identity"), - 'summary': Property(), + 'observed_data_refs': ListProperty(ReferenceProperty(type="observed-data")), + 'where_sighted_refs': ListProperty(ReferenceProperty(type="identity")), + 'summary': StringProperty(), }) # Explicitly define the first kwargs to make readable Sighting declarations. diff --git a/stix2/test/constants.py b/stix2/test/constants.py index 6d88a84..1c8ae2b 100644 --- a/stix2/test/constants.py +++ b/stix2/test/constants.py @@ -4,11 +4,21 @@ import pytz 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" +INTRUSION_SET_ID = "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29" 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" -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" +VULNERABILITY_ID = "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061" # Minimum required args for an Indicator instance INDICATOR_KWARGS = dict( diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/test_attack_pattern.py index a9d53b6..c0891a5 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/test_attack_pattern.py @@ -1,16 +1,22 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import ATTACK_PATTERN_ID + EXPECTED = """{ - "created": "2016-05-12T08:17:27.000Z", + "created": "2016-05-12T08:17:27Z", "description": "...", "external_references": [ { - "id": "CAPEC-163", + "external_id": "CAPEC-163", "source_name": "capec" } ], "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "modified": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27Z", "name": "Spear Phishing", "type": "attack-pattern" }""" @@ -19,12 +25,12 @@ EXPECTED = """{ def test_attack_pattern_example(): ap = stix2.AttackPattern( id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - created="2016-05-12T08:17:27.000Z", - modified="2016-05-12T08:17:27.000Z", + created="2016-05-12T08:17:27Z", + modified="2016-05-12T08:17:27Z", name="Spear Phishing", external_references=[{ "source_name": "capec", - "id": "CAPEC-163" + "external_id": "CAPEC-163" }], description="...", ) @@ -32,4 +38,33 @@ def test_attack_pattern_example(): 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 diff --git a/stix2/test/test_campaign.py b/stix2/test/test_campaign.py index ce68946..7f6c4e6 100644 --- a/stix2/test/test_campaign.py +++ b/stix2/test/test_campaign.py @@ -1,11 +1,17 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import CAMPAIGN_ID + EXPECTED = """{ - "created": "2016-04-06T20:03:00.000Z", + "created": "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.", "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", "type": "campaign" }""" @@ -15,12 +21,36 @@ def test_campaign_example(): campaign = stix2.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:00.000Z", - modified="2016-04-06T20:03:00.000Z", + created="2016-04-06T20:03:00Z", + modified="2016-04-06T20:03:00Z", name="Green Group Attacks Against Finance", description="Campaign by Green Group against a series of targets in the financial services sector." ) 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 diff --git a/stix2/test/test_course_of_action.py b/stix2/test/test_course_of_action.py index 8c64cde..f566e10 100644 --- a/stix2/test/test_course_of_action.py +++ b/stix2/test/test_course_of_action.py @@ -1,11 +1,17 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import COURSE_OF_ACTION_ID + EXPECTED = """{ - "created": "2016-04-06T20:03:48.000Z", + "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:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", "type": "course-of-action" }""" @@ -15,12 +21,36 @@ def test_course_of_action_example(): coa = stix2.CourseOfAction( id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", 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 ..." ) 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 diff --git a/stix2/test/test_external_reference.py b/stix2/test/test_external_reference.py index 52e21cc..f8d9b66 100644 --- a/stix2/test/test_external_reference.py +++ b/stix2/test/test_external_reference.py @@ -1,7 +1,8 @@ """Tests for stix.ExternalReference""" -import pytest +import re +import pytest import stix2 VERIS = """{ @@ -34,7 +35,7 @@ def test_external_reference_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 = """{ @@ -101,7 +102,7 @@ def test_external_reference_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 assert eval("stix2." + repr(ref)) == ref diff --git a/stix2/test/test_identity.py b/stix2/test/test_identity.py index 632f229..41c87bb 100644 --- a/stix2/test/test_identity.py +++ b/stix2/test/test_identity.py @@ -1,10 +1,16 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import IDENTITY_ID + EXPECTED = """{ - "created": "2015-12-21T19:59:11.000Z", + "created": "2015-12-21T19:59:11Z", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "identity_class": "individual", - "modified": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11Z", "name": "John Smith", "type": "identity" }""" @@ -13,12 +19,33 @@ EXPECTED = """{ def test_identity_example(): report = stix2.Identity( id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", - created="2015-12-21T19:59:11.000Z", - modified="2015-12-21T19:59:11.000Z", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", name="John Smith", identity_class="individual", ) 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 diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py index a941bc9..1c2b610 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -1,8 +1,8 @@ import datetime as dt +import re import pytest import pytz - import stix2 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 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): @@ -116,12 +117,11 @@ def test_indicator_created_ref_invalid_format(): def test_indicator_revoked_invalid(): 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.prop_name == "revoked" 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): @@ -145,3 +145,29 @@ def test_created_modified_time_are_identical_by_default(): ind = stix2.Indicator(**INDICATOR_KWARGS) 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']" diff --git a/stix2/test/test_intrusion_set.py b/stix2/test/test_intrusion_set.py index ba158d7..19fb641 100644 --- a/stix2/test/test_intrusion_set.py +++ b/stix2/test/test_intrusion_set.py @@ -1,10 +1,16 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import INTRUSION_SET_ID + EXPECTED = """{ "aliases": [ "Zookeeper" ], - "created": "2016-04-06T20:03:48.000Z", + "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": [ @@ -13,7 +19,7 @@ EXPECTED = """{ "damage" ], "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", - "modified": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "Bobcat Breakin", "type": "intrusion-set" }""" @@ -23,8 +29,8 @@ def test_intrusion_set_example(): intrusion_set = stix2.IntrusionSet( id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", name="Bobcat Breakin", description="Incidents usually feature a shared TTP of a bobcat being released...", aliases=["Zookeeper"], @@ -33,4 +39,37 @@ def test_intrusion_set_example(): 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 diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index f0431c4..edc35b6 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -1,8 +1,8 @@ import datetime as dt +import re import pytest import pytz - import stix2 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.fields == ['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" diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py index 56ce771..b3cc479 100644 --- a/stix2/test/test_markings.py +++ b/stix2/test/test_markings.py @@ -1,9 +1,14 @@ -import stix2 -from stix2.markings import TLP_WHITE +import datetime as dt + import pytest +import pytz +import stix2 +from stix2.other import TLP_WHITE + +from .constants import MARKING_DEFINITION_ID EXPECTED_TLP_MARKING_DEFINITION = """{ - "created": "2017-01-20T00:00:00.000Z", + "created": "2017-01-20T00:00:00Z", "definition": { "tlp": "white" }, @@ -13,7 +18,7 @@ EXPECTED_TLP_MARKING_DEFINITION = """{ }""" EXPECTED_STATEMENT_MARKING_DEFINITION = """{ - "created": "2017-01-20T00:00:00.000Z", + "created": "2017-01-20T00:00:00Z", "definition": { "statement": "Copyright 2016, Example Corp" }, @@ -33,7 +38,7 @@ EXPECTED_GRANULAR_MARKING = """{ }""" 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", "description": "Campaign by Green Group against a series of targets in the financial services sector.", "granular_markings": [ @@ -45,7 +50,7 @@ EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ } ], "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", "type": "campaign" }""" @@ -103,8 +108,8 @@ def test_campaign_with_granular_markings_example(): campaign = stix2.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:00.000Z", - modified="2016-04-06T20:03:00.000Z", + created="2016-04-06T20:03:00Z", + modified="2016-04-06T20:03:00Z", name="Green Group Attacks Against Finance", description="Campaign by Green Group against a series of targets in the financial services sector.", granular_markings=[ @@ -115,4 +120,27 @@ def test_campaign_with_granular_markings_example(): print(str(campaign)) 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 diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 6fd95a2..52dc15b 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -1,12 +1,18 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import OBSERVED_DATA_ID + EXPECTED = """{ - "created": "2016-04-06T19:58:16.000Z", + "created": "2016-04-06T19:58:16Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "first_observed": "2015-12-21T19:00:00Z", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "last_observed": "2015-12-21T19:00:00Z", - "modified": "2016-04-06T19:58:16.000Z", + "modified": "2016-04-06T19:58:16Z", "number_observed": 50, "objects": { "0": { @@ -21,8 +27,8 @@ def test_observed_data_example(): observed_data = stix2.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T19:58:16.000Z", - modified="2016-04-06T19:58:16.000Z", + created="2016-04-06T19:58:16Z", + modified="2016-04-06T19:58:16Z", first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", number_observed=50, @@ -35,4 +41,35 @@ def test_observed_data_example(): 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 diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index f3c4ff9..e83b2fc 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -1,7 +1,9 @@ import pytest -from stix2.properties import (Property, BooleanProperty, IDProperty, - ReferenceProperty, TypeProperty) +from stix2.properties import (BooleanProperty, IDProperty, IntegerProperty, + ListProperty, Property, ReferenceProperty, + StringProperty, TimestampProperty, TypeProperty) +from .constants import FAKE_TIME def test_property(): @@ -10,10 +12,10 @@ def test_property(): assert p.required is False -def test_basic_validate(): +def test_basic_clean(): class Prop(Property): - def validate(self, value): + def clean(self, value): if value == 42: return value else: @@ -21,9 +23,9 @@ def test_basic_validate(): p = Prop() - assert p.validate(42) == 42 + assert p.clean(42) == 42 with pytest.raises(ValueError): - p.validate(41) + p.clean(41) def test_default_field(): @@ -40,48 +42,132 @@ def test_default_field(): def test_fixed_property(): p = Property(fixed="2.0") - assert p.validate("2.0") + assert p.clean("2.0") with pytest.raises(ValueError): - assert p.validate("x") is False + assert p.clean("x") is False 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.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(): prop = TypeProperty('my-type') - assert prop.validate('my-type') + assert prop.clean('my-type') with pytest.raises(ValueError): - prop.validate('not-my-type') - assert prop.validate(prop.default()) + prop.clean('not-my-type') + assert prop.clean(prop.default()) def test_id_property(): 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): - idprop.validate('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') - assert idprop.validate(idprop.default()) + int_prop.clean(value) -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() - assert bool_prop.validate(True) is not None - assert bool_prop.validate(False) is not None - for invalid in ('true', 'false', "T", "F", 1, 0): - print(invalid) - with pytest.raises(ValueError): - bool_prop.validate(invalid) + assert bool_prop.clean(value) is not None + + +@pytest.mark.parametrize("value", [ + 'abc', + ['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(): 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): - 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") diff --git a/stix2/test/test_relationship.py b/stix2/test/test_relationship.py index f250ec8..72e659c 100644 --- a/stix2/test/test_relationship.py +++ b/stix2/test/test_relationship.py @@ -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.target_ref == 'malware--00000000-0000-0000-0000-000000000002' 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" diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py index 1783255..46a0a16 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -1,23 +1,27 @@ -import stix2 +import datetime as dt + import pytest -from .constants import INDICATOR_KWARGS +import pytz +import stix2 + +from .constants import INDICATOR_KWARGS, REPORT_ID EXPECTED = """{ - "created": "2015-12-21T19:59:11.000Z", + "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:11.000Z", + "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-201T17:00:00Z", + "published": "2016-01-20T17:00:00Z", "type": "report" }""" @@ -26,11 +30,11 @@ def test_report_example(): report = stix2.Report( id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", - created="2015-12-21T19:59:11.000Z", - modified="2015-12-21T19:59:11.000Z", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", name="The Black Vine Cyberespionage Group", description="A simple report with an indicator and campaign", - published="2016-01-201T17:00:00Z", + published="2016-01-20T17:00:00Z", labels=["campaign"], object_refs=[ "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", name="The Black Vine Cyberespionage Group", description="A simple report with an indicator and campaign", - published="2016-01-201T17:00:00Z", + published="2016-01-20T17:00:00Z", labels=["campaign"], object_refs=[ 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", name="The Black Vine Cyberespionage Group", description="A simple report with an indicator and campaign", - published="2016-01-201T17:00:00Z", + published="2016-01-20T17:00:00Z", labels=["campaign"], object_refs=[ 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 --." assert str(excinfo.value) == "Invalid value for Report 'object_refs': must match --." + +@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 diff --git a/stix2/test/test_sighting.py b/stix2/test/test_sighting.py index 11f7449..9da6d4e 100644 --- a/stix2/test/test_sighting.py +++ b/stix2/test/test_sighting.py @@ -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.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"] diff --git a/stix2/test/test_threat_actor.py b/stix2/test/test_threat_actor.py index 8006ac3..5844775 100644 --- a/stix2/test/test_threat_actor.py +++ b/stix2/test/test_threat_actor.py @@ -1,14 +1,20 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import THREAT_ACTOR_ID + EXPECTED = """{ - "created": "2016-04-06T20:03:48.000Z", + "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:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "Evil Org", "type": "threat-actor" }""" @@ -18,8 +24,8 @@ def test_threat_actor_example(): threat_actor = stix2.ThreatActor( id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", name="Evil Org", description="The Evil Org threat actor group", labels=["crime-syndicate"], @@ -27,4 +33,32 @@ def test_threat_actor_example(): 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 diff --git a/stix2/test/test_tool.py b/stix2/test/test_tool.py index 201d333..3193807 100644 --- a/stix2/test/test_tool.py +++ b/stix2/test/test_tool.py @@ -1,13 +1,19 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import TOOL_ID + EXPECTED = """{ - "created": "2016-04-06T20:03:48.000Z", + "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:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "VNC", "type": "tool" }""" @@ -17,12 +23,38 @@ def test_tool_example(): tool = stix2.Tool( id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", name="VNC", labels=["remote-access"], ) 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 diff --git a/stix2/test/test_vulnerability.py b/stix2/test/test_vulnerability.py index 3daebd9..565f077 100644 --- a/stix2/test/test_vulnerability.py +++ b/stix2/test/test_vulnerability.py @@ -1,7 +1,13 @@ +import datetime as dt + +import pytest +import pytz import stix2 +from .constants import VULNERABILITY_ID + EXPECTED = """{ - "created": "2016-05-12T08:17:27.000Z", + "created": "2016-05-12T08:17:27Z", "external_references": [ { "external_id": "CVE-2016-1234", @@ -9,7 +15,7 @@ EXPECTED = """{ } ], "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "modified": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27Z", "name": "CVE-2016-1234", "type": "vulnerability" }""" @@ -18,8 +24,8 @@ EXPECTED = """{ def test_vulnerability_example(): vulnerability = stix2.Vulnerability( id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - created="2016-05-12T08:17:27.000Z", - modified="2016-05-12T08:17:27.000Z", + created="2016-05-12T08:17:27Z", + modified="2016-05-12T08:17:27Z", name="CVE-2016-1234", external_references=[ stix2.ExternalReference(source_name='cve', @@ -29,4 +35,32 @@ def test_vulnerability_example(): 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 diff --git a/stix2/utils.py b/stix2/utils.py index 0acd5de..cdc7f54 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,6 +1,7 @@ """Utility functions and classes for the stix2 library.""" import datetime as dt +import json import pytz @@ -15,12 +16,35 @@ def get_timestamp(): 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 - # 2. Format in ISO format - # 3. Strip off "+00:00" - # 4. Add "Z" + try: + zoned = dttm.astimezone(pytz.utc) + except ValueError: + # 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 diff --git a/tox.ini b/tox.ini index fd580f9..80213d7 100644 --- a/tox.ini +++ b/tox.ini @@ -8,10 +8,11 @@ commands = pytest [testenv:pycodestyle] deps = flake8 + flake8-import-order pycodestyle commands = pycodestyle ./stix2 - flake8 --max-line-length=160 + flake8 --max-line-length=160 --import-order-style='google' [pycodestyle] ignore=