diff --git a/.gitignore b/.gitignore index e75def5..3b9971a 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,7 @@ target/ # Vim *.swp +# +# PyCharm +.idea/ + diff --git a/.travis.yml b/.travis.yml index 52cda7e..ff4730e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,7 @@ python: - "3.6" install: - pip install -U pip setuptools - - pip install tox-travis -script: tox + - pip install tox-travis pre-commit +script: + - tox + - if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then pre-commit run --all-files; fi diff --git a/docs/conf.py b/docs/conf.py index 9fba867..e5b9917 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,6 @@ -extensions = [] +extensions = [ + 'sphinx-prompt', +] templates_path = ['_templates'] source_suffix = '.rst' master_doc = 'index' diff --git a/docs/contributing.rst b/docs/contributing.rst index c7c215d..9aa2f0e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -21,14 +21,24 @@ Setting up a development environment We recommend using a `virtualenv `_. 1. Clone the repository. If you're planning to make pull request, you should fork -the repository on GitHub and clone your fork instead of the main repo:: +the repository on GitHub and clone your fork instead of the main repo: - $ git clone https://github.com/yourusername/cti-python-stix2.git +.. prompt:: bash -2. Install develoment-related dependencies:: + git clone https://github.com/yourusername/cti-python-stix2.git - $ cd cti-python-stix2 - $ pip install -r requirements.txt +2. Install develoment-related dependencies: + +.. prompt:: bash + + cd cti-python-stix2 + pip install -r requirements.txt + +3. Install `pre-commit `_ git hooks: + +.. prompt:: bash + + pre-commit install At this point you should be able to make changes to the code. @@ -55,15 +65,19 @@ or implements the features. Any code contributions to python-stix2 should come with new or updated tests. To run the tests in your current Python environment, use the ``pytest`` command -from the root project directory:: +from the root project directory: - $ pytest +.. prompt:: bash + + pytest This should show all of the tests that ran, along with their status. -You can run a specific test file by passing it on the command line:: +You can run a specific test file by passing it on the command line: - $ pytest stix2/test/test_.py +.. prompt:: bash + + pytest stix2/test/test_.py To ensure that the test you wrote is running, you can deliberately add an ``assert False`` statement at the beginning of the test. This is another benefit @@ -73,18 +87,22 @@ run) before making it pass. `tox `_ allows you to test a package across multiple versions of Python. Setting up multiple Python environments is beyond the scope of this guide, but feel free to ask for help setting them up. -Tox should be run from the root directory of the project::: +Tox should be run from the root directory of the project: - $ tox +.. prompt:: bash + + tox We aim for high test coverage, using the `coverage.py `_ library. Though it's not an absolute requirement to maintain 100% coverage, all code contributions must be accompanied by tests. To run coverage and look for untested lines of code, -run:: +run: - $ pytest --cov=stix2 - $ coverage html +.. prompt:: bash + + pytest --cov=stix2 + coverage html then look at the resulting report in ``htmlcov/index.html``. diff --git a/requirements.txt b/requirements.txt index b077b06..605bc54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ pre-commit pytest pytest-cov sphinx +sphinx-prompt tox -e . diff --git a/stix2/__init__.py b/stix2/__init__.py index 149829e..52a19ef 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -9,7 +9,8 @@ from .common import ExternalReference, KillChainPhase from .sdo import AttackPattern, Campaign, CourseOfAction, Identity, Indicator, \ IntrusionSet, Malware, ObservedData, Report, ThreatActor, Tool, \ Vulnerability -from .sro import Relationship +from .sro import Relationship, Sighting +from .markings import MarkingDefinition, GranularMarking, StatementMarking, TLPMarking def parse(data): diff --git a/stix2/base.py b/stix2/base.py index 878154d..ab7eea5 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -71,6 +71,11 @@ class _STIXBase(collections.Mapping): self._inner = kwargs + if self.granular_markings: + for m in self.granular_markings: + # TODO: check selectors + pass + def __getitem__(self, key): return self._inner[key] diff --git a/stix2/common.py b/stix2/common.py index 2a6479a..635aac1 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -1,8 +1,8 @@ """STIX 2 Common Data Types and Properties""" from .base import _STIXBase -from .properties import (Property, BooleanProperty, ReferenceProperty, - StringProperty, TimestampProperty) +from .properties import (Property, ListProperty, StringProperty, BooleanProperty, + ReferenceProperty, TimestampProperty) from .utils import NOW COMMON_PROPERTIES = { @@ -11,10 +11,9 @@ COMMON_PROPERTIES = { 'modified': TimestampProperty(default=lambda: NOW), 'external_references': Property(), 'revoked': BooleanProperty(), - 'created_by_ref': ReferenceProperty(), - # TODO: - # - object_marking_refs - # - granular_markings + 'created_by_ref': ReferenceProperty(type="identity"), + 'object_marking_refs': ListProperty(ReferenceProperty, element_type="marking-definition"), + 'granular_markings': ListProperty(Property) } diff --git a/stix2/markings.py b/stix2/markings.py new file mode 100644 index 0000000..aea0caa --- /dev/null +++ b/stix2/markings.py @@ -0,0 +1,76 @@ +"""STIX 2.0 Marking Objects""" + +from .base import _STIXBase +from .properties import IDProperty, TypeProperty, ListProperty, ReferenceProperty, Property, SelectorProperty +from .utils import NOW + + +class MarkingDefinition(_STIXBase): + _type = 'marking-definition' + _properties = { + 'created': Property(default=lambda: NOW), + 'external_references': Property(), + 'created_by_ref': ReferenceProperty(type="identity"), + 'object_marking_refs': ListProperty(ReferenceProperty, element_type="marking-definition"), + 'granular_marking': ListProperty(Property, element_type="granular-marking"), + 'type': TypeProperty(_type), + 'id': IDProperty(_type), + 'definition_type': Property(), + 'definition': Property(), + } + + +class GranularMarking(_STIXBase): + _properties = { + 'marking_ref': ReferenceProperty(required=True, type="marking-definition"), + 'selectors': ListProperty(SelectorProperty, required=True), + } + + +class TLPMarking(_STIXBase): + # TODO: don't allow the creation of any other TLPMarkings than the ones below + _properties = { + 'tlp': Property(required=True) + } + + +class StatementMarking(_STIXBase): + _properties = { + 'statement': Property(required=True) + } + + def __init__(self, statement=None, **kwargs): + # Allow statement as positional args. + if statement and not kwargs.get('statement'): + kwargs['statement'] = statement + + super(StatementMarking, self).__init__(**kwargs) + + +TLP_WHITE = MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="white") +) + +TLP_GREEN = MarkingDefinition( + id="marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="green") +) + +TLP_AMBER = MarkingDefinition( + id="marking-definition--f88d31f6-486f-44da-b317-01333bde0b82", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="amber") +) + +TLP_RED = MarkingDefinition( + id="marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed", + created="2017-01-20T00:00:00.000Z", + definition_type="tlp", + definition=TLPMarking(tlp="red") +) diff --git a/stix2/properties.py b/stix2/properties.py index fe5e520..1fd1dd7 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 +from .base import _STIXBase class Property(object): @@ -54,8 +55,9 @@ class Property(object): raise ValueError("must equal '{0}'.".format(self._fixed_value)) return value - def __init__(self, required=False, fixed=None, clean=None, default=None): + def __init__(self, required=False, fixed=None, clean=None, default=None, type=None): self.required = required + self.type = type if fixed: self._fixed_value = fixed self.validate = self._default_validate @@ -78,7 +80,7 @@ class Property(object): class ListProperty(Property): - def __init__(self, contained, **kwargs): + def __init__(self, contained, element_type=None, **kwargs): """ contained should be a type whose constructor creates an object from the value """ @@ -88,6 +90,7 @@ class ListProperty(Property): self.contained = bool else: self.contained = contained + self.element_type = element_type super(ListProperty, self).__init__(**kwargs) def validate(self, value): @@ -118,10 +121,18 @@ class ListProperty(Property): 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("the type of objects in the list must have a constructor that creates an object from the value.") + 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.") + return result class StringProperty(Property): @@ -145,7 +156,6 @@ class StringProperty(Property): class TypeProperty(Property): - def __init__(self, type): super(TypeProperty, self).__init__(fixed=type) @@ -226,9 +236,33 @@ REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" class ReferenceProperty(Property): - # TODO: support references that must be to a specific object type + def __init__(self, required=False, type=None): + """ + references sometimes must be to a specific object type + """ + self.type = type + super(ReferenceProperty, self).__init__(required, type=type) def validate(self, value): + if isinstance(value, _STIXBase): + value = value.id + if self.type: + if not value.startswith(self.type): + raise ValueError("must start with '{0}'.".format(self.type)) if not REF_REGEX.match(value): raise ValueError("must match --.") return value + + +SELECTOR_REGEX = re.compile("^[a-z0-9_-]{3,250}(\\.(\\[\\d+\\]|[a-z0-9_-]{1,250}))*$") + + +class SelectorProperty(Property): + def __init__(self, type=None): + # ignore type + super(SelectorProperty, self).__init__() + + def validate(self, value): + if not SELECTOR_REGEX.match(value): + raise ValueError("values must adhere to selector syntax") + return value diff --git a/stix2/sdo.py b/stix2/sdo.py index d996277..f7c9ad6 100644 --- a/stix2/sdo.py +++ b/stix2/sdo.py @@ -2,8 +2,8 @@ from .base import _STIXBase from .common import COMMON_PROPERTIES, KillChainPhase -from .properties import (StringProperty, IDProperty, ListProperty, - TypeProperty, Property) +from .properties import (Property, ListProperty, StringProperty, TypeProperty, + IDProperty, ReferenceProperty) from .utils import NOW @@ -138,7 +138,7 @@ class Report(_STIXBase): 'name': Property(required=True), 'description': Property(), 'published': Property(), - 'object_refs': Property(), + 'object_refs': ListProperty(ReferenceProperty), }) diff --git a/stix2/sro.py b/stix2/sro.py index 19751fb..4fcae6c 100644 --- a/stix2/sro.py +++ b/stix2/sro.py @@ -2,7 +2,7 @@ from .base import _STIXBase from .common import COMMON_PROPERTIES -from .properties import IDProperty, TypeProperty, Property +from .properties import IDProperty, TypeProperty, ReferenceProperty, ListProperty, Property class Relationship(_STIXBase): @@ -13,8 +13,9 @@ class Relationship(_STIXBase): 'id': IDProperty(_type), 'type': TypeProperty(_type), 'relationship_type': Property(required=True), - 'source_ref': Property(required=True), - 'target_ref': Property(required=True), + 'description': Property(), + 'source_ref': ReferenceProperty(required=True), + 'target_ref': ReferenceProperty(required=True), }) # Explicitly define the first three kwargs to make readable Relationship declarations. @@ -31,12 +32,31 @@ class Relationship(_STIXBase): if target_ref and not kwargs.get('target_ref'): kwargs['target_ref'] = target_ref - # If actual STIX objects (vs. just the IDs) are passed in, extract the - # ID values to use in the Relationship object. - if kwargs.get('source_ref') and isinstance(kwargs['source_ref'], _STIXBase): - kwargs['source_ref'] = kwargs['source_ref'].id - - if kwargs.get('target_ref') and isinstance(kwargs['target_ref'], _STIXBase): - kwargs['target_ref'] = kwargs['target_ref'].id - super(Relationship, self).__init__(**kwargs) + + +class Sighting(_STIXBase): + _type = 'sighting' + _properties = COMMON_PROPERTIES.copy() + _properties.update({ + 'id': IDProperty(_type), + 'type': TypeProperty(_type), + 'first_seen': Property(), + '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"), + 'summary': Property(), + }) + + # Explicitly define the first kwargs to make readable Sighting declarations. + def __init__(self, sighting_of_ref=None, **kwargs): + # TODO: + # - description + + # Allow sighting_of_ref as a positional arg. + if sighting_of_ref and not kwargs.get('sighting_of_ref'): + kwargs['sighting_of_ref'] = sighting_of_ref + + super(Sighting, self).__init__(**kwargs) diff --git a/stix2/test/fixtures.py b/stix2/test/conftest.py similarity index 100% rename from stix2/test/fixtures.py rename to stix2/test/conftest.py diff --git a/stix2/test/constants.py b/stix2/test/constants.py index 1fc09cb..6d88a84 100644 --- a/stix2/test/constants.py +++ b/stix2/test/constants.py @@ -8,6 +8,7 @@ INDICATOR_ID = "indicator--01234567-89ab-cdef-0123-456789abcdef" MALWARE_ID = "malware--fedcba98-7654-3210-fedc-ba9876543210" RELATIONSHIP_ID = "relationship--00000000-1111-2222-3333-444444444444" IDENTITY_ID = "identity--d4d765ce-cff7-40e8-b7a6-e205d005ac2c" +SIGHTING_ID = "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb" # Minimum required args for an Indicator instance INDICATOR_KWARGS = dict( @@ -27,3 +28,7 @@ RELATIONSHIP_KWARGS = dict( source_ref=INDICATOR_ID, target_ref=MALWARE_ID, ) + +SIGHTING_KWARGS = dict( + sighting_of_ref=INDICATOR_ID, +) diff --git a/stix2/test/test_bundle.py b/stix2/test/test_bundle.py index 03ac640..39ca2d5 100644 --- a/stix2/test/test_bundle.py +++ b/stix2/test/test_bundle.py @@ -2,8 +2,6 @@ import pytest import stix2 -from .fixtures import clock, uuid4, indicator, malware, relationship # noqa: F401 - EXPECTED_BUNDLE = """{ "id": "bundle--00000000-0000-0000-0000-000000000004", "objects": [ @@ -73,13 +71,13 @@ def test_bundle_with_wrong_spec_version(): assert str(excinfo.value) == "Invalid value for Bundle 'spec_version': must equal '2.0'." -def test_create_bundle(indicator, malware, relationship): # noqa: F811 +def test_create_bundle(indicator, malware, relationship): bundle = stix2.Bundle(objects=[indicator, malware, relationship]) assert str(bundle) == EXPECTED_BUNDLE -def test_create_bundle_with_positional_args(indicator, malware, relationship): # noqa: F811 +def test_create_bundle_with_positional_args(indicator, malware, relationship): bundle = stix2.Bundle(indicator, malware, relationship) assert str(bundle) == EXPECTED_BUNDLE diff --git a/stix2/test/test_fixtures.py b/stix2/test/test_fixtures.py index 5c0409e..9078972 100644 --- a/stix2/test/test_fixtures.py +++ b/stix2/test/test_fixtures.py @@ -2,14 +2,13 @@ import datetime as dt import uuid from .constants import FAKE_TIME -from .fixtures import clock, uuid4 # noqa: F401 -def test_clock(clock): # noqa: F811 +def test_clock(clock): assert dt.datetime.now() == FAKE_TIME -def test_my_uuid4_fixture(uuid4): # noqa: F811 +def test_my_uuid4_fixture(uuid4): assert uuid.uuid4() == "00000000-0000-0000-0000-000000000001" assert uuid.uuid4() == "00000000-0000-0000-0000-000000000002" assert uuid.uuid4() == "00000000-0000-0000-0000-000000000003" diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py index 436bb28..d5fd3f4 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -6,7 +6,6 @@ import pytz import stix2 from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS -from .fixtures import clock, uuid4, indicator # noqa: F401 EXPECTED_INDICATOR = """{ "created": "2017-01-01T00:00:01Z", @@ -49,7 +48,7 @@ def test_indicator_with_all_required_fields(): assert repr(ind) == EXPECTED_INDICATOR_REPR -def test_indicator_autogenerated_fields(indicator): # noqa: F811 +def test_indicator_autogenerated_fields(indicator): assert indicator.type == 'indicator' assert indicator.id == 'indicator--00000000-0000-0000-0000-000000000001' assert indicator.created == FAKE_TIME @@ -96,7 +95,7 @@ def test_indicator_required_field_pattern(): def test_indicator_created_ref_invalid_format(): with pytest.raises(ValueError) as excinfo: stix2.Indicator(created_by_ref='myprefix--12345678', **INDICATOR_KWARGS) - assert str(excinfo.value) == "Invalid value for Indicator 'created_by_ref': must match --." + assert str(excinfo.value) == "Invalid value for Indicator 'created_by_ref': must start with 'identity'." def test_indicator_revoked_invalid(): @@ -105,7 +104,7 @@ def test_indicator_revoked_invalid(): assert str(excinfo.value) == "Invalid value for Indicator 'revoked': must be a boolean value." -def test_cannot_assign_to_indicator_attributes(indicator): # noqa: F811 +def test_cannot_assign_to_indicator_attributes(indicator): with pytest.raises(ValueError) as excinfo: indicator.valid_from = dt.datetime.now() diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index 9537840..409b3d2 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -7,7 +7,6 @@ import re import stix2 from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS -from .fixtures import clock, uuid4, malware # noqa: F401 EXPECTED_MALWARE = """{ "created": "2016-05-12T08:17:27Z", @@ -36,7 +35,7 @@ def test_malware_with_all_required_fields(): assert str(mal) == EXPECTED_MALWARE -def test_malware_autogenerated_fields(malware): # noqa: F811 +def test_malware_autogenerated_fields(malware): assert malware.type == 'malware' assert malware.id == 'malware--00000000-0000-0000-0000-000000000001' assert malware.created == FAKE_TIME @@ -78,7 +77,7 @@ def test_malware_required_field_name(): assert str(excinfo.value) == "Missing required field(s) for Malware: (name)." -def test_cannot_assign_to_malware_attributes(malware): # noqa: F811 +def test_cannot_assign_to_malware_attributes(malware): with pytest.raises(ValueError) as excinfo: malware.name = "Cryptolocker II" diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py new file mode 100644 index 0000000..fd9b376 --- /dev/null +++ b/stix2/test/test_markings.py @@ -0,0 +1,115 @@ +import stix2 +from stix2.markings import TLP_WHITE +import pytest + +EXPECTED_TLP_MARKING_DEFINITION = """{ + "created": "2017-01-20T00:00:00.000Z", + "definition": { + "tlp": "white" + }, + "definition_type": "tlp", + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "type": "marking-definition" +}""" + +EXPECTED_STATEMENT_MARKING_DEFINITION = """{ + "created": "2017-01-20T00:00:00.000Z", + "definition": { + "statement": "Copyright 2016, Example Corp" + }, + "definition_type": "statement", + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "type": "marking-definition" +}""" + +EXPECTED_GRANULAR_MARKING = """{ + "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "selectors": [ + "abc", + "abc.[23]", + "abc.def", + "abc.[2].efg" + ] +}""" + +EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ + "created": "2016-04-06T20:03:00.000Z", + "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": [ + { + "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "selectors": [ + "description" + ] + } + ], + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "modified": "2016-04-06T20:03:00.000Z", + "name": "Green Group Attacks Against Finance", + "type": "campaign" +}""" + + +def test_marking_def_example_with_tlp(): + assert str(TLP_WHITE) == EXPECTED_TLP_MARKING_DEFINITION + + +def test_marking_def_example_with_statement(): + marking_definition = stix2.MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="statement", + definition=stix2.StatementMarking(statement="Copyright 2016, Example Corp") + ) + + assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION + + +def test_marking_def_example_with_positional_statement(): + marking_definition = stix2.MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="statement", + definition=stix2.StatementMarking("Copyright 2016, Example Corp") + ) + + assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION + + +def test_granular_example(): + granular_marking = stix2.GranularMarking( + marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + selectors=["abc", "abc.[23]", "abc.def", "abc.[2].efg"] + ) + + assert str(granular_marking) == EXPECTED_GRANULAR_MARKING + + +def test_granular_example_with_bad_selector(): + with pytest.raises(ValueError) 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" + + +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", + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", + granular_markings=[ + stix2.GranularMarking( + marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + selectors=["description"]) + ]) + print(str(campaign)) + assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS + +# TODO: Add other examples diff --git a/stix2/test/test_relationship.py b/stix2/test/test_relationship.py index a501ea6..0c96bb1 100644 --- a/stix2/test/test_relationship.py +++ b/stix2/test/test_relationship.py @@ -7,7 +7,6 @@ import stix2 from .constants import FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID from .constants import RELATIONSHIP_KWARGS -from .fixtures import clock, uuid4, indicator, malware, relationship # noqa: F401 EXPECTED_RELATIONSHIP = """{ @@ -36,7 +35,7 @@ def test_relationship_all_required_fields(): assert str(rel) == EXPECTED_RELATIONSHIP -def test_relationship_autogenerated_fields(relationship): # noqa: F811 +def test_relationship_autogenerated_fields(relationship): assert relationship.type == 'relationship' assert relationship.id == 'relationship--00000000-0000-0000-0000-000000000001' assert relationship.created == FAKE_TIME @@ -89,7 +88,7 @@ def test_relationship_required_field_target_ref(): assert str(excinfo.value) == "Missing required field(s) for Relationship: (target_ref)." -def test_cannot_assign_to_relationship_attributes(relationship): # noqa: F811 +def test_cannot_assign_to_relationship_attributes(relationship): with pytest.raises(ValueError) as excinfo: relationship.relationship_type = "derived-from" @@ -102,7 +101,7 @@ def test_invalid_kwarg_to_relationship(): assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']" in str(excinfo) -def test_create_relationship_from_objects_rather_than_ids(indicator, malware): # noqa: F811 +def test_create_relationship_from_objects_rather_than_ids(indicator, malware): rel = stix2.Relationship( relationship_type="indicates", source_ref=indicator, @@ -115,7 +114,7 @@ def test_create_relationship_from_objects_rather_than_ids(indicator, malware): assert rel.id == 'relationship--00000000-0000-0000-0000-000000000003' -def test_create_relationship_with_positional_args(indicator, malware): # noqa: F811 +def test_create_relationship_with_positional_args(indicator, malware): rel = stix2.Relationship(indicator, 'indicates', malware) assert rel.relationship_type == 'indicates' diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py index 2727dbc..7cfcae9 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -1,4 +1,6 @@ import stix2 +import pytest +from .constants import INDICATOR_KWARGS EXPECTED = """{ "created": "2015-12-21T19:59:11Z", @@ -39,4 +41,45 @@ def test_report_example(): assert str(report) == EXPECTED + +def test_report_example_objects_in_object_refs(): + 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", + name="The Black Vine Cyberespionage Group", + description="A simple report with an indicator and campaign", + published="2016-01-201T17:00:00Z", + labels=["campaign"], + object_refs=[ + stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ], + ) + + assert str(report) == EXPECTED + + +def test_report_example_objects_in_object_refs_with_bad_id(): + with pytest.raises(ValueError) as excinfo: + 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", + name="The Black Vine Cyberespionage Group", + description="A simple report with an indicator and campaign", + published="2016-01-201T17:00:00Z", + labels=["campaign"], + object_refs=[ + stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), + "campaign-83422c77-904c-4dc1-aff5-5c38f3a2c55c", # the "bad" id, missing a "-" + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ], + ) + + assert str(excinfo.value) == "Invalid value for Report 'object_refs': must match --." + # TODO: Add other examples diff --git a/stix2/test/test_sighting.py b/stix2/test/test_sighting.py new file mode 100644 index 0000000..ad30c63 --- /dev/null +++ b/stix2/test/test_sighting.py @@ -0,0 +1,81 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import INDICATOR_ID, SIGHTING_ID, SIGHTING_KWARGS + + +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" + ] +}""" + +BAD_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": [ + "malware--8cc7afd6-5455-4d2b-a736-e614ee631d99" + ] +}""" + + +def test_sighting_all_required_fields(): + now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + + s = stix2.Sighting( + type='sighting', + id=SIGHTING_ID, + created=now, + modified=now, + sighting_of_ref=INDICATOR_ID, + where_sighted_refs=["identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"] + ) + assert str(s) == EXPECTED_SIGHTING + + +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: + stix2.Sighting( + type='sighting', + id=SIGHTING_ID, + created=now, + modified=now, + sighting_of_ref=INDICATOR_ID, + where_sighted_refs=["malware--8cc7afd6-5455-4d2b-a736-e614ee631d99"] + ) + + 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: + stix2.Sighting(type='xxx', **SIGHTING_KWARGS) + + assert str(excinfo.value) == "Invalid value for Sighting 'type': must equal 'sighting'." + + +def test_invalid_kwarg_to_sighting(): + with pytest.raises(TypeError) as excinfo: + stix2.Sighting(my_custom_property="foo", **SIGHTING_KWARGS) + assert str(excinfo.value) == "unexpected keyword arguments: ['my_custom_property']" in str(excinfo) + + +def test_create_sighting_from_objects_rather_than_ids(malware): # noqa: F811 + rel = stix2.Sighting(sighting_of_ref=malware) + + assert rel.sighting_of_ref == 'malware--00000000-0000-0000-0000-000000000001' + assert rel.id == 'sighting--00000000-0000-0000-0000-000000000002' diff --git a/tox.ini b/tox.ini index 62c57d0..fd580f9 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,9 @@ commands = ignore= max-line-length=160 +[flake8] +max-line-length=160 + [travis] python = 2.6: py26