Merge branch 'master' into parsing

stix2.1
clenk 2017-04-19 15:22:36 -04:00
commit 76acd8c0c2
13 changed files with 180 additions and 51 deletions

View File

@ -10,6 +10,7 @@ from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \
Vulnerability
from .sro import Relationship, Sighting
from .utils import get_dict
from . import exceptions
def parse(data):

View File

@ -4,6 +4,8 @@ import collections
import datetime as dt
import json
from .exceptions import ExtraFieldsError, ImmutableError, InvalidValueError, \
MissingFieldsError
from .utils import format_datetime, get_timestamp, NOW
__all__ = ['STIXJSONEncoder', '_STIXBase']
@ -41,14 +43,10 @@ class _STIXBase(collections.Mapping):
try:
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__,
prop_name,
exc))
raise InvalidValueError(self.__class__, prop_name, reason=str(exc))
def __init__(self, **kwargs):
cls = self.__class__
class_name = cls.__name__
# Use the same timestamp for any auto-generated datetimes
self.__now = get_timestamp()
@ -56,15 +54,13 @@ class _STIXBase(collections.Mapping):
# Detect any keyword arguments not allowed for a specific type
extra_kwargs = list(set(kwargs) - set(cls._properties))
if extra_kwargs:
raise TypeError("unexpected keyword arguments: " + str(extra_kwargs))
raise ExtraFieldsError(cls, extra_kwargs)
# Detect any missing required fields
required_fields = get_required_properties(cls._properties)
missing_kwargs = set(required_fields) - set(kwargs)
if missing_kwargs:
msg = "Missing required field(s) for {type}: ({fields})."
field_list = ", ".join(x for x in sorted(list(missing_kwargs)))
raise ValueError(msg.format(type=class_name, fields=field_list))
raise MissingFieldsError(cls, missing_kwargs)
for prop_name, prop_metadata in cls._properties.items():
self._check_property(prop_name, prop_metadata, kwargs)
@ -91,8 +87,7 @@ class _STIXBase(collections.Mapping):
def __setattr__(self, name, value):
if name != '_inner' and not name.startswith("_STIXBase__"):
print(name)
raise ValueError("Cannot modify properties after creation.")
raise ImmutableError
super(_STIXBase, self).__setattr__(name, value)
def __str__(self):

51
stix2/exceptions.py Normal file
View File

@ -0,0 +1,51 @@
class STIXError(Exception):
"""Base class for errors generated in the stix2 library."""
class InvalidValueError(STIXError, ValueError):
"""An invalid value was provided to a STIX object's __init__."""
def __init__(self, cls, prop_name, reason):
super(InvalidValueError, self).__init__()
self.cls = cls
self.prop_name = prop_name
self.reason = reason
def __str__(self):
msg = "Invalid value for {0.cls.__name__} '{0.prop_name}': {0.reason}"
return msg.format(self)
class MissingFieldsError(STIXError, ValueError):
"""Missing required field(s) when constructing STIX object."""
def __init__(self, cls, fields):
super(MissingFieldsError, self).__init__()
self.cls = cls
self.fields = sorted(list(fields))
def __str__(self):
msg = "Missing required field(s) for {0}: ({1})."
return msg.format(self.cls.__name__,
", ".join(x for x in self.fields))
class ExtraFieldsError(STIXError, TypeError):
"""Extra field(s) were provided when constructing STIX object."""
def __init__(self, cls, fields):
super(ExtraFieldsError, self).__init__()
self.cls = cls
self.fields = sorted(list(fields))
def __str__(self):
msg = "Unexpected field(s) for {0}: ({1})."
return msg.format(self.cls.__name__,
", ".join(x for x in self.fields))
class ImmutableError(STIXError, ValueError):
"""Attempted to modify an object after creation"""
def __init__(self):
super(ImmutableError, self).__init__("Cannot modify properties after creation.")

View File

@ -240,5 +240,5 @@ class SelectorProperty(Property):
def clean(self, value):
if not SELECTOR_REGEX.match(value):
raise ValueError("values must adhere to selector syntax")
raise ValueError("must adhere to selector syntax.")
return value

