From 555c81d30ff28ab0950884794d8dc8a2e841c617 Mon Sep 17 00:00:00 2001 From: clenk Date: Tue, 9 May 2017 11:03:19 -0400 Subject: [PATCH] Add EmailMessage and EmbeddedObjectProperty (for embedded object types like EmailMIMEComponent) --- stix2/__init__.py | 4 ++- stix2/base.py | 9 +++--- stix2/observables.py | 49 +++++++++++++++++++++++++++----- stix2/properties.py | 24 ++++++++++++++-- stix2/test/test_observed_data.py | 46 ++++++++++++++++++++++++++++++ stix2/test/test_properties.py | 17 ++++++++++- 6 files changed, 132 insertions(+), 17 deletions(-) diff --git a/stix2/__init__.py b/stix2/__init__.py index 4a9ec75..c770dd8 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -3,7 +3,8 @@ # flake8: noqa from .bundle import Bundle -from .observables import Artifact, AutonomousSystem, EmailAddress, File +from .observables import Artifact, AutonomousSystem, EmailAddress, \ + EmailMessage, File from .other import ExternalReference, KillChainPhase, MarkingDefinition, \ GranularMarking, StatementMarking, TLPMarking from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \ @@ -36,6 +37,7 @@ OBJ_MAP_OBSERVABLE = { 'artifact': Artifact, 'autonomous-system': AutonomousSystem, 'email-address': EmailAddress, + 'email-message': EmailMessage, 'file': File, } diff --git a/stix2/base.py b/stix2/base.py index c69d44d..b812dab 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -153,13 +153,12 @@ class Observable(_STIXBase): def _check_property(self, prop_name, prop, kwargs): super(Observable, self)._check_property(prop_name, prop, kwargs) - if prop_name.endswith('_ref'): - ref = kwargs[prop_name].split('--', 1)[0] + if prop_name.endswith('_ref') and prop_name in kwargs: + ref = kwargs[prop_name] if ref not in self._STIXBase__valid_refs: raise InvalidObjRefError(self.__class__, prop_name, "'%s' is not a valid object in local scope" % ref) - if prop_name.endswith('_refs'): - for r in kwargs[prop_name]: - ref = r.split('--', 1)[0] + elif prop_name.endswith('_refs') and prop_name in kwargs: + for ref in kwargs[prop_name]: if ref not in self._STIXBase__valid_refs: raise InvalidObjRefError(self.__class__, prop_name, "'%s' is not a valid object in local scope" % ref) # TODO also check the type of the object referenced, not just that the key exists diff --git a/stix2/observables.py b/stix2/observables.py index 7cad32a..5119af1 100644 --- a/stix2/observables.py +++ b/stix2/observables.py @@ -1,11 +1,16 @@ -"""STIX 2.0 Cyber Observable Objects""" +"""STIX 2.0 Cyber Observable Objects -from .base import Observable -# from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty, -# HashesProperty, HexProperty, IDProperty, -# IntegerProperty, ListProperty, ReferenceProperty, -# StringProperty, TimestampProperty, TypeProperty) -from .properties import BinaryProperty, HashesProperty, IntegerProperty, ObjectReferenceProperty, StringProperty, TypeProperty +Embedded observable object types, such as Email MIME Component, which is +embedded in Email Message objects, inherit from _STIXBase instead of Observable +and do not have a '_type' attribute. +""" + +from .base import _STIXBase, Observable +from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, HashesProperty, + IntegerProperty, ListProperty, + ObjectReferenceProperty, StringProperty, + TimestampProperty, TypeProperty) class Artifact(Observable): @@ -39,6 +44,36 @@ class EmailAddress(Observable): } +class EmailMIMEComponent(_STIXBase): + _properties = { + 'body': StringProperty(), + 'body_raw_ref': ObjectReferenceProperty(), + 'content_type': StringProperty(), + 'content_disposition': StringProperty(), + } + + +class EmailMessage(Observable): + _type = 'email-message' + _properties = { + 'type': TypeProperty(_type), + 'is_multipart': BooleanProperty(required=True), + 'date': TimestampProperty(), + 'content_type': StringProperty(), + 'from_ref': ObjectReferenceProperty(), + 'sender_ref': ObjectReferenceProperty(), + 'to_refs': ListProperty(ObjectReferenceProperty), + 'cc_refs': ListProperty(ObjectReferenceProperty), + 'bcc_refs': ListProperty(ObjectReferenceProperty), + 'subject': StringProperty(), + 'received_lines': ListProperty(StringProperty), + 'additional_header_fields': DictionaryProperty(), + 'body': StringProperty(), + 'body_multipart': ListProperty(EmbeddedObjectProperty(type=EmailMIMEComponent)), + 'raw_email_ref': ObjectReferenceProperty(), + } + + class File(Observable): _type = 'file' _properties = { diff --git a/stix2/properties.py b/stix2/properties.py index 57ebeca..9e4ad9e 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -115,10 +115,15 @@ class ListProperty(Property): # TODO Should we raise an error here? valid = item - if isinstance(valid, collections.Mapping): - result.append(self.contained(**valid)) + if type(self.contained) is EmbeddedObjectProperty: + obj_type = self.contained.type else: - result.append(self.contained(valid)) + obj_type = self.contained + + if isinstance(valid, collections.Mapping): + result.append(obj_type(**valid)) + else: + result.append(obj_type(valid)) # STIX spec forbids empty lists if len(result) < 1: @@ -339,3 +344,16 @@ class SelectorProperty(Property): class ObjectReferenceProperty(StringProperty): pass + + +class EmbeddedObjectProperty(Property): + def __init__(self, type, required=False): + self.type = type + super(EmbeddedObjectProperty, self).__init__(required, type=type) + + def clean(self, value): + if type(value) is dict: + value = self.type(**value) + elif not isinstance(value, self.type): + raise ValueError("must be of type %s." % self.type.__name__) + return value diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 28388c8..3c8f86f 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -152,4 +152,50 @@ def test_parse_email_address(data): stix2.parse(odata_str) +@pytest.mark.parametrize("data", [ + """ + { + "type": "email-message", + "is_multipart": true, + "content_type": "multipart/mixed", + "date": "2016-06-19T14:20:40.000Z", + "from_ref": "1", + "to_refs": [ + "2" + ], + "cc_refs": [ + "3" + ], + "subject": "Check out this picture of a cat!", + "additional_header_fields": { + "Content-Disposition": "inline", + "X-Mailer": "Mutt/1.5.23", + "X-Originating-IP": "198.51.100.3" + }, + "body_multipart": [ + { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!" + }, + { + "content_type": "image/png", + "content_disposition": "attachment; filename=\\"tabby.png\\"", + "body_raw_ref": "4" + }, + { + "content_type": "application/zip", + "content_disposition": "attachment; filename=\\"tabby_pics.zip\\"", + "body_raw_ref": "5" + } + ] + } + """ +]) +def test_parse_email_message(data): + odata = stix2.parse_observable(data, [str(i) for i in range(1, 6)]) + assert odata.type == "email-message" + assert odata.body_multipart[0].content_disposition == "inline" + + # TODO: Add other examples diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index 246f349..7321a5c 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -1,8 +1,10 @@ import pytest from stix2.exceptions import DictionaryKeyError +from stix2.observables import EmailMIMEComponent from stix2.properties import (BinaryProperty, BooleanProperty, - DictionaryProperty, HashesProperty, HexProperty, + DictionaryProperty, EmbeddedObjectProperty, + HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty, Property, ReferenceProperty, StringProperty, TimestampProperty, TypeProperty) @@ -232,3 +234,16 @@ def test_hashes_property_invalid(value): with pytest.raises(ValueError): hash_prop.clean(value) + + +def test_embedded_property(): + emb_prop = EmbeddedObjectProperty(type=EmailMIMEComponent) + mime = EmailMIMEComponent( + content_type="text/plain; charset=utf-8", + content_disposition="inline", + body="Cats are funny!" + ) + assert emb_prop.clean(mime) + + with pytest.raises(ValueError): + emb_prop.clean("string")