From 5e4ca9e882417f95819e8b1d87657bd0182a6e93 Mon Sep 17 00:00:00 2001 From: clenk Date: Wed, 5 Apr 2017 17:12:44 -0400 Subject: [PATCH 01/21] Add parsing of Malware objects --- .gitignore | 2 ++ stix2/__init__.py | 19 +++++++++++++++++++ stix2/test/test_malware.py | 11 +++++++++++ 3 files changed, 32 insertions(+) diff --git a/.gitignore b/.gitignore index e50675e..e75def5 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ docs/_build/ # PyBuilder target/ +# Vim +*.swp diff --git a/stix2/__init__.py b/stix2/__init__.py index 7762890..0e6dd94 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -2,9 +2,28 @@ # flake8: noqa +import json + from .bundle import Bundle from .common import ExternalReference, KillChainPhase from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \ IntrusionSet, Malware, ObservedData, Report, ThreatActor, Tool, \ Vulnerability from .sro import Relationship + + +def parse(data): + """Deserialize a string or file-like object into a STIX object""" + + try: + obj = json.loads(data) + except TypeError: + obj = json.load(data) + + if 'type' not in obj: + # TODO parse external references, kill chain phases, and granular markings + pass + elif obj['type'] == 'malware': + return sdo.Malware(**obj) + + return obj diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index 0538920..8e628ab 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -88,3 +88,14 @@ def test_invalid_kwarg_to_malware(): with pytest.raises(TypeError) as excinfo: stix2.Malware(my_custom_property="foo", **MALWARE_KWARGS) assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']" + + +def test_parse_malware(): + mal = stix2.parse(EXPECTED_MALWARE) + + assert mal.type == 'malware' + assert mal.id == MALWARE_ID + assert mal.created == "2016-05-12T08:17:27Z" + assert mal.modified == "2016-05-12T08:17:27Z" + assert mal.labels == ['ransomware'] + assert mal.name == "Cryptolocker" From ce0de97df1a4a7ff6777b19063554e2e10ea271b Mon Sep 17 00:00:00 2001 From: clenk Date: Thu, 6 Apr 2017 13:08:48 -0400 Subject: [PATCH 02/21] Validate UUID portion of IDProperty --- stix2/properties.py | 5 ++++- stix2/test/test_properties.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/stix2/properties.py b/stix2/properties.py index 3bb2633..471ffa9 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -102,9 +102,12 @@ class IDProperty(Property): super(IDProperty, self).__init__() def validate(self, value): - # TODO: validate GUID as well if not value.startswith(self.required_prefix): raise ValueError("must start with '{0}'.".format(self.required_prefix)) + try: + uuid.UUID(value.split('--', 1)[1], version=4) + except Exception: + raise ValueError("must have a valid version 4 UUID after the prefix.") return value def default(self): diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index f3c4ff9..6e1d44d 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -63,8 +63,13 @@ def test_id_property(): idprop = IDProperty('my-type') assert idprop.validate('my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') - with pytest.raises(ValueError): + with pytest.raises(ValueError) as excinfo: idprop.validate('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') + assert str(excinfo.value) == "must start with 'my-type--'." + with pytest.raises(ValueError) as excinfo: + idprop.validate('my-type--foo') + assert str(excinfo.value) == "must have a valid version 4 UUID after the prefix." + assert idprop.validate(idprop.default()) From 253989cc52ac89b84725229fa9ef2702aba4669f Mon Sep 17 00:00:00 2001 From: clenk Date: Thu, 6 Apr 2017 16:08:36 -0400 Subject: [PATCH 03/21] Coerce boolean properties automatically for values like "true", "F", or 1 --- stix2/properties.py | 25 ++++++++++++++++++++++--- stix2/test/test_indicator.py | 2 +- stix2/test/test_properties.py | 14 +++++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/stix2/properties.py b/stix2/properties.py index 471ffa9..b638f5f 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -115,12 +115,31 @@ class IDProperty(Property): class BooleanProperty(Property): - # TODO: Consider coercing some values (like the strings "true" and "false") + + 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("not a coercible boolean value.") def validate(self, value): - if not isinstance(value, bool): + try: + return self.clean(value) + except ValueError: raise ValueError("must be a boolean value.") - return value REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py index 11197ed..436bb28 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -101,7 +101,7 @@ def test_indicator_created_ref_invalid_format(): def test_indicator_revoked_invalid(): with pytest.raises(ValueError) as excinfo: - stix2.Indicator(revoked='false', **INDICATOR_KWARGS) + stix2.Indicator(revoked='no', **INDICATOR_KWARGS) assert str(excinfo.value) == "Invalid value for Indicator 'revoked': must be a boolean value." diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index 6e1d44d..a210302 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -78,7 +78,19 @@ def test_boolean_property(): 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): + assert bool_prop.validate('True') is not None + assert bool_prop.validate('False') is not None + assert bool_prop.validate('true') is not None + assert bool_prop.validate('false') is not None + assert bool_prop.validate('TRUE') is not None + assert bool_prop.validate('FALSE') is not None + assert bool_prop.validate('T') is not None + assert bool_prop.validate('F') is not None + assert bool_prop.validate('t') is not None + assert bool_prop.validate('f') is not None + assert bool_prop.validate(1) is not None + assert bool_prop.validate(0) is not None + for invalid in ('abc', ['false'], {'true': 'true'}, 2, -1): print(invalid) with pytest.raises(ValueError): bool_prop.validate(invalid) From 474833248d96fb272b758b72b7192dd1a37d0123 Mon Sep 17 00:00:00 2001 From: clenk Date: Thu, 6 Apr 2017 19:17:32 -0400 Subject: [PATCH 04/21] Flesh out ListProperty, add StringProperty --- stix2/properties.py | 56 +++++++++++++++++++++++++++++++---- stix2/test/test_properties.py | 21 +++++++++++-- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/stix2/properties.py b/stix2/properties.py index b638f5f..0b52544 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,5 +1,6 @@ import re import uuid +import sys class Property(object): @@ -72,21 +73,66 @@ class Property(object): return value -class List(Property): +class ListProperty(Property): def __init__(self, contained): """ contained should be a type whose constructor creates an object from the value """ - self.contained = contained + if contained == StringProperty: + self.contained = StringProperty().string_type + elif contained == BooleanProperty: + self.contained = bool + else: + self.contained = contained def validate(self, value): - # TODO: ensure iterable - for item in value: - self.contained.validate(item) + try: + list_ = self.clean(value) + except ValueError: + raise + + if len(list_) < 1: + raise ValueError("must not be empty.") + + try: + for item in list_: + self.contained.validate(item) + except ValueError: + raise + except AttributeError: + # type of list has no validate() function (eg. built in Python types) + # TODO Should we raise an error here? + pass + + return list_ def clean(self, value): return [self.contained(x) for x in value] + try: + return [self.contained(x) for x in value] + except TypeError: + raise ValueError("must be an iterable over a type whose constructor creates an object from the value.") + + +class StringProperty(Property): + + def __init__(self): + if sys.version_info[0] == 2: + self.string_type = unicode + else: + self.string_type = str + super(StringProperty, self).__init__() + + def clean(self, value): + return self.string_type(value) + + def validate(self, value): + try: + val = self.clean(value) + except ValueError: + raise + return val class TypeProperty(Property): diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index a210302..22a6b44 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -1,7 +1,8 @@ import pytest -from stix2.properties import (Property, BooleanProperty, IDProperty, - ReferenceProperty, TypeProperty) +from stix2.properties import (Property, BooleanProperty, ListProperty, + StringProperty, TypeProperty, IDProperty, + ReferenceProperty) def test_property(): @@ -50,6 +51,22 @@ def test_fixed_property(): assert p.validate(p.default()) +def test_list_property(): + p = ListProperty(StringProperty) + + assert p.validate(['abc', 'xyz']) + with pytest.raises(ValueError): + p.validate([]) + + +def test_string_property(): + prop = StringProperty() + + assert prop.validate('foobar') + assert prop.validate(1) + assert prop.validate([1, 2, 3]) + + def test_type_property(): prop = TypeProperty('my-type') From 93b8076ae3e59e310472345a61b06d375a13e80a Mon Sep 17 00:00:00 2001 From: clenk Date: Fri, 7 Apr 2017 14:53:40 -0400 Subject: [PATCH 05/21] Use StringProperty and ListProperty in Malware objects, fix bugs in those properties --- stix2/properties.py | 8 ++++---- stix2/sdo.py | 9 +++++---- stix2/test/test_malware.py | 8 ++++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/stix2/properties.py b/stix2/properties.py index 0b52544..e62302a 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -75,7 +75,7 @@ class Property(object): class ListProperty(Property): - def __init__(self, contained): + def __init__(self, contained, **kwargs): """ contained should be a type whose constructor creates an object from the value """ @@ -85,6 +85,7 @@ class ListProperty(Property): self.contained = bool else: self.contained = contained + super(ListProperty, self).__init__(**kwargs) def validate(self, value): try: @@ -108,7 +109,6 @@ class ListProperty(Property): return list_ def clean(self, value): - return [self.contained(x) for x in value] try: return [self.contained(x) for x in value] except TypeError: @@ -117,12 +117,12 @@ class ListProperty(Property): class StringProperty(Property): - def __init__(self): + def __init__(self, **kwargs): if sys.version_info[0] == 2: self.string_type = unicode else: self.string_type = str - super(StringProperty, self).__init__() + super(StringProperty, self).__init__(**kwargs) def clean(self, value): return self.string_type(value) diff --git a/stix2/sdo.py b/stix2/sdo.py index ffe073d..8a45ff4 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -2,7 +2,8 @@ from .base import _STIXBase from .common import COMMON_PROPERTIES -from .properties import IDProperty, TypeProperty, Property +from .properties import (StringProperty, IDProperty, ListProperty, + TypeProperty, Property) from .utils import NOW @@ -105,9 +106,9 @@ class Malware(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'labels': Property(required=True), - 'name': Property(required=True), - 'description': Property(), + 'labels': ListProperty(StringProperty, required=True), + 'name': StringProperty(required=True), + 'description': StringProperty(), 'kill_chain_phases': Property(), }) diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index 8e628ab..7668ffa 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -2,6 +2,7 @@ import datetime as dt import pytest import pytz +import re import stix2 @@ -99,3 +100,10 @@ def test_parse_malware(): assert mal.modified == "2016-05-12T08:17:27Z" 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) From bab8d187c9e607268d50c337705ea15b3c5603be Mon Sep 17 00:00:00 2001 From: clenk Date: Fri, 7 Apr 2017 17:34:06 -0400 Subject: [PATCH 06/21] Rework kill chain phases --- stix2/base.py | 1 + stix2/common.py | 7 ++++--- stix2/properties.py | 2 +- stix2/sdo.py | 4 ++-- stix2/test/test_malware.py | 16 ++++++++++++++++ 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index a44d879..878154d 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -58,6 +58,7 @@ class _STIXBase(collections.Mapping): if extra_kwargs: raise TypeError("unexpected keyword arguments: " + str(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/common.py b/stix2/common.py index 7b4e3ea..3a77d0b 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -1,7 +1,8 @@ """STIX 2 Common Data Types and Properties""" from .base import _STIXBase -from .properties import Property, BooleanProperty, ReferenceProperty +from .properties import (Property, BooleanProperty, ReferenceProperty, + StringProperty) from .utils import NOW COMMON_PROPERTIES = { @@ -28,6 +29,6 @@ class ExternalReference(_STIXBase): class KillChainPhase(_STIXBase): _properties = { - 'kill_chain_name': Property(required=True), - 'phase_name': Property(required=True), + 'kill_chain_name': StringProperty(required=True), + 'phase_name': StringProperty(required=True), } diff --git a/stix2/properties.py b/stix2/properties.py index e62302a..5a57ffb 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -110,7 +110,7 @@ class ListProperty(Property): def clean(self, value): try: - return [self.contained(x) for x in value] + return [self.contained(**x) if type(x) is dict else self.contained(x) for x in value] except TypeError: raise ValueError("must be an iterable over a type whose constructor creates an object from the value.") diff --git a/stix2/sdo.py b/stix2/sdo.py index 8a45ff4..d996277 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -1,7 +1,7 @@ """STIX 2.0 Domain Objects""" from .base import _STIXBase -from .common import COMMON_PROPERTIES +from .common import COMMON_PROPERTIES, KillChainPhase from .properties import (StringProperty, IDProperty, ListProperty, TypeProperty, Property) from .utils import NOW @@ -109,7 +109,7 @@ class Malware(_STIXBase): 'labels': ListProperty(StringProperty, required=True), 'name': StringProperty(required=True), 'description': StringProperty(), - 'kill_chain_phases': Property(), + 'kill_chain_phases': ListProperty(KillChainPhase), }) diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index 7668ffa..5206e60 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -107,3 +107,19 @@ def test_parse_malware_invalid_labels(): 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" From dd382520d66ac3142a940108fd70535fad7a54a0 Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 10 Apr 2017 10:18:54 -0400 Subject: [PATCH 07/21] Clean up code/comments --- setup.py | 1 + stix2/properties.py | 12 ++++++--- stix2/test/test_properties.py | 50 ++++++++++++++++++++++------------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/setup.py b/setup.py index 22ee0ac..4c15871 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ from setuptools import setup, find_packages install_requires = [ 'pytz', + 'six', ] setup( diff --git a/stix2/properties.py b/stix2/properties.py index 5a57ffb..7dab517 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,6 +1,6 @@ import re import uuid -import sys +from six import PY2 class Property(object): @@ -93,6 +93,7 @@ class ListProperty(Property): except ValueError: raise + # STIX spec forbids empty lists if len(list_) < 1: raise ValueError("must not be empty.") @@ -109,16 +110,21 @@ class ListProperty(Property): return list_ def clean(self, value): + try: + iter(value) + except TypeError: + raise ValueError("must be an iterable.") + try: return [self.contained(**x) if type(x) is dict else self.contained(x) for x in value] except TypeError: - raise ValueError("must be an iterable over a type whose constructor creates an object from the value.") + raise ValueError("the type of objects in the list must have a constructor that creates an object from the value.") class StringProperty(Property): def __init__(self, **kwargs): - if sys.version_info[0] == 2: + if PY2: self.string_type = unicode else: self.string_type = str diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index 22a6b44..dad250c 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -90,27 +90,39 @@ def test_id_property(): 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() - assert bool_prop.validate(True) is not None - assert bool_prop.validate(False) is not None - assert bool_prop.validate('True') is not None - assert bool_prop.validate('False') is not None - assert bool_prop.validate('true') is not None - assert bool_prop.validate('false') is not None - assert bool_prop.validate('TRUE') is not None - assert bool_prop.validate('FALSE') is not None - assert bool_prop.validate('T') is not None - assert bool_prop.validate('F') is not None - assert bool_prop.validate('t') is not None - assert bool_prop.validate('f') is not None - assert bool_prop.validate(1) is not None - assert bool_prop.validate(0) is not None - for invalid in ('abc', ['false'], {'true': 'true'}, 2, -1): - print(invalid) - with pytest.raises(ValueError): - bool_prop.validate(invalid) + assert bool_prop.validate(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.validate(value) def test_reference_property(): From 168105603bfedb944f1e2860d2e735b3ff9fdc17 Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 10 Apr 2017 10:42:07 -0400 Subject: [PATCH 08/21] Parse dictionaries as well as strings and file-like objects --- stix2/__init__.py | 11 +++++++---- stix2/test/test_malware.py | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/stix2/__init__.py b/stix2/__init__.py index 0e6dd94..149829e 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -15,10 +15,13 @@ from .sro import Relationship def parse(data): """Deserialize a string or file-like object into a STIX object""" - try: - obj = json.loads(data) - except TypeError: - obj = json.load(data) + if type(data) is dict: + obj = data + else: + try: + obj = json.loads(data) + except TypeError: + obj = json.load(data) if 'type' not in obj: # TODO parse external references, kill chain phases, and granular markings diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index 5206e60..c80c28e 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -91,8 +91,19 @@ def test_invalid_kwarg_to_malware(): assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']" -def test_parse_malware(): - mal = stix2.parse(EXPECTED_MALWARE) +@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 From 2e3dfe5d843e0c50832e3d2d2f8fa16e965e0f66 Mon Sep 17 00:00:00 2001 From: clenk Date: Tue, 11 Apr 2017 12:10:55 -0400 Subject: [PATCH 09/21] Add TimestampProperty --- setup.py | 1 + stix2/common.py | 6 +++--- stix2/properties.py | 27 +++++++++++++++++++++++++++ stix2/test/test_attack_pattern.py | 8 ++++---- stix2/test/test_campaign.py | 8 ++++---- stix2/test/test_course_of_action.py | 8 ++++---- stix2/test/test_identity.py | 8 ++++---- stix2/test/test_intrusion_set.py | 8 ++++---- stix2/test/test_malware.py | 4 ++-- stix2/test/test_observed_data.py | 8 ++++---- stix2/test/test_properties.py | 21 ++++++++++++++++++++- stix2/test/test_report.py | 8 ++++---- stix2/test/test_threat_actor.py | 8 ++++---- stix2/test/test_tool.py | 8 ++++---- stix2/test/test_vulnerability.py | 8 ++++---- 15 files changed, 93 insertions(+), 46 deletions(-) diff --git a/setup.py b/setup.py index 4c15871..eff4c2b 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ from setuptools import setup, find_packages install_requires = [ 'pytz', 'six', + 'python-dateutil', ] setup( diff --git a/stix2/common.py b/stix2/common.py index 3a77d0b..2a6479a 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -2,13 +2,13 @@ from .base import _STIXBase from .properties import (Property, BooleanProperty, ReferenceProperty, - StringProperty) + StringProperty, 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), + 'created': TimestampProperty(default=lambda: NOW), + 'modified': TimestampProperty(default=lambda: NOW), 'external_references': Property(), 'revoked': BooleanProperty(), 'created_by_ref': ReferenceProperty(), diff --git a/stix2/properties.py b/stix2/properties.py index 7dab517..fe5e520 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,6 +1,9 @@ import re import uuid from six import PY2 +import datetime as dt +import pytz +from dateutil import parser class Property(object): @@ -194,6 +197,30 @@ class BooleanProperty(Property): raise ValueError("must be a boolean value.") +class TimestampProperty(Property): + + def validate(self, value): + if isinstance(value, dt.datetime): + return value + elif isinstance(value, dt.date): + return dt.datetime.combine(value, dt.time()) + + try: + return parser.parse(value).astimezone(pytz.utc) + except ValueError: + # Doesn't have timezone info in the string + try: + return pytz.utc.localize(parser.parse(value)) + except TypeError: + # Unknown format + raise ValueError("must be a datetime object, date object, or " + "timestamp string in a recognizable format.") + except TypeError: + # Isn't a string + raise ValueError("must be a datetime object, date object, or " + "timestamp string.") + + REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" "-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/test_attack_pattern.py index a9d53b6..36b5b6d 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/test_attack_pattern.py @@ -1,7 +1,7 @@ import stix2 EXPECTED = """{ - "created": "2016-05-12T08:17:27.000Z", + "created": "2016-05-12T08:17:27Z", "description": "...", "external_references": [ { @@ -10,7 +10,7 @@ EXPECTED = """{ } ], "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,8 +19,8 @@ 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", diff --git a/stix2/test/test_campaign.py b/stix2/test/test_campaign.py index ce68946..741f5ba 100644 --- a/stix2/test/test_campaign.py +++ b/stix2/test/test_campaign.py @@ -1,11 +1,11 @@ import stix2 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,8 +15,8 @@ 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." ) diff --git a/stix2/test/test_course_of_action.py b/stix2/test/test_course_of_action.py index 8c64cde..d35680e 100644 --- a/stix2/test/test_course_of_action.py +++ b/stix2/test/test_course_of_action.py @@ -1,11 +1,11 @@ import stix2 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,8 +15,8 @@ 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 ..." ) diff --git a/stix2/test/test_identity.py b/stix2/test/test_identity.py index 632f229..e43614e 100644 --- a/stix2/test/test_identity.py +++ b/stix2/test/test_identity.py @@ -1,10 +1,10 @@ import stix2 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,8 +13,8 @@ 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", ) diff --git a/stix2/test/test_intrusion_set.py b/stix2/test/test_intrusion_set.py index ba158d7..e60532f 100644 --- a/stix2/test/test_intrusion_set.py +++ b/stix2/test/test_intrusion_set.py @@ -4,7 +4,7 @@ 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 +13,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 +23,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"], diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index c80c28e..9537840 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -107,8 +107,8 @@ def test_parse_malware(data): assert mal.type == 'malware' assert mal.id == MALWARE_ID - assert mal.created == "2016-05-12T08:17:27Z" - assert mal.modified == "2016-05-12T08:17:27Z" + 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" diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 6fd95a2..f47e43e 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -1,12 +1,12 @@ import stix2 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 +21,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, diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index dad250c..0bbcae1 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -2,7 +2,8 @@ import pytest from stix2.properties import (Property, BooleanProperty, ListProperty, StringProperty, TypeProperty, IDProperty, - ReferenceProperty) + ReferenceProperty, TimestampProperty) +from .constants import FAKE_TIME def test_property(): @@ -131,3 +132,21 @@ def test_reference_property(): assert ref_prop.validate("my-type--3a331bfe-0566-55e1-a4a0-9a2cd355a300") with pytest.raises(ValueError): ref_prop.validate("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.validate(value) == FAKE_TIME + + +def test_timestamp_property_invalid(): + ts_prop = TimestampProperty() + with pytest.raises(ValueError): + ts_prop.validate(1) + with pytest.raises(ValueError): + ts_prop.validate("someday sometime") diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py index acd4e30..2727dbc 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -1,14 +1,14 @@ import stix2 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", @@ -24,8 +24,8 @@ 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", diff --git a/stix2/test/test_threat_actor.py b/stix2/test/test_threat_actor.py index 8006ac3..c958f80 100644 --- a/stix2/test/test_threat_actor.py +++ b/stix2/test/test_threat_actor.py @@ -1,14 +1,14 @@ import stix2 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 +18,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"], diff --git a/stix2/test/test_tool.py b/stix2/test/test_tool.py index 201d333..322ff11 100644 --- a/stix2/test/test_tool.py +++ b/stix2/test/test_tool.py @@ -1,13 +1,13 @@ import stix2 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,8 +17,8 @@ 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"], ) diff --git a/stix2/test/test_vulnerability.py b/stix2/test/test_vulnerability.py index 3daebd9..ee84533 100644 --- a/stix2/test/test_vulnerability.py +++ b/stix2/test/test_vulnerability.py @@ -1,7 +1,7 @@ import stix2 EXPECTED = """{ - "created": "2016-05-12T08:17:27.000Z", + "created": "2016-05-12T08:17:27Z", "external_references": [ { "external_id": "CVE-2016-1234", @@ -9,7 +9,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 +18,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', From 35981025c5010e0d294318a33a88f4f3452a63c5 Mon Sep 17 00:00:00 2001 From: clenk Date: Fri, 14 Apr 2017 10:42:17 -0400 Subject: [PATCH 10/21] Rework ListProperty, fix merging issues --- stix2/common.py | 2 +- stix2/markings.py | 18 ++++++------- stix2/properties.py | 62 +++++++++++++++++++++------------------------ stix2/sro.py | 4 +-- 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/stix2/common.py b/stix2/common.py index 635aac1..1ec8cca 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -12,7 +12,7 @@ COMMON_PROPERTIES = { 'external_references': Property(), 'revoked': BooleanProperty(), '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) } diff --git a/stix2/markings.py b/stix2/markings.py index aea0caa..aff2ccb 100644 --- a/stix2/markings.py +++ b/stix2/markings.py @@ -5,14 +5,21 @@ from .properties import IDProperty, TypeProperty, ListProperty, ReferencePropert from .utils import NOW +class GranularMarking(_STIXBase): + _properties = { + 'marking_ref': ReferenceProperty(required=True, type="marking-definition"), + 'selectors': ListProperty(SelectorProperty, required=True), + } + + 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"), + 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")), + 'granular_marking': ListProperty(GranularMarking), 'type': TypeProperty(_type), 'id': IDProperty(_type), 'definition_type': Property(), @@ -20,13 +27,6 @@ class MarkingDefinition(_STIXBase): } -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 = { diff --git a/stix2/properties.py b/stix2/properties.py index 1fd1dd7..1ed09a1 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -4,6 +4,7 @@ from six import PY2 import datetime as dt import pytz from dateutil import parser +import inspect from .base import _STIXBase @@ -77,45 +78,31 @@ class Property(object): pass return value + def __call__(self, value=None): + if value is not None: + return value + class ListProperty(Property): - def __init__(self, contained, element_type=None, **kwargs): + 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. """ if contained == StringProperty: self.contained = StringProperty().string_type elif contained == BooleanProperty: self.contained = bool + elif inspect.isclass(contained) and issubclass(contained, Property): + # If it's a class and not an instance, instantiate it so that + # validate() can be called on it, and ListProperty.validate() will + # use __call__ when it appends the item. + self.contained = contained() else: self.contained = contained - self.element_type = element_type super(ListProperty, self).__init__(**kwargs) def validate(self, value): - try: - list_ = self.clean(value) - except ValueError: - raise - - # STIX spec forbids empty lists - if len(list_) < 1: - raise ValueError("must not be empty.") - - try: - for item in list_: - self.contained.validate(item) - except ValueError: - raise - except AttributeError: - # type of list has no validate() function (eg. built in Python types) - # TODO Should we raise an error here? - pass - - return list_ - - def clean(self, value): try: iter(value) except TypeError: @@ -124,14 +111,23 @@ class ListProperty(Property): result = [] for item in value: try: - if type(item) is dict: - result.append(self.contained(**item)) - elif isinstance(item, ReferenceProperty): - result.append(self.contained(type=self.element_type)) - else: - result.append(self.contained(item)) - except TypeError: - raise ValueError("the type of objects in the list must have a constructor that creates an object from the value.") + valid = self.contained.validate(item) + except ValueError: + raise + except AttributeError: + # type of list has no validate() function (eg. built in Python types) + # TODO Should we raise an error here? + valid = item + + if type(valid) is dict: + 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 diff --git a/stix2/sro.py b/stix2/sro.py index 4fcae6c..2537567 100644 --- a/stix2/sro.py +++ b/stix2/sro.py @@ -45,8 +45,8 @@ class Sighting(_STIXBase): 'last_seen': Property(), 'count': Property(), 'sighting_of_ref': ReferenceProperty(required=True), - 'observed_data_refs': ListProperty(ReferenceProperty, element_type="observed-data"), - 'where_sighted_refs': ListProperty(ReferenceProperty, element_type="identity"), + 'observed_data_refs': ListProperty(ReferenceProperty(type="observed-data")), + 'where_sighted_refs': ListProperty(ReferenceProperty(type="identity")), 'summary': Property(), }) From b4f116a33f93991a16e2cdb269a5ee263c1c2e56 Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 17 Apr 2017 10:48:13 -0400 Subject: [PATCH 11/21] Fix TimestampProperty - improved timestamp formatting - python-stix2 will only include subsecond values if they don't equal 0 - in Python 3.6, datetime.astimezone doesn't throw an error on naive timestamps as in previous versions --- stix2/markings.py | 5 +++-- stix2/properties.py | 31 ++++++++++++++++--------------- stix2/test/test_markings.py | 12 ++++++------ stix2/utils.py | 26 +++++++++++++++++--------- 4 files changed, 42 insertions(+), 32 deletions(-) diff --git a/stix2/markings.py b/stix2/markings.py index aff2ccb..7742bbf 100644 --- a/stix2/markings.py +++ b/stix2/markings.py @@ -1,7 +1,8 @@ """STIX 2.0 Marking Objects""" from .base import _STIXBase -from .properties import IDProperty, TypeProperty, ListProperty, ReferenceProperty, Property, SelectorProperty +from .properties import (IDProperty, TypeProperty, ListProperty, TimestampProperty, + ReferenceProperty, Property, SelectorProperty) from .utils import NOW @@ -15,7 +16,7 @@ class GranularMarking(_STIXBase): class MarkingDefinition(_STIXBase): _type = 'marking-definition' _properties = { - 'created': Property(default=lambda: NOW), + 'created': TimestampProperty(default=lambda: NOW), 'external_references': Property(), 'created_by_ref': ReferenceProperty(type="identity"), 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")), diff --git a/stix2/properties.py b/stix2/properties.py index 1ed09a1..e2ad31e 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -206,25 +206,26 @@ class BooleanProperty(Property): class TimestampProperty(Property): def validate(self, value): - if isinstance(value, dt.datetime): - return value - elif isinstance(value, dt.date): - return dt.datetime.combine(value, dt.time()) + 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.timezone('US/Eastern')) + # value isn't a date or datetime object so assume it's a string try: - return parser.parse(value).astimezone(pytz.utc) - except ValueError: - # Doesn't have timezone info in the string - try: - return pytz.utc.localize(parser.parse(value)) - except TypeError: - # Unknown format - raise ValueError("must be a datetime object, date object, or " - "timestamp string in a recognizable format.") + parsed = parser.parse(value) except TypeError: - # Isn't a string + # Unknown format raise ValueError("must be a datetime object, date object, or " - "timestamp string.") + "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 + # TODO Should we default to system local timezone instead? + 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}" diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py index fd9b376..78e42f9 100644 --- a/stix2/test/test_markings.py +++ b/stix2/test/test_markings.py @@ -3,7 +3,7 @@ from stix2.markings import TLP_WHITE import pytest EXPECTED_TLP_MARKING_DEFINITION = """{ - "created": "2017-01-20T00:00:00.000Z", + "created": "2017-01-20T00:00:00Z", "definition": { "tlp": "white" }, @@ -13,7 +13,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 +33,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 +45,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" }""" @@ -100,8 +100,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=[ diff --git a/stix2/utils.py b/stix2/utils.py index 0acd5de..eafaa76 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,7 +1,7 @@ """Utility functions and classes for the stix2 library.""" import datetime as dt - +import time import pytz # Sentinel value for fields that should be set to the current time. @@ -15,12 +15,20 @@ 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" - - # TODO: how to handle timestamps with subsecond 0's - return dttm.astimezone(pytz.utc).isoformat()[:-6] + "Z" + try: + zoned = dttm.astimezone(pytz.utc) + except ValueError: + # dttm is timezone-naive + tz_name = time.tzname[time.localtime().tm_isdst] + zoned = pytz.timezone(tz_name).localize(dttm).astimezone(pytz.utc) + 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" From 40810646fb4d1937cd425296af65d102b570d453 Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 17 Apr 2017 13:16:14 -0400 Subject: [PATCH 12/21] Fix Flake8 error and use UTC as default timezone --- stix2/properties.py | 8 ++------ stix2/utils.py | 6 ++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/stix2/properties.py b/stix2/properties.py index e2ad31e..2ae1bc3 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,6 +1,6 @@ import re import uuid -from six import PY2 +from six import text_type import datetime as dt import pytz from dateutil import parser @@ -134,10 +134,7 @@ class ListProperty(Property): class StringProperty(Property): def __init__(self, **kwargs): - if PY2: - self.string_type = unicode - else: - self.string_type = str + self.string_type = text_type super(StringProperty, self).__init__(**kwargs) def clean(self, value): @@ -224,7 +221,6 @@ class TimestampProperty(Property): return parsed.astimezone(pytz.utc) else: # Doesn't have timezone info in the string; assume UTC - # TODO Should we default to system local timezone instead? return pytz.utc.localize(parsed) diff --git a/stix2/utils.py b/stix2/utils.py index eafaa76..ae85f5f 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,7 +1,6 @@ """Utility functions and classes for the stix2 library.""" import datetime as dt -import time import pytz # Sentinel value for fields that should be set to the current time. @@ -24,9 +23,8 @@ def format_datetime(dttm): try: zoned = dttm.astimezone(pytz.utc) except ValueError: - # dttm is timezone-naive - tz_name = time.tzname[time.localtime().tm_isdst] - zoned = pytz.timezone(tz_name).localize(dttm).astimezone(pytz.utc) + # 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") From cf0b56c04fd0a1cf568470fdfd73a132c855bd89 Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 17 Apr 2017 14:15:01 -0400 Subject: [PATCH 13/21] Use correct property classes for ExternalReference --- stix2/common.py | 8 ++++---- stix2/test/test_external_reference.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/stix2/common.py b/stix2/common.py index 1ec8cca..f8e2981 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -19,10 +19,10 @@ COMMON_PROPERTIES = { class ExternalReference(_STIXBase): _properties = { - 'source_name': Property(required=True), - 'description': Property(), - 'url': Property(), - 'external_id': Property(), + 'source_name': StringProperty(required=True), + 'description': StringProperty(), + 'url': StringProperty(), + 'external_id': StringProperty(), } diff --git a/stix2/test/test_external_reference.py b/stix2/test/test_external_reference.py index 5c39852..3b26675 100644 --- a/stix2/test/test_external_reference.py +++ b/stix2/test/test_external_reference.py @@ -1,6 +1,7 @@ """Tests for stix.ExternalReference""" import pytest +import re import stix2 @@ -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 From 635a3ec3898147867fe1a0c0f2c9cc32a47c08fe Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 17 Apr 2017 15:13:11 -0400 Subject: [PATCH 14/21] Combine clean() and validate() --- stix2/base.py | 2 +- stix2/properties.py | 67 +++++++++++------------------------ stix2/test/test_properties.py | 54 ++++++++++++++-------------- 3 files changed, 48 insertions(+), 75 deletions(-) diff --git a/stix2/base.py b/stix2/base.py index ab7eea5..05c1bed 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -39,7 +39,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: msg = "Invalid value for {0} '{1}': {2}" raise ValueError(msg.format(self.__class__.__name__, diff --git a/stix2/properties.py b/stix2/properties.py index 2ae1bc3..da77278 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -22,7 +22,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`) @@ -30,15 +30,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 @@ -46,36 +41,27 @@ 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. """ - 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 - - def validate(self, value): - try: - value = self.clean(value) - except NotImplementedError: - pass return value def __call__(self, value=None): @@ -95,14 +81,14 @@ class ListProperty(Property): self.contained = bool elif inspect.isclass(contained) and issubclass(contained, Property): # If it's a class and not an instance, instantiate it so that - # validate() can be called on it, and ListProperty.validate() will + # 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 validate(self, value): + def clean(self, value): try: iter(value) except TypeError: @@ -111,11 +97,11 @@ class ListProperty(Property): result = [] for item in value: try: - valid = self.contained.validate(item) + valid = self.contained.clean(item) except ValueError: raise except AttributeError: - # type of list has no validate() function (eg. built in Python types) + # type of list has no clean() function (eg. built in Python types) # TODO Should we raise an error here? valid = item @@ -140,13 +126,6 @@ class StringProperty(Property): def clean(self, value): return self.string_type(value) - def validate(self, value): - try: - val = self.clean(value) - except ValueError: - raise - return val - class TypeProperty(Property): def __init__(self, type): @@ -159,7 +138,7 @@ class IDProperty(Property): self.required_prefix = type + "--" super(IDProperty, self).__init__() - def validate(self, value): + def clean(self, value): if not value.startswith(self.required_prefix): raise ValueError("must start with '{0}'.".format(self.required_prefix)) try: @@ -191,18 +170,12 @@ class BooleanProperty(Property): if value == 0: return False - raise ValueError("not a coercible boolean value.") - - def validate(self, value): - try: - return self.clean(value) - except ValueError: - raise ValueError("must be a boolean value.") + raise ValueError("must be a boolean value.") class TimestampProperty(Property): - def validate(self, value): + def clean(self, value): if isinstance(value, dt.date): if hasattr(value, 'hour'): return value @@ -236,7 +209,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: @@ -255,7 +228,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("values must adhere to selector syntax") return value diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index 0bbcae1..adcecda 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -12,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: @@ -23,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(): @@ -42,53 +42,53 @@ 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.validate(['abc', 'xyz']) + assert p.clean(['abc', 'xyz']) with pytest.raises(ValueError): - p.validate([]) + p.clean([]) def test_string_property(): prop = StringProperty() - assert prop.validate('foobar') - assert prop.validate(1) - assert prop.validate([1, 2, 3]) + 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.validate('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') + 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.validate('my-type--foo') + idprop.clean('my-type--foo') assert str(excinfo.value) == "must have a valid version 4 UUID after the prefix." - assert idprop.validate(idprop.default()) + assert idprop.clean(idprop.default()) @pytest.mark.parametrize("value", [ @@ -110,7 +110,7 @@ def test_id_property(): def test_boolean_property_valid(value): bool_prop = BooleanProperty() - assert bool_prop.validate(value) is not None + assert bool_prop.clean(value) is not None @pytest.mark.parametrize("value", [ @@ -123,15 +123,15 @@ def test_boolean_property_valid(value): def test_boolean_property_invalid(value): bool_prop = BooleanProperty() with pytest.raises(ValueError): - bool_prop.validate(value) + 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", [ @@ -141,12 +141,12 @@ def test_reference_property(): ]) def test_timestamp_property_valid(value): ts_prop = TimestampProperty() - assert ts_prop.validate(value) == FAKE_TIME + assert ts_prop.clean(value) == FAKE_TIME def test_timestamp_property_invalid(): ts_prop = TimestampProperty() with pytest.raises(ValueError): - ts_prop.validate(1) + ts_prop.clean(1) with pytest.raises(ValueError): - ts_prop.validate("someday sometime") + ts_prop.clean("someday sometime") From a14d507f48c7f2e20c138e7e179190bcd10f636f Mon Sep 17 00:00:00 2001 From: clenk Date: Tue, 18 Apr 2017 09:19:38 -0400 Subject: [PATCH 15/21] Add IntegerProperty --- stix2/properties.py | 9 +++++++++ stix2/test/test_properties.py | 24 +++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/stix2/properties.py b/stix2/properties.py index da77278..9312e84 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -151,6 +151,15 @@ class IDProperty(Property): return self.required_prefix + str(uuid.uuid4()) +class IntegerProperty(Property): + + def clean(self, value): + try: + return int(value) + except Exception: + raise ValueError("must be an integer.") + + class BooleanProperty(Property): def clean(self, value): diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index adcecda..7e9dc13 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -2,7 +2,8 @@ import pytest from stix2.properties import (Property, BooleanProperty, ListProperty, StringProperty, TypeProperty, IDProperty, - ReferenceProperty, TimestampProperty) + IntegerProperty, ReferenceProperty, + TimestampProperty) from .constants import FAKE_TIME @@ -91,6 +92,27 @@ def test_id_property(): 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): + int_prop.clean(value) + + @pytest.mark.parametrize("value", [ True, False, From 05ccffc5bd2c350b1db3eb726b02b77d9aa0bb08 Mon Sep 17 00:00:00 2001 From: clenk Date: Tue, 18 Apr 2017 09:21:38 -0400 Subject: [PATCH 16/21] Use correct Property classes for all STIX objects --- stix2/sdo.py | 118 ++++++++++++++++++----------------- stix2/sro.py | 16 ++--- stix2/test/test_indicator.py | 4 +- stix2/test/test_report.py | 8 +-- 4 files changed, 76 insertions(+), 70 deletions(-) diff --git a/stix2/sdo.py b/stix2/sdo.py index f7c9ad6..77db8dc 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -3,7 +3,8 @@ from .base import _STIXBase from .common import COMMON_PROPERTIES, KillChainPhase from .properties import (Property, ListProperty, StringProperty, TypeProperty, - IDProperty, ReferenceProperty) + IDProperty, TimestampProperty, ReferenceProperty, + IntegerProperty) from .utils import NOW @@ -14,9 +15,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), }) @@ -27,12 +28,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(), }) @@ -43,8 +44,8 @@ class CourseOfAction(_STIXBase): _properties.update({ 'type': TypeProperty(_type), 'id': IDProperty(_type), - 'name': Property(required=True), - 'description': Property(), + 'name': StringProperty(required=True), + 'description': StringProperty(), }) @@ -55,11 +56,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(), }) @@ -70,13 +72,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), }) @@ -87,15 +89,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), }) @@ -120,9 +122,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(), }) @@ -134,10 +136,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), }) @@ -149,17 +151,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), }) @@ -170,11 +172,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(), }) @@ -185,6 +187,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 2537567..30feef9 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 (ListProperty, StringProperty, TypeProperty, + IDProperty, TimestampProperty, ReferenceProperty, + IntegerProperty) 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(type="observed-data")), 'where_sighted_refs': ListProperty(ReferenceProperty(type="identity")), - 'summary': Property(), + 'summary': StringProperty(), }) # Explicitly define the first kwargs to make readable Sighting declarations. diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py index d5fd3f4..421f681 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -2,6 +2,7 @@ import datetime as dt import pytest import pytz +import re import stix2 @@ -45,7 +46,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): diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py index 7cfcae9..2346658 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -17,7 +17,7 @@ EXPECTED = """{ "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" }""" @@ -30,7 +30,7 @@ def test_report_example(): 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 +50,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 +71,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), From fabfbe20ec9cd4e1062e7120e059b5dd60638643 Mon Sep 17 00:00:00 2001 From: clenk Date: Wed, 19 Apr 2017 09:22:08 -0400 Subject: [PATCH 17/21] Parse all SDOs and SROs --- stix2/__init__.py | 27 +++++++++++++++++-- stix2/test/constants.py | 12 ++++++++- stix2/test/test_attack_pattern.py | 34 ++++++++++++++++++++++++ stix2/test/test_campaign.py | 29 ++++++++++++++++++++ stix2/test/test_course_of_action.py | 29 ++++++++++++++++++++ stix2/test/test_identity.py | 26 ++++++++++++++++++ stix2/test/test_indicator.py | 26 ++++++++++++++++++ stix2/test/test_intrusion_set.py | 38 ++++++++++++++++++++++++++ stix2/test/test_observed_data.py | 36 +++++++++++++++++++++++++ stix2/test/test_relationship.py | 24 +++++++++++++++++ stix2/test/test_report.py | 41 ++++++++++++++++++++++++++++- stix2/test/test_sighting.py | 24 +++++++++++++++++ stix2/test/test_threat_actor.py | 33 +++++++++++++++++++++++ stix2/test/test_tool.py | 31 ++++++++++++++++++++++ stix2/test/test_vulnerability.py | 33 +++++++++++++++++++++++ 15 files changed, 439 insertions(+), 4 deletions(-) diff --git a/stix2/__init__.py b/stix2/__init__.py index 52a19ef..c82afd4 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -24,10 +24,33 @@ def parse(data): except TypeError: obj = json.load(data) + 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, + } + if 'type' not in obj: # TODO parse external references, kill chain phases, and granular markings pass - elif obj['type'] == 'malware': - return sdo.Malware(**obj) + 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/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 36b5b6d..ec82516 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/test_attack_pattern.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import ATTACK_PATTERN_ID + EXPECTED = """{ "created": "2016-05-12T08:17:27Z", "description": "...", @@ -32,4 +37,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": [ + { + "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].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 741f5ba..e1bec3b 100644 --- a/stix2/test/test_campaign.py +++ b/stix2/test/test_campaign.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import CAMPAIGN_ID + EXPECTED = """{ "created": "2016-04-06T20:03:00Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", @@ -23,4 +28,28 @@ def test_campaign_example(): 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 d35680e..3626539 100644 --- a/stix2/test/test_course_of_action.py +++ b/stix2/test/test_course_of_action.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import COURSE_OF_ACTION_ID + EXPECTED = """{ "created": "2016-04-06T20:03:48Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", @@ -23,4 +28,28 @@ def test_course_of_action_example(): 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_identity.py b/stix2/test/test_identity.py index e43614e..00726c9 100644 --- a/stix2/test/test_identity.py +++ b/stix2/test/test_identity.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import IDENTITY_ID + EXPECTED = """{ "created": "2015-12-21T19:59:11Z", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", @@ -21,4 +26,25 @@ def test_identity_example(): 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 421f681..0ad5bb0 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -124,3 +124,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 e60532f..e6cf1cf 100644 --- a/stix2/test/test_intrusion_set.py +++ b/stix2/test/test_intrusion_set.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import INTRUSION_SET_ID + EXPECTED = """{ "aliases": [ "Zookeeper" @@ -33,4 +38,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_observed_data.py b/stix2/test/test_observed_data.py index f47e43e..df3eaff 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import OBSERVED_DATA_ID + EXPECTED = """{ "created": "2016-04-06T19:58:16Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", @@ -35,4 +40,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: Add other examples diff --git a/stix2/test/test_relationship.py b/stix2/test/test_relationship.py index 0c96bb1..2ce3be5 100644 --- a/stix2/test/test_relationship.py +++ b/stix2/test/test_relationship.py @@ -121,3 +121,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 2346658..a71c0cd 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -1,6 +1,8 @@ import stix2 import pytest -from .constants import INDICATOR_KWARGS +import pytz +import datetime as dt +from .constants import INDICATOR_KWARGS, REPORT_ID EXPECTED = """{ "created": "2015-12-21T19:59:11Z", @@ -82,4 +84,41 @@ def test_report_example_objects_in_object_refs_with_bad_id(): 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 ad30c63..b2b97df 100644 --- a/stix2/test/test_sighting.py +++ b/stix2/test/test_sighting.py @@ -79,3 +79,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 c958f80..7eabbed 100644 --- a/stix2/test/test_threat_actor.py +++ b/stix2/test/test_threat_actor.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import THREAT_ACTOR_ID + EXPECTED = """{ "created": "2016-04-06T20:03:48Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", @@ -27,4 +32,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 322ff11..0e4cd5c 100644 --- a/stix2/test/test_tool.py +++ b/stix2/test/test_tool.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import TOOL_ID + EXPECTED = """{ "created": "2016-04-06T20:03:48Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", @@ -25,4 +30,30 @@ def test_tool_example(): 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 ee84533..bfa9c26 100644 --- a/stix2/test/test_vulnerability.py +++ b/stix2/test/test_vulnerability.py @@ -1,5 +1,10 @@ +import pytest +import pytz +import datetime as dt import stix2 +from .constants import VULNERABILITY_ID + EXPECTED = """{ "created": "2016-05-12T08:17:27Z", "external_references": [ @@ -29,4 +34,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 From d06df8b9da92e6f95d6cf69b177b147ba523d511 Mon Sep 17 00:00:00 2001 From: clenk Date: Wed, 19 Apr 2017 14:32:56 -0400 Subject: [PATCH 18/21] Fix parsing errors - Typos in Attack Pattern tests - Put MarkingDefinition, ExternalReference, and KillChainPhase together in a file for objects that aren't SDOs or SROs - Create utility function to return dictionary from string or file-like object - Put off testing parsing Cyber Observable Objects until a later commit --- stix2/__init__.py | 15 ++---- stix2/common.py | 24 ++------- stix2/{markings.py => other.py} | 83 ++++++++++++++++++++++++------- stix2/properties.py | 3 +- stix2/sdo.py | 3 +- stix2/test/test_attack_pattern.py | 10 ++-- stix2/test/test_markings.py | 29 ++++++++++- stix2/test/test_observed_data.py | 2 +- stix2/utils.py | 17 +++++++ 9 files changed, 128 insertions(+), 58 deletions(-) rename stix2/{markings.py => other.py} (54%) diff --git a/stix2/__init__.py b/stix2/__init__.py index c82afd4..af1a58d 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -2,27 +2,20 @@ # flake8: noqa -import json - 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 def parse(data): """Deserialize a string or file-like object into a STIX object""" - if type(data) is dict: - obj = data - else: - try: - obj = json.loads(data) - except TypeError: - obj = json.load(data) + obj = get_dict(data) obj_map = { 'attack-pattern': AttackPattern, diff --git a/stix2/common.py b/stix2/common.py index f8e2981..a541b09 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -1,33 +1,17 @@ """STIX 2 Common Data Types and Properties""" -from .base import _STIXBase -from .properties import (Property, ListProperty, StringProperty, BooleanProperty, +from .properties import (ListProperty, BooleanProperty, ReferenceProperty, TimestampProperty) +from .other import ExternalReference, GranularMarking from .utils import NOW COMMON_PROPERTIES = { # 'type' and 'id' should be defined on each individual type 'created': TimestampProperty(default=lambda: NOW), 'modified': TimestampProperty(default=lambda: NOW), - 'external_references': Property(), + 'external_references': ListProperty(ExternalReference), 'revoked': BooleanProperty(), 'created_by_ref': ReferenceProperty(type="identity"), 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")), - 'granular_markings': ListProperty(Property) + 'granular_markings': ListProperty(GranularMarking), } - - -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), - } diff --git a/stix2/markings.py b/stix2/other.py similarity index 54% rename from stix2/markings.py rename to stix2/other.py index 7742bbf..d4ad88f 100644 --- a/stix2/markings.py +++ b/stix2/other.py @@ -1,9 +1,26 @@ -"""STIX 2.0 Marking Objects""" +"""STIX 2.0 Objects that are neither SDOs nor SROs""" from .base import _STIXBase from .properties import (IDProperty, TypeProperty, ListProperty, TimestampProperty, - ReferenceProperty, Property, SelectorProperty) -from .utils import NOW + ReferenceProperty, Property, SelectorProperty, + StringProperty) +from .utils import NOW, get_dict + + +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): @@ -13,21 +30,6 @@ class GranularMarking(_STIXBase): } -class MarkingDefinition(_STIXBase): - _type = 'marking-definition' - _properties = { - 'created': TimestampProperty(default=lambda: NOW), - 'external_references': Property(), - 'created_by_ref': ReferenceProperty(type="identity"), - 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")), - 'granular_marking': ListProperty(GranularMarking), - 'type': TypeProperty(_type), - 'id': IDProperty(_type), - 'definition_type': Property(), - 'definition': Property(), - } - - class TLPMarking(_STIXBase): # TODO: don't allow the creation of any other TLPMarkings than the ones below _properties = { @@ -48,6 +50,51 @@ class StatementMarking(_STIXBase): 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", diff --git a/stix2/properties.py b/stix2/properties.py index 9312e84..ef40905 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -5,6 +5,7 @@ import datetime as dt import pytz from dateutil import parser import inspect +import collections from .base import _STIXBase @@ -105,7 +106,7 @@ class ListProperty(Property): # TODO Should we raise an error here? valid = item - if type(valid) is dict: + if isinstance(valid, collections.Mapping): result.append(self.contained(**valid)) else: result.append(self.contained(valid)) diff --git a/stix2/sdo.py b/stix2/sdo.py index 77db8dc..105786b 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -1,7 +1,8 @@ """STIX 2.0 Domain Objects""" from .base import _STIXBase -from .common import COMMON_PROPERTIES, KillChainPhase +from .common import COMMON_PROPERTIES +from .other import KillChainPhase from .properties import (Property, ListProperty, StringProperty, TypeProperty, IDProperty, TimestampProperty, ReferenceProperty, IntegerProperty) diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/test_attack_pattern.py index ec82516..33be679 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/test_attack_pattern.py @@ -10,7 +10,7 @@ EXPECTED = """{ "description": "...", "external_references": [ { - "id": "CAPEC-163", + "external_id": "CAPEC-163", "source_name": "capec" } ], @@ -29,7 +29,7 @@ def test_attack_pattern_example(): name="Spear Phishing", external_references=[{ "source_name": "capec", - "id": "CAPEC-163" + "external_id": "CAPEC-163" }], description="...", ) @@ -47,7 +47,7 @@ def test_attack_pattern_example(): "description": "...", "external_references": [ { - "id": "CAPEC-163", + "external_id": "CAPEC-163", "source_name": "capec" } ], @@ -62,8 +62,8 @@ def test_parse_attack_pattern(data): 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].id == ['CAPEC-163'] - assert ap.external_references[0].source_name == ['capec'] + 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_markings.py b/stix2/test/test_markings.py index 78e42f9..7deddfa 100644 --- a/stix2/test/test_markings.py +++ b/stix2/test/test_markings.py @@ -1,6 +1,10 @@ import stix2 -from stix2.markings import TLP_WHITE +from stix2.other import TLP_WHITE import pytest +import pytz +import datetime as dt + +from .constants import MARKING_DEFINITION_ID EXPECTED_TLP_MARKING_DEFINITION = """{ "created": "2017-01-20T00:00:00Z", @@ -112,4 +116,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 df3eaff..f4243e2 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -69,6 +69,6 @@ def test_parse_observed_data(data): 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" + # assert odata.objects["0"].type == "file" # TODO # TODO: Add other examples diff --git a/stix2/utils.py b/stix2/utils.py index ae85f5f..f91215b 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -2,6 +2,7 @@ import datetime as dt import pytz +import json # Sentinel value for fields that should be set to the current time. # We can't use the standard 'default' approach, since if there are multiple @@ -30,3 +31,19 @@ def format_datetime(dttm): ms = zoned.strftime("%f") ts = ts + '.' + ms.rstrip("0") return ts + "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 From fe4c4d78fc5272cc8becba0641228ece46c54a99 Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 24 Apr 2017 16:33:59 -0400 Subject: [PATCH 19/21] Fix typos, add to Property class documentation, small performance boosts, and let strings and booleans in a ListProperty be handled by __call__(). --- stix2/__init__.py | 39 ++++++++++++++++++++------------------- stix2/other.py | 2 +- stix2/properties.py | 22 +++++++++++++--------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/stix2/__init__.py b/stix2/__init__.py index 1aae2b2..187d18a 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -13,35 +13,36 @@ 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) - 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, - } - if 'type' not in obj: # TODO parse external references, kill chain phases, and granular markings pass else: try: - obj_class = obj_map[obj['type']] + obj_class = OBJ_MAP[obj['type']] return obj_class(**obj) except KeyError: # TODO handle custom objects diff --git a/stix2/other.py b/stix2/other.py index d4ad88f..d8d5f1d 100644 --- a/stix2/other.py +++ b/stix2/other.py @@ -39,7 +39,7 @@ class TLPMarking(_STIXBase): class StatementMarking(_STIXBase): _properties = { - 'statement': Property(required=True) + 'statement': StringProperty(required=True) } def __init__(self, statement=None, **kwargs): diff --git a/stix2/properties.py b/stix2/properties.py index c4cb2b5..07b84a9 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -42,9 +42,15 @@ 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 a lambda function for `default as a keyword + 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_clean(self, value): @@ -66,8 +72,10 @@ class Property(object): return value def __call__(self, value=None): - if value is not None: - return value + """Used by ListProperty to handle lists that have been defined with + either a class or an instance. + """ + return value class ListProperty(Property): @@ -76,11 +84,7 @@ class ListProperty(Property): """ Contained should be a function which returns an object from the value. """ - if contained == StringProperty: - self.contained = StringProperty().string_type - elif contained == BooleanProperty: - self.contained = bool - elif inspect.isclass(contained) and issubclass(contained, Property): + 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. @@ -191,7 +195,7 @@ class TimestampProperty(Property): return value else: # Add a time component - return dt.datetime.combine(value, dt.time(), tzinfo=pytz.timezone('US/Eastern')) + 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: From cd815bfe84ad8827569f9350462b32fc3d77b242 Mon Sep 17 00:00:00 2001 From: clenk Date: Mon, 24 Apr 2017 18:29:56 -0400 Subject: [PATCH 20/21] Fix import order, add flake8-import-order plugin to Tox --- setup.py | 2 +- stix2/bundle.py | 2 +- stix2/common.py | 4 ++-- stix2/other.py | 8 ++++---- stix2/properties.py | 12 +++++++----- stix2/sdo.py | 6 +++--- stix2/sro.py | 6 +++--- stix2/test/test_attack_pattern.py | 3 ++- stix2/test/test_campaign.py | 3 ++- stix2/test/test_course_of_action.py | 3 ++- stix2/test/test_external_reference.py | 2 +- stix2/test/test_identity.py | 3 ++- stix2/test/test_indicator.py | 3 +-- stix2/test/test_intrusion_set.py | 3 ++- stix2/test/test_malware.py | 16 ++++++++++++++-- stix2/test/test_markings.py | 7 ++++--- stix2/test/test_observed_data.py | 3 ++- stix2/test/test_properties.py | 7 +++---- stix2/test/test_report.py | 6 ++++-- stix2/test/test_threat_actor.py | 3 ++- stix2/test/test_tool.py | 3 ++- stix2/test/test_vulnerability.py | 3 ++- stix2/utils.py | 3 ++- tox.ini | 3 ++- 24 files changed, 70 insertions(+), 44 deletions(-) diff --git a/setup.py b/setup.py index eff4c2b..0fbad77 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -from setuptools import setup, find_packages +from setuptools import find_packages, setup install_requires = [ 'pytz', 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 a541b09..29cbf62 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -1,8 +1,8 @@ """STIX 2 Common Data Types and Properties""" -from .properties import (ListProperty, BooleanProperty, - ReferenceProperty, TimestampProperty) from .other import ExternalReference, GranularMarking +from .properties import (BooleanProperty, ListProperty, ReferenceProperty, + TimestampProperty) from .utils import NOW COMMON_PROPERTIES = { diff --git a/stix2/other.py b/stix2/other.py index d8d5f1d..9b7f03a 100644 --- a/stix2/other.py +++ b/stix2/other.py @@ -1,10 +1,10 @@ """STIX 2.0 Objects that are neither SDOs nor SROs""" from .base import _STIXBase -from .properties import (IDProperty, TypeProperty, ListProperty, TimestampProperty, - ReferenceProperty, Property, SelectorProperty, - StringProperty) -from .utils import NOW, get_dict +from .properties import (IDProperty, ListProperty, Property, ReferenceProperty, + SelectorProperty, StringProperty, TimestampProperty, + TypeProperty) +from .utils import get_dict, NOW class ExternalReference(_STIXBase): diff --git a/stix2/properties.py b/stix2/properties.py index 07b84a9..ecdbf18 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,11 +1,13 @@ +import collections +import datetime as dt +import inspect import re import uuid -from six import text_type -import datetime as dt -import pytz + from dateutil import parser -import inspect -import collections +import pytz +from six import text_type + from .base import _STIXBase diff --git a/stix2/sdo.py b/stix2/sdo.py index 105786b..693b750 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -3,9 +3,9 @@ from .base import _STIXBase from .common import COMMON_PROPERTIES from .other import KillChainPhase -from .properties import (Property, ListProperty, StringProperty, TypeProperty, - IDProperty, TimestampProperty, ReferenceProperty, - IntegerProperty) +from .properties import (IDProperty, IntegerProperty, ListProperty, Property, + ReferenceProperty, StringProperty, TimestampProperty, + TypeProperty) from .utils import NOW diff --git a/stix2/sro.py b/stix2/sro.py index 30feef9..c13fff3 100644 --- a/stix2/sro.py +++ b/stix2/sro.py @@ -2,9 +2,9 @@ from .base import _STIXBase from .common import COMMON_PROPERTIES -from .properties import (ListProperty, StringProperty, TypeProperty, - IDProperty, TimestampProperty, ReferenceProperty, - IntegerProperty) +from .properties import (IDProperty, IntegerProperty, ListProperty, + ReferenceProperty, StringProperty, TimestampProperty, + TypeProperty) class Relationship(_STIXBase): diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/test_attack_pattern.py index 33be679..c0891a5 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/test_attack_pattern.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import ATTACK_PATTERN_ID diff --git a/stix2/test/test_campaign.py b/stix2/test/test_campaign.py index e1bec3b..7f6c4e6 100644 --- a/stix2/test/test_campaign.py +++ b/stix2/test/test_campaign.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import CAMPAIGN_ID diff --git a/stix2/test/test_course_of_action.py b/stix2/test/test_course_of_action.py index 3626539..f566e10 100644 --- a/stix2/test/test_course_of_action.py +++ b/stix2/test/test_course_of_action.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import COURSE_OF_ACTION_ID diff --git a/stix2/test/test_external_reference.py b/stix2/test/test_external_reference.py index 5d46f87..f8d9b66 100644 --- a/stix2/test/test_external_reference.py +++ b/stix2/test/test_external_reference.py @@ -1,8 +1,8 @@ """Tests for stix.ExternalReference""" -import pytest import re +import pytest import stix2 VERIS = """{ diff --git a/stix2/test/test_identity.py b/stix2/test/test_identity.py index 00726c9..41c87bb 100644 --- a/stix2/test/test_identity.py +++ b/stix2/test/test_identity.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import IDENTITY_ID diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py index dfa04b2..1c2b610 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -1,9 +1,8 @@ import datetime as dt +import re import pytest import pytz -import re - import stix2 from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS diff --git a/stix2/test/test_intrusion_set.py b/stix2/test/test_intrusion_set.py index e6cf1cf..19fb641 100644 --- a/stix2/test/test_intrusion_set.py +++ b/stix2/test/test_intrusion_set.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import INTRUSION_SET_ID diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index 0006ef2..edc35b6 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -1,9 +1,8 @@ import datetime as dt +import re import pytest import pytz -import re - import stix2 from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS @@ -148,3 +147,16 @@ def test_parse_malware_kill_chain_phases(): 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 c23abcb..b3cc479 100644 --- a/stix2/test/test_markings.py +++ b/stix2/test/test_markings.py @@ -1,8 +1,9 @@ -import stix2 -from stix2.other import TLP_WHITE +import datetime as dt + import pytest import pytz -import datetime as dt +import stix2 +from stix2.other import TLP_WHITE from .constants import MARKING_DEFINITION_ID diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index f4243e2..52dc15b 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import OBSERVED_DATA_ID diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index 7e9dc13..06172ab 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -1,9 +1,8 @@ import pytest -from stix2.properties import (Property, BooleanProperty, ListProperty, - StringProperty, TypeProperty, IDProperty, - IntegerProperty, ReferenceProperty, - TimestampProperty) +from stix2.properties import (BooleanProperty, IDProperty, IntegerProperty, + ListProperty, Property, ReferenceProperty, + StringProperty, TimestampProperty, TypeProperty) from .constants import FAKE_TIME diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py index f28e607..46a0a16 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -1,7 +1,9 @@ -import stix2 +import datetime as dt + import pytest import pytz -import datetime as dt +import stix2 + from .constants import INDICATOR_KWARGS, REPORT_ID EXPECTED = """{ diff --git a/stix2/test/test_threat_actor.py b/stix2/test/test_threat_actor.py index 7eabbed..5844775 100644 --- a/stix2/test/test_threat_actor.py +++ b/stix2/test/test_threat_actor.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import THREAT_ACTOR_ID diff --git a/stix2/test/test_tool.py b/stix2/test/test_tool.py index 0e4cd5c..3193807 100644 --- a/stix2/test/test_tool.py +++ b/stix2/test/test_tool.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import TOOL_ID diff --git a/stix2/test/test_vulnerability.py b/stix2/test/test_vulnerability.py index bfa9c26..565f077 100644 --- a/stix2/test/test_vulnerability.py +++ b/stix2/test/test_vulnerability.py @@ -1,6 +1,7 @@ +import datetime as dt + import pytest import pytz -import datetime as dt import stix2 from .constants import VULNERABILITY_ID diff --git a/stix2/utils.py b/stix2/utils.py index f91215b..cdc7f54 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,9 +1,10 @@ """Utility functions and classes for the stix2 library.""" import datetime as dt -import pytz import json +import pytz + # Sentinel value for fields that should be set to the current time. # We can't use the standard 'default' approach, since if there are multiple # timestamps in a single object, the timestamps will vary by a few microseconds. 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= From c5ba5dad658bc2175a7d17cc1fc513e74c0d0d2b Mon Sep 17 00:00:00 2001 From: clenk Date: Tue, 25 Apr 2017 10:03:37 -0400 Subject: [PATCH 21/21] Modify UUID checking --- stix2/properties.py | 4 ++-- stix2/test/test_properties.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stix2/properties.py b/stix2/properties.py index ecdbf18..76fe31b 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -149,9 +149,9 @@ class IDProperty(Property): if not value.startswith(self.required_prefix): raise ValueError("must start with '{0}'.".format(self.required_prefix)) try: - uuid.UUID(value.split('--', 1)[1], version=4) + uuid.UUID(value.split('--', 1)[1]) except Exception: - raise ValueError("must have a valid version 4 UUID after the prefix.") + raise ValueError("must have a valid UUID after the prefix.") return value def default(self): diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index 06172ab..e83b2fc 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -86,7 +86,7 @@ def test_id_property(): 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 version 4 UUID after the prefix." + assert str(excinfo.value) == "must have a valid UUID after the prefix." assert idprop.clean(idprop.default())