View File

@ -51,23 +51,32 @@ def test_empty_bundle():
def test_bundle_with_wrong_type():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Bundle(type="not-a-bundle")
assert excinfo.value.cls == stix2.Bundle
assert excinfo.value.prop_name == "type"
assert excinfo.value.reason == "must equal 'bundle'."
assert str(excinfo.value) == "Invalid value for Bundle 'type': must equal 'bundle'."
def test_bundle_id_must_start_with_bundle():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Bundle(id='my-prefix--')
assert excinfo.value.cls == stix2.Bundle
assert excinfo.value.prop_name == "id"
assert excinfo.value.reason == "must start with 'bundle--'."
assert str(excinfo.value) == "Invalid value for Bundle 'id': must start with 'bundle--'."
def test_bundle_with_wrong_spec_version():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Bundle(spec_version="1.2")
assert excinfo.value.cls == stix2.Bundle
assert excinfo.value.prop_name == "spec_version"
assert excinfo.value.reason == "must equal '2.0'."
assert str(excinfo.value) == "Invalid value for Bundle 'spec_version': must equal '2.0'."

View File

@ -108,6 +108,9 @@ def test_external_reference_offline():
def test_external_reference_source_required():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.ExternalReference()
assert excinfo.value.cls == stix2.ExternalReference
assert excinfo.value.fields == ["source_name"]
assert str(excinfo.value) == "Missing required field(s) for ExternalReference: (source_name)."

View File

@ -69,54 +69,76 @@ def test_indicator_autogenerated_fields(indicator):
def test_indicator_type_must_be_indicator():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Indicator(type='xxx', **INDICATOR_KWARGS)
assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.prop_name == "type"
assert excinfo.value.reason == "must equal 'indicator'."
assert str(excinfo.value) == "Invalid value for Indicator 'type': must equal 'indicator'."
def test_indicator_id_must_start_with_indicator():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Indicator(id='my-prefix--', **INDICATOR_KWARGS)
assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.prop_name == "id"
assert excinfo.value.reason == "must start with 'indicator--'."
assert str(excinfo.value) == "Invalid value for Indicator 'id': must start with 'indicator--'."
def test_indicator_required_fields():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.Indicator()
assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.fields == ["labels", "pattern"]
assert str(excinfo.value) == "Missing required field(s) for Indicator: (labels, pattern)."
def test_indicator_required_field_pattern():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.Indicator(labels=['malicious-activity'])
assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.fields == ["pattern"]
assert str(excinfo.value) == "Missing required field(s) for Indicator: (pattern)."
def test_indicator_created_ref_invalid_format():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Indicator(created_by_ref='myprefix--12345678', **INDICATOR_KWARGS)
assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.prop_name == "created_by_ref"
assert excinfo.value.reason == "must start with 'identity'."
assert str(excinfo.value) == "Invalid value for Indicator 'created_by_ref': must start with 'identity'."
def test_indicator_revoked_invalid():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Indicator(revoked='no', **INDICATOR_KWARGS)
assert str(excinfo.value) == "Invalid value for Indicator 'revoked': must be a boolean value."
assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.prop_name == "revoked"
assert excinfo.value.reason == "must be a boolean value."
def test_cannot_assign_to_indicator_attributes(indicator):
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.ImmutableError) as excinfo:
indicator.valid_from = dt.datetime.now()
assert str(excinfo.value) == "Cannot modify properties after creation."
def test_invalid_kwarg_to_indicator():
with pytest.raises(TypeError) as excinfo:
with pytest.raises(stix2.exceptions.ExtraFieldsError) as excinfo:
stix2.Indicator(my_custom_property="foo", **INDICATOR_KWARGS)
assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']"
assert excinfo.value.cls == stix2.Indicator
assert excinfo.value.fields == ['my_custom_property']
assert str(excinfo.value) == "Unexpected field(s) for Indicator: (my_custom_property)."
def test_created_modified_time_are_identical_by_default():

View File

