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