From 1b7abaf2285663762dfe016a8da7412f1b58042b Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Sun, 14 Jul 2019 15:34:31 -0400 Subject: [PATCH] WIP: updating objects to be compliant with stix2.1 WD05. This includes SDO/SRO class updates, but no unit test updates. The class updates broke unit tests, so that still needs to be addressed. --- stix2/exceptions.py | 130 ++++++++++++++++++++++---------------------- stix2/v21/sdo.py | 46 ++++++++++++++-- stix2/v21/sro.py | 1 + 3 files changed, 109 insertions(+), 68 deletions(-) diff --git a/stix2/exceptions.py b/stix2/exceptions.py index f1f1c09..946300c 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -19,37 +19,87 @@ class InvalidValueError(STIXError, ValueError): return msg.format(self) -class MissingPropertiesError(STIXError, ValueError): +class InvalidPropertyConfigurationError(STIXError, ValueError): + """ + Represents an invalid combination of properties on a STIX object. This + class can be used directly when the object requirements are more + complicated and none of the more specific exception subclasses apply. + """ + def __init__(self, message, cls): + super(InvalidPropertyConfigurationError, self).__init__(message) + self.cls = cls + + +class MissingPropertiesError(InvalidPropertyConfigurationError): """Missing one or more required properties when constructing STIX object.""" def __init__(self, cls, properties): - super(MissingPropertiesError, self).__init__() - self.cls = cls - self.properties = sorted(list(properties)) + self.properties = sorted(properties) - def __str__(self): - msg = "No values for required properties for {0}: ({1})." - return msg.format( - self.cls.__name__, + msg = "No values for required properties for {0}: ({1}).".format( + cls.__name__, ", ".join(x for x in self.properties), ) + super(MissingPropertiesError, self).__init__(msg, cls) -class ExtraPropertiesError(STIXError, TypeError): + +class ExtraPropertiesError(InvalidPropertyConfigurationError): """One or more extra properties were provided when constructing STIX object.""" def __init__(self, cls, properties): - super(ExtraPropertiesError, self).__init__() - self.cls = cls - self.properties = sorted(list(properties)) + self.properties = sorted(properties) - def __str__(self): - msg = "Unexpected properties for {0}: ({1})." - return msg.format( - self.cls.__name__, + msg = "Unexpected properties for {0}: ({1}).".format( + cls.__name__, ", ".join(x for x in self.properties), ) + super(ExtraPropertiesError, self).__init__(msg, cls) + + +class MutuallyExclusivePropertiesError(InvalidPropertyConfigurationError): + """Violating interproperty mutually exclusive constraint of a STIX object type.""" + + def __init__(self, cls, properties): + self.properties = sorted(properties) + + msg = "The ({1}) properties for {0} are mutually exclusive.".format( + cls.__name__, + ", ".join(x for x in self.properties), + ) + + super(MutuallyExclusivePropertiesError, self).__init__(msg, cls) + + +class DependentPropertiesError(InvalidPropertyConfigurationError): + """Violating interproperty dependency constraint of a STIX object type.""" + + def __init__(self, cls, dependencies): + self.dependencies = dependencies + + msg = "The property dependencies for {0}: ({1}) are not met.".format( + cls.__name__, + ", ".join(name for x in self.dependencies for name in x), + ) + + super(DependentPropertiesError, self).__init__(msg, cls) + + +class AtLeastOnePropertyError(InvalidPropertyConfigurationError): + """Violating a constraint of a STIX object type that at least one of the given properties must be populated.""" + + def __init__(self, cls, properties): + self.properties = sorted(properties) + + msg = "At least one of the ({1}) properties for {0} must be " \ + "populated.".format( + cls.__name__, + ", ".join(x for x in self.properties), + ) + + super(AtLeastOnePropertyError, self).__init__(msg, cls) + class ImmutableError(STIXError, ValueError): """Attempted to modify an object after creation.""" @@ -103,54 +153,6 @@ class UnmodifiablePropertyError(STIXError, ValueError): return msg.format(", ".join(self.unchangable_properties)) -class MutuallyExclusivePropertiesError(STIXError, TypeError): - """Violating interproperty mutually exclusive constraint of a STIX object type.""" - - def __init__(self, cls, properties): - super(MutuallyExclusivePropertiesError, self).__init__() - self.cls = cls - self.properties = sorted(list(properties)) - - def __str__(self): - msg = "The ({1}) properties for {0} are mutually exclusive." - return msg.format( - self.cls.__name__, - ", ".join(x for x in self.properties), - ) - - -class DependentPropertiesError(STIXError, TypeError): - """Violating interproperty dependency constraint of a STIX object type.""" - - def __init__(self, cls, dependencies): - super(DependentPropertiesError, self).__init__() - self.cls = cls - self.dependencies = dependencies - - def __str__(self): - msg = "The property dependencies for {0}: ({1}) are not met." - return msg.format( - self.cls.__name__, - ", ".join(name for x in self.dependencies for name in x), - ) - - -class AtLeastOnePropertyError(STIXError, TypeError): - """Violating a constraint of a STIX object type that at least one of the given properties must be populated.""" - - def __init__(self, cls, properties): - super(AtLeastOnePropertyError, self).__init__() - self.cls = cls - self.properties = sorted(list(properties)) - - def __str__(self): - msg = "At least one of the ({1}) properties for {0} must be populated." - return msg.format( - self.cls.__name__, - ", ".join(x for x in self.properties), - ) - - class RevokeError(STIXError, ValueError): """Attempted to an operation on a revoked object.""" diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index 8ec4131..66000c1 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -7,6 +7,7 @@ from six.moves.urllib.parse import quote_plus from ..core import STIXDomainObject from ..custom import _custom_object_builder +from ..exceptions import InvalidPropertyConfigurationError from ..properties import ( BinaryProperty, BooleanProperty, EmbeddedObjectProperty, EnumProperty, FloatProperty, IDProperty, IntegerProperty, ListProperty, @@ -33,6 +34,7 @@ class AttackPattern(STIXDomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), ('description', StringProperty()), + ('aliases', ListProperty(StringProperty)), ('kill_chain_phases', ListProperty(KillChainPhase)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), @@ -146,7 +148,7 @@ class Grouping(STIXDomainObject): ('name', StringProperty()), ('description', StringProperty()), ('context', StringProperty(required=True)), - ('object_refs', ListProperty(ReferenceProperty)), + ('object_refs', ListProperty(ReferenceProperty, required=True)), ]) @@ -198,6 +200,8 @@ class Indicator(STIXDomainObject): ('description', StringProperty()), ('indicator_types', ListProperty(StringProperty, required=True)), ('pattern', PatternProperty(required=True)), + ('pattern_type', StringProperty(required=True)), + ('pattern_version', StringProperty()), ('valid_from', TimestampProperty(default=lambda: NOW, required=True)), ('valid_until', TimestampProperty()), ('kill_chain_phases', ListProperty(KillChainPhase)), @@ -245,6 +249,7 @@ class Infrastructure(STIXDomainObject): ('name', StringProperty(required=True)), ('description', StringProperty()), ('infrastructure_types', ListProperty(StringProperty, required=True)), + ('aliases', ListProperty(StringProperty)), ('kill_chain_phases', ListProperty(KillChainPhase)), ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), @@ -318,6 +323,7 @@ class Location(STIXDomainObject): ('created_by_ref', ReferenceProperty(type='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty()), ('description', StringProperty()), ('latitude', FloatProperty(min=-90.0, max=90.0)), ('longitude', FloatProperty(min=-180.0, max=180.0)), @@ -346,6 +352,20 @@ class Location(STIXDomainObject): self._check_properties_dependency(['latitude'], ['longitude']) self._check_properties_dependency(['longitude'], ['latitude']) + if not ( + 'region' in self + or 'country' in self + or ( + 'latitude' in self + and 'longitude' in self + ) + ): + raise InvalidPropertyConfigurationError( + "Location objects must have the properties 'region', " + "'country', or 'latitude' and 'longitude'", + Location + ) + def to_maps_url(self, map_engine="Google Maps"): """Return URL to this location in an online map engine. @@ -411,7 +431,7 @@ class Malware(STIXDomainObject): ('created_by_ref', ReferenceProperty(type='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ('name', StringProperty(required=True)), + ('name', StringProperty()), ('description', StringProperty()), ('malware_types', ListProperty(StringProperty, required=True)), ('is_family', BooleanProperty(required=True)), @@ -443,6 +463,12 @@ class Malware(STIXDomainObject): msg = "{0.id} 'last_seen' must be greater than or equal to 'first_seen'" raise ValueError(msg.format(self)) + if self.is_family and "name" not in self: + raise InvalidPropertyConfigurationError( + "'name' is a required property for malware families", + Malware + ) + class MalwareAnalysis(STIXDomainObject): # TODO: Add link @@ -471,7 +497,7 @@ class MalwareAnalysis(STIXDomainObject): ('operating_system_ref', ReferenceProperty(type='software', spec_version='2.1')), ('installed_software_refs', ListProperty(ReferenceProperty(type='software', spec_version='2.1'))), ('configuration_version', StringProperty()), - ('module', StringProperty()), + ('modules', ListProperty(StringProperty)), ('analysis_engine_version', StringProperty()), ('analysis_definition_version', StringProperty()), ('submitted', TimestampProperty()), @@ -580,7 +606,6 @@ class Opinion(STIXDomainObject): ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('explanation', StringProperty()), ('authors', ListProperty(StringProperty)), - ('object_refs', ListProperty(ReferenceProperty, required=True)), ( 'opinion', EnumProperty( allowed=[ @@ -592,6 +617,7 @@ class Opinion(STIXDomainObject): ], required=True, ), ), + ('object_refs', ListProperty(ReferenceProperty, required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -649,6 +675,8 @@ class ThreatActor(STIXDomainObject): ('description', StringProperty()), ('threat_actor_types', ListProperty(StringProperty, required=True)), ('aliases', ListProperty(StringProperty)), + ('first_seen', TimestampProperty()), + ('last_seen', TimestampProperty()), ('roles', ListProperty(StringProperty)), ('goals', ListProperty(StringProperty)), ('sophistication', StringProperty()), @@ -665,6 +693,16 @@ class ThreatActor(STIXDomainObject): ('granular_markings', ListProperty(GranularMarking)), ]) + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + first_observed = self.get('first_seen') + last_observed = self.get('last_seen') + + if first_observed and last_observed and last_observed < first_observed: + msg = "{0.id} 'last_seen' must be greater than or equal to 'first_seen'" + raise ValueError(msg.format(self)) + class Tool(STIXDomainObject): # TODO: Add link diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py index 144df59..149094e 100644 --- a/stix2/v21/sro.py +++ b/stix2/v21/sro.py @@ -80,6 +80,7 @@ class Sighting(STIXRelationshipObject): ('created_by_ref', ReferenceProperty(type='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('description', StringProperty()), ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), ('count', IntegerProperty(min=0, max=999999999)),