@ -36,23 +36,29 @@ def test_kill_chain_example():
def test_kill_chain_required_fields():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.KillChainPhase()
assert excinfo.value.cls == stix2.KillChainPhase
assert excinfo.value.fields == ["kill_chain_name", "phase_name"]
assert str(excinfo.value) == "Missing required field(s) for KillChainPhase: (kill_chain_name, phase_name)."
def test_kill_chain_required_field_chain_name():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.KillChainPhase(phase_name="weaponization")
assert excinfo.value.cls == stix2.KillChainPhase
assert excinfo.value.fields == ["kill_chain_name"]
assert str(excinfo.value) == "Missing required field(s) for KillChainPhase: (kill_chain_name)."
def test_kill_chain_required_field_phase_name():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.KillChainPhase(kill_chain_name="lockheed-martin-cyber-kill-chain")
assert excinfo.value.cls == stix2.KillChainPhase
assert excinfo.value.fields == ["phase_name"]
assert str(excinfo.value) == "Missing required field(s) for KillChainPhase: (phase_name)."

View File

@ -52,42 +52,57 @@ def test_malware_autogenerated_fields(malware):
def test_malware_type_must_be_malware():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Malware(type='xxx', **MALWARE_KWARGS)
assert excinfo.value.cls == stix2.Malware
assert excinfo.value.prop_name == "type"
assert excinfo.value.reason == "must equal 'malware'."
assert str(excinfo.value) == "Invalid value for Malware 'type': must equal 'malware'."
def test_malware_id_must_start_with_malware():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Malware(id='my-prefix--', **MALWARE_KWARGS)
assert excinfo.value.cls == stix2.Malware
assert excinfo.value.prop_name == "id"
assert excinfo.value.reason == "must start with 'malware--'."
assert str(excinfo.value) == "Invalid value for Malware 'id': must start with 'malware--'."
def test_malware_required_fields():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.Malware()
assert excinfo.value.cls == stix2.Malware
assert excinfo.value.fields == ["labels", "name"]
assert str(excinfo.value) == "Missing required field(s) for Malware: (labels, name)."
def test_malware_required_field_name():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.Malware(labels=['ransomware'])
assert excinfo.value.cls == stix2.Malware
assert excinfo.value.fields == ["name"]
assert str(excinfo.value) == "Missing required field(s) for Malware: (name)."
def test_cannot_assign_to_malware_attributes(malware):
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.ImmutableError) as excinfo:
malware.name = "Cryptolocker II"
assert str(excinfo.value) == "Cannot modify properties after creation."
def test_invalid_kwarg_to_malware():
with pytest.raises(TypeError) as excinfo:
with pytest.raises(stix2.exceptions.ExtraFieldsError) as excinfo:
stix2.Malware(my_custom_property="foo", **MALWARE_KWARGS)
assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']"
assert excinfo.value.cls == stix2.Malware
assert excinfo.value.fields == ['my_custom_property']
assert str(excinfo.value) == "Unexpected field(s) for Malware: (my_custom_property)."
@pytest.mark.parametrize("data", [

View File

@ -91,13 +91,16 @@ def test_granular_example():
def test_granular_example_with_bad_selector():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.GranularMarking(
marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
selectors=["abc[0]"] # missing "."
)
assert str(excinfo.value) == "Invalid value for GranularMarking 'selectors': values must adhere to selector syntax"
assert excinfo.value.cls == stix2.GranularMarking
assert excinfo.value.prop_name == "selectors"
assert excinfo.value.reason == "must adhere to selector syntax."
assert str(excinfo.value) == "Invalid value for GranularMarking 'selectors': must adhere to selector syntax."
def test_campaign_with_granular_markings_example():

View File

@ -54,51 +54,63 @@ def test_relationship_autogenerated_fields(relationship):
def test_relationship_type_must_be_relationship():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Relationship(type='xxx', **RELATIONSHIP_KWARGS)
assert excinfo.value.cls == stix2.Relationship
assert excinfo.value.prop_name == "type"
assert excinfo.value.reason == "must equal 'relationship'."
assert str(excinfo.value) == "Invalid value for Relationship 'type': must equal 'relationship'."
def test_relationship_id_must_start_with_relationship():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Relationship(id='my-prefix--', **RELATIONSHIP_KWARGS)
assert excinfo.value.cls == stix2.Relationship
assert excinfo.value.prop_name == "id"
assert excinfo.value.reason == "must start with 'relationship--'."
assert str(excinfo.value) == "Invalid value for Relationship 'id': must start with 'relationship--'."
def test_relationship_required_field_relationship_type():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.Relationship()
assert str(excinfo.value) == "Missing required field(s) for Relationship: (relationship_type, source_ref, target_ref)."
def test_relationship_missing_some_required_fields():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.Relationship(relationship_type='indicates')
assert str(excinfo.value) == "Missing required field(s) for Relationship: (source_ref, target_ref)."
def test_relationship_required_field_target_ref():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.MissingFieldsError) as excinfo:
stix2.Relationship(
relationship_type='indicates',
source_ref=INDICATOR_ID
)
assert excinfo.value.cls == stix2.Relationship
assert excinfo.value.fields == ["target_ref"]
assert str(excinfo.value) == "Missing required field(s) for Relationship: (target_ref)."
def test_cannot_assign_to_relationship_attributes(relationship):
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.ImmutableError) as excinfo:
relationship.relationship_type = "derived-from"
assert str(excinfo.value) == "Cannot modify properties after creation."
def test_invalid_kwarg_to_relationship():
with pytest.raises(TypeError) as excinfo:
with pytest.raises(stix2.exceptions.ExtraFieldsError) as excinfo:
stix2.Relationship(my_custom_property="foo", **RELATIONSHIP_KWARGS)
assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']" in str(excinfo)
assert excinfo.value.cls == stix2.Relationship
assert excinfo.value.fields == ['my_custom_property']
assert str(excinfo.value) == "Unexpected field(s) for Relationship: (my_custom_property)."
def test_create_relationship_from_objects_rather_than_ids(indicator, malware):

