Merge branch 'master' into parsing

stix2.1
clenk 2017-04-11 15:05:22 -04:00
commit bc8bdccece
23 changed files with 468 additions and 65 deletions

4
.gitignore vendored
View File

@ -59,3 +59,7 @@ target/
# Vim
*.swp
#
# PyCharm
.idea/

View File

@ -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

View File

@ -1,4 +1,6 @@
extensions = []
extensions = [
'sphinx-prompt',
]
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'

View File

@ -21,14 +21,24 @@ Setting up a development environment
We recommend using a `virtualenv <https://virtualenv.pypa.io/en/stable/>`_.
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 <http://pre-commit.com/#usage>`_ 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_<xxx>.py
.. prompt:: bash
pytest stix2/test/test_<xxx>.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 <https://tox.readthedocs.io/en/latest/>`_ 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
<http://coverage.readthedocs.io/en/latest/>`_ 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``.

View File

@ -3,6 +3,7 @@ pre-commit
pytest
pytest-cov
sphinx
sphinx-prompt
tox
-e .

View File

@ -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):

View File

@ -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]

View File

@ -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)
}

76
stix2/markings.py Normal file
View File

@ -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")
)

View File

@ -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 <object-type>--<guid>.")
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

View File

@ -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),
})

View File

@ -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)

View File

@ -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,
)

View File

@ -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

View File

@ -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"

View File

@ -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 <object-type>--<guid>."
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()

View File

@ -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"

115
stix2/test/test_markings.py Normal file
View File

@ -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

View File

@ -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'

View File

@ -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 <object-type>--<guid>."
# TODO: Add other examples

View File

@ -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'

View File

@ -17,6 +17,9 @@ commands =
ignore=
max-line-length=160
[flake8]
max-line-length=160
[travis]
python =
2.6: py26