View File

@ -65,7 +65,7 @@ def test_report_example_objects_in_object_refs():
def test_report_example_objects_in_object_refs_with_bad_id():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Report(
id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
@ -82,6 +82,9 @@ def test_report_example_objects_in_object_refs_with_bad_id():
],
)
assert excinfo.value.cls == stix2.Report
assert excinfo.value.prop_name == "object_refs"
assert excinfo.value.reason == "must match <object-type>--<guid>."
assert str(excinfo.value) == "Invalid value for Report 'object_refs': must match <object-type>--<guid>."

View File

@ -48,7 +48,7 @@ def test_sighting_all_required_fields():
def test_sighting_bad_where_sighted_refs():
now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc)
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Sighting(
type='sighting',
id=SIGHTING_ID,
@ -58,20 +58,29 @@ def test_sighting_bad_where_sighted_refs():
where_sighted_refs=["malware--8cc7afd6-5455-4d2b-a736-e614ee631d99"]
)
assert excinfo.value.cls == stix2.Sighting
assert excinfo.value.prop_name == "where_sighted_refs"
assert excinfo.value.reason == "must start with 'identity'."
assert str(excinfo.value) == "Invalid value for Sighting 'where_sighted_refs': must start with 'identity'."
def test_sighting_type_must_be_sightings():
with pytest.raises(ValueError) as excinfo:
with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
stix2.Sighting(type='xxx', **SIGHTING_KWARGS)
assert excinfo.value.cls == stix2.Sighting
assert excinfo.value.prop_name == "type"
assert excinfo.value.reason == "must equal 'sighting'."
assert str(excinfo.value) == "Invalid value for Sighting 'type': must equal 'sighting'."
def test_invalid_kwarg_to_sighting():
with pytest.raises(TypeError) as excinfo:
with pytest.raises(stix2.exceptions.ExtraFieldsError) as excinfo:
stix2.Sighting(my_custom_property="foo", **SIGHTING_KWARGS)
assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']" in str(excinfo)
assert excinfo.value.cls == stix2.Sighting
assert excinfo.value.fields == ['my_custom_property']
assert str(excinfo.value) == "Unexpected field(s) for Sighting: (my_custom_property)."
def test_create_sighting_from_objects_rather_than_ids(malware): # noqa: F811