diff --git a/.isort.cfg b/.isort.cfg index 84bc4c7..d644f60 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -15,3 +15,5 @@ known_third_party = taxii2client, known_first_party = stix2 force_sort_within_sections = 1 +multi_line_output = 5 +include_trailing_comma = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 742cbb2..7620c35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,16 @@ +repos: - repo: https://github.com/pre-commit/pre-commit-hooks - sha: v0.9.4 + rev: v1.3.0 hooks: - id: trailing-whitespace - id: flake8 args: - --max-line-length=160 - id: check-merge-conflict +- repo: https://github.com/asottile/add-trailing-comma + rev: v0.6.4 + hooks: + - id: add-trailing-comma - repo: https://github.com/FalconSocial/pre-commit-python-sorter sha: b57843b0b874df1d16eb0bef00b868792cb245c2 hooks: diff --git a/CHANGELOG b/CHANGELOG index e9f8abf..9c6b2ad 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,15 @@ CHANGELOG ========= +1.1.0 - 2018-11-28 + +- Most (if not all) STIX 2.1 SDOs/SROs and core objects have been implemented according to the latest CSD/WD document +- There is an implementation for the conversion scales +- #196, #193 Removing duplicate code for: properties, registering objects, parsing objects, custom objects +- #80, #197 Most (if not all) tests created for v20 are also implemented for v21 +- #189 Added extra checks for the pre-commit tool +- #202 It is now possible to pass a Bundle into add() method in Memory datastores + 1.0.4 - 2018-11-15 * #225 MemorySource fix to support custom objects diff --git a/README.rst b/README.rst index cb25892..c509d46 100644 --- a/README.rst +++ b/README.rst @@ -1,42 +1,34 @@ -|Build_Status| |Coverage| |Version| +|Build_Status| |Coverage| |Version| |Downloads_Badge| cti-python-stix2 ================ -This is an `OASIS TC Open -Repository `__. +This is an `OASIS TC Open Repository `__. See the `Governance <#governance>`__ section for more information. -This repository provides Python APIs for serializing and de- -serializing -STIX 2 JSON content, along with higher-level APIs for common tasks, -including data markings, versioning, and for resolving STIX IDs across -multiple data sources. +This repository provides Python APIs for serializing and de-serializing STIX2 +JSON content, along with higher-level APIs for common tasks, including data +markings, versioning, and for resolving STIX IDs across multiple data sources. -For more information, see `the -documentation `__ on -ReadTheDocs. +For more information, see `the documentation `__ on ReadTheDocs. Installation ------------ Install with `pip `__: -:: +.. code-block:: bash - pip install stix2 + $ pip install stix2 Usage ----- -To create a STIX object, provide keyword arguments to the type's -constructor. Certain required attributes of all objects, such as -``type`` or -``id``, will be set automatically if not provided as keyword -arguments. +To create a STIX object, provide keyword arguments to the type's constructor. +Certain required attributes of all objects, such as ``type`` or ``id``, will +be set automatically if not provided as keyword arguments. -.. code:: python +.. code-block:: python from stix2 import Indicator @@ -44,135 +36,100 @@ arguments. labels=["malicious-activity"], pattern="[file:hashes.md5 = 'd41d8cd98f00b204e9800998ecf8427e']") -To parse a STIX JSON string into a Python STIX object, use -``parse()``: +To parse a STIX JSON string into a Python STIX object, use ``parse()``: -.. code:: python +.. code-block:: python from stix2 import parse indicator = parse("""{ "type": "indicator", + "spec_version": "2.1", "id": "indicator--dbcbd659-c927-4f9a-994f-0a2632274394", "created": "2017-09-26T23:33:39.829Z", "modified": "2017-09-26T23:33:39.829Z", - "labels": [ + "name": "File hash for malware variant", + "indicator_types": [ "malicious-activity" ], - "name": "File hash for malware variant", "pattern": "[file:hashes.md5 ='d41d8cd98f00b204e9800998ecf8427e']", "valid_from": "2017-09-26T23:33:39.829952Z" }""") + print(indicator) -For more in-depth documentation, please see -`https://stix2.readthedocs.io/ `__. +For more in-depth documentation, please see `https://stix2.readthedocs.io/ `__. STIX 2.X Technical Specification Support ---------------------------------------- -This version of python-stix2 supports STIX 2.0 by default. Although, -the -`stix2` Python library is built to support multiple versions of the -STIX -Technical Specification. With every major release of stix2 the -``import stix2`` -statement will automatically load the SDO/SROs equivalent to the most -recent -supported 2.X Technical Specification. Please see the library -documentation -for more details. +This version of python-stix2 brings initial support to STIX 2.1 currently at the +CSD level. The intention is to help debug components of the library and also +check for problems that should be fixed in the specification. + +The `stix2` Python library is built to support multiple versions of the STIX +Technical Specification. With every major release of stix2 the ``import stix2`` +statement will automatically load the SDO/SROs equivalent to the most recent +supported 2.X Committee Specification. Please see the library documentation for +more details. Governance ---------- -This GitHub public repository ( -**https://github.com/oasis-open/cti-python-stix2** ) was -`proposed `__ -and -`approved `__ +This GitHub public repository (**https://github.com/oasis-open/cti-python-stix2**) was +`proposed `__ and +`approved `__ [`bis `__] by the -`OASIS Cyber Threat Intelligence (CTI) -TC `__ as an `OASIS TC -Open -Repository `__ -to support development of open source resources related to Technical -Committee work. +`OASIS Cyber Threat Intelligence (CTI) TC `__ +as an `OASIS TC Open Repository `__ +to support development of open source resources related to Technical Committee work. -While this TC Open Repository remains associated with the sponsor TC, -its -development priorities, leadership, intellectual property terms, -participation rules, and other matters of governance are `separate and -distinct `__ +While this TC Open Repository remains associated with the sponsor TC, its +development priorities, leadership, intellectual property terms, participation +rules, and other matters of governance are `separate and distinct +`__ from the OASIS TC Process and related policies. All contributions made to this TC Open Repository are subject to open -source license terms expressed in the `BSD-3-Clause -License `__. -That license was selected as the declared `"Applicable -License" `__ +source license terms expressed in the `BSD-3-Clause License `__. +That license was selected as the declared `"Applicable License" `__ when the TC Open Repository was created. -As documented in `"Public Participation -Invited `__", -contributions to this OASIS TC Open Repository are invited from all -parties, whether affiliated with OASIS or not. Participants must have -a -GitHub account, but no fees or OASIS membership obligations are -required. Participation is expected to be consistent with the `OASIS -TC Open Repository Guidelines and -Procedures `__, -the open source -`LICENSE `__ +As documented in `"Public Participation Invited +`__", +contributions to this OASIS TC Open Repository are invited from all parties, +whether affiliated with OASIS or not. Participants must have a GitHub account, +but no fees or OASIS membership obligations are required. Participation is +expected to be consistent with the `OASIS TC Open Repository Guidelines and Procedures +`__, +the open source `LICENSE `__ designated for this particular repository, and the requirement for an -`Individual Contributor License -Agreement `__ +`Individual Contributor License Agreement `__ that governs intellectual property. Maintainers ~~~~~~~~~~~ -TC Open Repository -`Maintainers `__ +TC Open Repository `Maintainers `__ are responsible for oversight of this project's community development -activities, including evaluation of GitHub `pull -requests `__ -and -`preserving `__ -open source principles of openness and fairness. Maintainers are -recognized and trusted experts who serve to implement community goals -and consensus design preferences. +activities, including evaluation of GitHub +`pull requests `__ +and `preserving `__ +open source principles of openness and fairness. Maintainers are recognized +and trusted experts who serve to implement community goals and consensus design +preferences. -Initially, the associated TC members have designated one or more -persons -to serve as Maintainer(s); subsequently, participating community -members -may select additional or substitute Maintainers, per `consensus -agreements `__. +Initially, the associated TC members have designated one or more persons to +serve as Maintainer(s); subsequently, participating community members may +select additional or substitute Maintainers, per `consensus agreements +`__. -.. _currentMaintainers: +.. _currentmaintainers: **Current Maintainers of this TC Open Repository** - `Chris Lenk `__; GitHub ID: - https://github.com/clenk/; WWW: `MITRE - Corporation `__ + https://github.com/clenk/; WWW: `MITRE Corporation `__ - `Emmanuelle Vargas-Gonzalez `__; GitHub ID: https://github.com/emmanvg/; WWW: `MITRE @@ -181,39 +138,32 @@ repositories/maintainers-guide#additionalMaintainers>`__. About OASIS TC Open Repositories -------------------------------- -- `TC Open Repositories: Overview and - Resources `__ -- `Frequently Asked - Questions `__ -- `Open Source - Licenses `__ -- `Contributor License Agreements - (CLAs) `__ -- `Maintainers' Guidelines and - Agreement `__ +- `TC Open Repositories: Overview and Resources `__ +- `Frequently Asked Questions `__ +- `Open Source Licenses `__ +- `Contributor License Agreements (CLAs) `__ +- `Maintainers' Guidelines and Agreement `__ Feedback -------- -Questions or comments about this TC Open Repository's activities -should be -composed as GitHub issues or comments. If use of an issue/comment is -not +Questions or comments about this TC Open Repository's activities should be +composed as GitHub issues or comments. If use of an issue/comment is not possible or appropriate, questions may be directed by email to the -Maintainer(s) `listed above <#currentmaintainers>`__. Please send -general questions about TC Open Repository participation to OASIS -Staff at +Maintainer(s) `listed above <#currentmaintainers>`__. Please send general +questions about TC Open Repository participation to OASIS Staff at repository-admin@oasis-open.org and any specific CLA-related questions to repository-cla@oasis-open.org. .. |Build_Status| image:: https://travis-ci.org/oasis-open/cti-python-stix2.svg?branch=master :target: https://travis-ci.org/oasis-open/cti-python-stix2 + :alt: Build Status .. |Coverage| image:: https://codecov.io/gh/oasis-open/cti-python-stix2/branch/master/graph/badge.svg :target: https://codecov.io/gh/oasis-open/cti-python-stix2 + :alt: Coverage .. |Version| image:: https://img.shields.io/pypi/v/stix2.svg?maxAge=3600 :target: https://pypi.python.org/pypi/stix2/ + :alt: Version +.. |Downloads_Badge| image:: https://img.shields.io/pypi/dm/stix2.svg?maxAge=3600 + :target: https://pypi.python.org/pypi/stix2/ + :alt: Downloads diff --git a/docs/api/confidence/stix2.confidence.scales.rst b/docs/api/confidence/stix2.confidence.scales.rst new file mode 100644 index 0000000..0d7cdae --- /dev/null +++ b/docs/api/confidence/stix2.confidence.scales.rst @@ -0,0 +1,5 @@ +scales +======================= + +.. automodule:: stix2.confidence.scales + :members: \ No newline at end of file diff --git a/docs/api/stix2.confidence.rst b/docs/api/stix2.confidence.rst new file mode 100644 index 0000000..23ac076 --- /dev/null +++ b/docs/api/stix2.confidence.rst @@ -0,0 +1,5 @@ +confidence +================ + +.. automodule:: stix2.confidence + :members: \ No newline at end of file diff --git a/docs/api/stix2.v20.bundle.rst b/docs/api/stix2.v20.bundle.rst new file mode 100644 index 0000000..bd2abd4 --- /dev/null +++ b/docs/api/stix2.v20.bundle.rst @@ -0,0 +1,5 @@ +bundle +================ + +.. automodule:: stix2.v20.bundle + :members: \ No newline at end of file diff --git a/docs/api/stix2.v21.bundle.rst b/docs/api/stix2.v21.bundle.rst new file mode 100644 index 0000000..da67082 --- /dev/null +++ b/docs/api/stix2.v21.bundle.rst @@ -0,0 +1,5 @@ +bundle +================ + +.. automodule:: stix2.v21.bundle + :members: \ No newline at end of file diff --git a/docs/api/stix2.v21.common.rst b/docs/api/stix2.v21.common.rst new file mode 100644 index 0000000..b480481 --- /dev/null +++ b/docs/api/stix2.v21.common.rst @@ -0,0 +1,5 @@ +common +================ + +.. automodule:: stix2.v21.common + :members: \ No newline at end of file diff --git a/docs/api/stix2.v21.observables.rst b/docs/api/stix2.v21.observables.rst new file mode 100644 index 0000000..56f409a --- /dev/null +++ b/docs/api/stix2.v21.observables.rst @@ -0,0 +1,5 @@ +observables +===================== + +.. automodule:: stix2.v21.observables + :members: \ No newline at end of file diff --git a/docs/api/stix2.v21.sdo.rst b/docs/api/stix2.v21.sdo.rst new file mode 100644 index 0000000..b1a568d --- /dev/null +++ b/docs/api/stix2.v21.sdo.rst @@ -0,0 +1,5 @@ +sdo +============= + +.. automodule:: stix2.v21.sdo + :members: \ No newline at end of file diff --git a/docs/api/stix2.v21.sro.rst b/docs/api/stix2.v21.sro.rst new file mode 100644 index 0000000..3532a37 --- /dev/null +++ b/docs/api/stix2.v21.sro.rst @@ -0,0 +1,5 @@ +sro +============= + +.. automodule:: stix2.v21.sro + :members: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 572fc5a..2a10fbd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +import datetime import os import re import sys @@ -6,6 +7,7 @@ from six import class_types from sphinx.ext.autodoc import ClassDocumenter from stix2.base import _STIXBase +from stix2.version import __version__ sys.path.insert(0, os.path.abspath('..')) @@ -31,11 +33,11 @@ source_suffix = '.rst' master_doc = 'index' project = 'stix2' -copyright = '2017, OASIS Open' +copyright = '{}, OASIS Open'.format(datetime.date.today().year) author = 'OASIS Open' -version = '1.0.4' -release = '1.0.4' +version = __version__ +release = __version__ language = None exclude_patterns = ['_build', '_templates', 'Thumbs.db', '.DS_Store', 'guide/.ipynb_checkpoints'] @@ -49,7 +51,7 @@ html_sidebars = { 'navigation.html', 'relations.html', 'searchbox.html', - ] + ], } latex_elements = {} diff --git a/examples/taxii_example.py b/examples/taxii_example.py index 1457770..51ba821 100644 --- a/examples/taxii_example.py +++ b/examples/taxii_example.py @@ -7,8 +7,10 @@ import stix2 def main(): - collection = Collection("http://127.0.0.1:5000/trustgroup1/collections/52892447-4d7e-4f70-b94d-d7f22742ff63/", - user="admin", password="Password0") + collection = Collection( + "http://127.0.0.1:5000/trustgroup1/collections/52892447-4d7e-4f70-b94d-d7f22742ff63/", + user="admin", password="Password0", + ) # instantiate TAXII data source taxii = stix2.TAXIICollectionSource(collection) diff --git a/setup.cfg b/setup.cfg index 2859375..9b91927 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,11 +5,8 @@ tag = True [bumpversion:file:stix2/version.py] -[bumpversion:file:docs/conf.py] - [metadata] license_file = LICENSE [bdist_wheel] universal = 1 - diff --git a/setup.py b/setup.py index d701e60..07de2a4 100644 --- a/setup.py +++ b/setup.py @@ -11,26 +11,27 @@ VERSION_FILE = os.path.join(BASE_DIR, 'stix2', 'version.py') def get_version(): with open(VERSION_FILE) as f: for line in f.readlines(): - if line.startswith("__version__"): + if line.startswith('__version__'): version = line.split()[-1].strip('"') return version raise AttributeError("Package does not have a __version__") -with open('README.rst') as f: - long_description = f.read() +def get_long_description(): + with open('README.rst') as f: + return f.read() setup( name='stix2', version=get_version(), description='Produce and consume STIX 2 JSON content', - long_description=long_description, - url='https://github.com/oasis-open/cti-python-stix2', + long_description=get_long_description(), + url='https://oasis-open.github.io/cti-documentation/', author='OASIS Cyber Threat Intelligence Technical Committee', author_email='cti-users@lists.oasis-open.org', - maintainer='Greg Back', - maintainer_email='gback@mitre.org', + maintainer='Chris Lenk, Emmanuelle Vargas-Gonzalez', + maintainer_email='clenk@mitre.org, emmanuelle@mitre.org', license='BSD', classifiers=[ 'Development Status :: 4 - Beta', @@ -45,7 +46,7 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], - keywords="stix stix2 json cti cyber threat intelligence", + keywords='stix stix2 json cti cyber threat intelligence', packages=find_packages(exclude=['*.test']), install_requires=[ 'python-dateutil', @@ -55,7 +56,12 @@ setup( 'six', 'stix2-patterns', ], + project_urls={ + 'Documentation': 'https://stix2.readthedocs.io/', + 'Source Code': 'https://github.com/oasis-open/cti-python-stix2/', + 'Bug Tracker': 'https://github.com/oasis-open/cti-python-stix2/issues/', + }, extras_require={ - 'taxii': ['taxii2-client'] - } + 'taxii': ['taxii2-client'], + }, ) diff --git a/stix2/__init__.py b/stix2/__init__.py index 449be68..3bedec8 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -3,6 +3,7 @@ .. autosummary:: :toctree: api + confidence core datastore environment @@ -11,49 +12,57 @@ patterns properties utils - workbench + v20.bundle v20.common v20.observables v20.sdo v20.sro + v21.bundle + v21.common + v21.observables + v21.sdo + v21.sro + workbench + """ # flake8: noqa -from .core import Bundle, _collect_stix2_obj_maps, _register_type, parse +from .confidence import scales +from .core import _collect_stix2_mappings, parse, parse_observable from .datastore import CompositeDataSource -from .datastore.filesystem import (FileSystemSink, FileSystemSource, - FileSystemStore) +from .datastore.filesystem import ( + FileSystemSink, FileSystemSource, FileSystemStore, +) from .datastore.filters import Filter from .datastore.memory import MemorySink, MemorySource, MemoryStore -from .datastore.taxii import (TAXIICollectionSink, TAXIICollectionSource, - TAXIICollectionStore) +from .datastore.taxii import ( + TAXIICollectionSink, TAXIICollectionSource, TAXIICollectionStore, +) from .environment import Environment, ObjectFactory -from .markings import (add_markings, clear_markings, get_markings, is_marked, - remove_markings, set_markings) -from .patterns import (AndBooleanExpression, AndObservationExpression, - BasicObjectPathComponent, BinaryConstant, - BooleanConstant, EqualityComparisonExpression, - FloatConstant, FollowedByObservationExpression, - GreaterThanComparisonExpression, - GreaterThanEqualComparisonExpression, HashConstant, - HexConstant, InComparisonExpression, IntegerConstant, - IsSubsetComparisonExpression, - IsSupersetComparisonExpression, - LessThanComparisonExpression, - LessThanEqualComparisonExpression, - LikeComparisonExpression, ListConstant, - ListObjectPathComponent, MatchesComparisonExpression, - ObjectPath, ObservationExpression, OrBooleanExpression, - OrObservationExpression, ParentheticalExpression, - QualifiedObservationExpression, - ReferenceObjectPathComponent, RepeatQualifier, - StartStopQualifier, StringConstant, TimestampConstant, - WithinQualifier) +from .markings import ( + add_markings, clear_markings, get_markings, is_marked, remove_markings, + set_markings, +) +from .patterns import ( + AndBooleanExpression, AndObservationExpression, BasicObjectPathComponent, + BinaryConstant, BooleanConstant, EqualityComparisonExpression, + FloatConstant, FollowedByObservationExpression, + GreaterThanComparisonExpression, GreaterThanEqualComparisonExpression, + HashConstant, HexConstant, InComparisonExpression, IntegerConstant, + IsSubsetComparisonExpression, IsSupersetComparisonExpression, + LessThanComparisonExpression, LessThanEqualComparisonExpression, + LikeComparisonExpression, ListConstant, ListObjectPathComponent, + MatchesComparisonExpression, ObjectPath, ObservationExpression, + OrBooleanExpression, OrObservationExpression, ParentheticalExpression, + QualifiedObservationExpression, ReferenceObjectPathComponent, + RepeatQualifier, StartStopQualifier, StringConstant, TimestampConstant, + WithinQualifier, +) from .utils import new_version, revoke from .v20 import * # This import will always be the latest STIX 2.X version from .version import __version__ -_collect_stix2_obj_maps() +_collect_stix2_mappings() -DEFAULT_VERSION = "2.0" # Default version will always be the latest STIX 2.X version +DEFAULT_VERSION = '2.0' # Default version will always be the latest STIX 2.X version diff --git a/stix2/base.py b/stix2/base.py index 0fd7858..a6bafff 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -1,4 +1,4 @@ -"""Base classes for type definitions in the stix2 library.""" +"""Base classes for type definitions in the STIX2 library.""" import collections import copy @@ -6,11 +6,12 @@ import datetime as dt import simplejson as json -from .exceptions import (AtLeastOnePropertyError, CustomContentError, - DependentPropertiesError, ExtraPropertiesError, - ImmutableError, InvalidObjRefError, InvalidValueError, - MissingPropertiesError, - MutuallyExclusivePropertiesError) +from .exceptions import ( + AtLeastOnePropertyError, CustomContentError, DependentPropertiesError, + ExtraPropertiesError, ImmutableError, InvalidObjRefError, + InvalidValueError, MissingPropertiesError, + MutuallyExclusivePropertiesError, +) from .markings.utils import validate from .utils import NOW, find_property_index, format_datetime, get_timestamp from .utils import new_version as _new_version @@ -104,11 +105,11 @@ class _STIXBase(collections.Mapping): def _check_at_least_one_property(self, list_of_properties=None): if not list_of_properties: list_of_properties = sorted(list(self.__class__._properties.keys())) - if "type" in list_of_properties: - list_of_properties.remove("type") + if 'type' in list_of_properties: + list_of_properties.remove('type') current_properties = self.properties_populated() list_of_properties_populated = set(list_of_properties).intersection(current_properties) - if list_of_properties and (not list_of_properties_populated or list_of_properties_populated == set(["extensions"])): + if list_of_properties and (not list_of_properties_populated or list_of_properties_populated == set(['extensions'])): raise AtLeastOnePropertyError(self.__class__, list_of_properties) def _check_properties_dependency(self, list_of_properties, list_of_dependent_properties): @@ -121,8 +122,8 @@ class _STIXBase(collections.Mapping): raise DependentPropertiesError(self.__class__, failed_dependency_pairs) def _check_object_constraints(self): - for m in self.get("granular_markings", []): - validate(self, m.get("selectors")) + for m in self.get('granular_markings', []): + validate(self, m.get('selectors')) def __init__(self, allow_custom=False, **kwargs): cls = self.__class__ @@ -190,7 +191,7 @@ class _STIXBase(collections.Mapping): # usual behavior of this method reads an __init__-assigned attribute, # which would cause infinite recursion. So this check disables all # attribute reads until the instance has been properly initialized. - unpickling = "_inner" not in self.__dict__ + unpickling = '_inner' not in self.__dict__ if not unpickling and name in self: return self.__getitem__(name) raise AttributeError("'%s' object has no attribute '%s'" % @@ -206,8 +207,10 @@ class _STIXBase(collections.Mapping): def __repr__(self): props = [(k, self[k]) for k in self.object_properties() if self.get(k)] - return "{0}({1})".format(self.__class__.__name__, - ", ".join(["{0!s}={1!r}".format(k, v) for k, v in props])) + return '{0}({1})'.format( + self.__class__.__name__, + ', '.join(['{0!s}={1!r}'.format(k, v) for k, v in props]), + ) def __deepcopy__(self, memo): # Assume: we can ignore the memo argument, because no object will ever contain the same sub-object multiple times. @@ -273,7 +276,7 @@ class _STIXBase(collections.Mapping): def sort_by(element): return find_property_index(self, *element) - kwargs.update({'indent': 4, 'separators': (",", ": "), 'item_sort_key': sort_by}) + kwargs.update({'indent': 4, 'separators': (',', ': '), 'item_sort_key': sort_by}) if include_optional_defaults: return json.dumps(self, cls=STIXJSONIncludeOptionalDefaultsEncoder, **kwargs) diff --git a/stix2/confidence/__init__.py b/stix2/confidence/__init__.py new file mode 100644 index 0000000..80e0dc7 --- /dev/null +++ b/stix2/confidence/__init__.py @@ -0,0 +1,10 @@ +""" +Functions to operate with STIX2 Confidence scales. + +.. autosummary:: + :toctree: confidence + + scales + +| +""" diff --git a/stix2/confidence/scales.py b/stix2/confidence/scales.py new file mode 100644 index 0000000..97405cd --- /dev/null +++ b/stix2/confidence/scales.py @@ -0,0 +1,571 @@ +# -*- coding: utf-8 -*- + +"""Functions to perform conversions between the different Confidence scales. +As specified in STIX™ Version 2.1. Part 1: STIX Core Concepts - Appendix B""" + + +def none_low_med_high_to_value(scale_value): + """ + This method will transform a string value from the None / Low / Med / + High scale to its confidence integer representation. + + The scale for this confidence representation is the following: + + .. list-table:: None, Low, Med, High to STIX Confidence + :header-rows: 1 + + * - None/ Low/ Med/ High + - STIX Confidence Value + * - Not Specified + - Not Specified + * - None + - 0 + * - Low + - 15 + * - Med + - 50 + * - High + - 85 + + Args: + scale_value (str): A string value from the scale. Accepted strings are + "None", "Low", "Med" and "High". Argument is case sensitive. + + Returns: + int: The numerical representation corresponding to values in the + None / Low / Med / High scale. + + Raises: + ValueError: If `scale_value` is not within the accepted strings. + """ + if scale_value == 'None': + return 0 + elif scale_value == 'Low': + return 15 + elif scale_value == 'Med': + return 50 + elif scale_value == 'High': + return 85 + else: + raise ValueError("STIX Confidence value cannot be determined for %s" % scale_value) + + +def value_to_none_low_medium_high(confidence_value): + """ + This method will transform an integer value into the None / Low / Med / + High scale string representation. + + The scale for this confidence representation is the following: + + .. list-table:: STIX Confidence to None, Low, Med, High + :header-rows: 1 + + * - Range of Values + - None/ Low/ Med/ High + * - 0 + - None + * - 1-29 + - Low + * - 30-69 + - Med + * - 70-100 + - High + + Args: + confidence_value (int): An integer value between 0 and 100. + + Returns: + str: A string corresponding to the None / Low / Med / High scale. + + Raises: + ValueError: If `confidence_value` is out of bounds. + + """ + if confidence_value == 0: + return 'None' + elif 29 >= confidence_value >= 1: + return 'Low' + elif 69 >= confidence_value >= 30: + return 'Med' + elif 100 >= confidence_value >= 70: + return 'High' + else: + raise ValueError("Range of values out of bounds: %s" % confidence_value) + + +def zero_ten_to_value(scale_value): + """ + This method will transform a string value from the 0-10 scale to its + confidence integer representation. + + The scale for this confidence representation is the following: + + .. list-table:: 0-10 to STIX Confidence + :header-rows: 1 + + * - 0-10 Scale + - STIX Confidence Value + * - 0 + - 0 + * - 1 + - 10 + * - 2 + - 20 + * - 3 + - 30 + * - 4 + - 40 + * - 5 + - 50 + * - 6 + - 60 + * - 7 + - 70 + * - 8 + - 80 + * - 9 + - 90 + * - 10 + - 100 + + Args: + scale_value (str): A string value from the scale. Accepted strings are "0" + through "10" inclusive. + + Returns: + int: The numerical representation corresponding to values in the 0-10 + scale. + + Raises: + ValueError: If `scale_value` is not within the accepted strings. + + """ + if scale_value == '0': + return 0 + elif scale_value == '1': + return 10 + elif scale_value == '2': + return 20 + elif scale_value == '3': + return 30 + elif scale_value == '4': + return 40 + elif scale_value == '5': + return 50 + elif scale_value == '6': + return 60 + elif scale_value == '7': + return 70 + elif scale_value == '8': + return 80 + elif scale_value == '9': + return 90 + elif scale_value == '10': + return 100 + else: + raise ValueError("STIX Confidence value cannot be determined for %s" % scale_value) + + +def value_to_zero_ten(confidence_value): + """ + This method will transform an integer value into the 0-10 scale string + representation. + + The scale for this confidence representation is the following: + + .. list-table:: STIX Confidence to 0-10 + :header-rows: 1 + + * - Range of Values + - 0-10 Scale + * - 0-4 + - 0 + * - 5-14 + - 1 + * - 15-24 + - 2 + * - 25-34 + - 3 + * - 35-44 + - 4 + * - 45-54 + - 5 + * - 55-64 + - 6 + * - 65-74 + - 7 + * - 75-84 + - 8 + * - 95-94 + - 9 + * - 95-100 + - 10 + + Args: + confidence_value (int): An integer value between 0 and 100. + + Returns: + str: A string corresponding to the 0-10 scale. + + Raises: + ValueError: If `confidence_value` is out of bounds. + + """ + if 4 >= confidence_value >= 0: + return '0' + elif 14 >= confidence_value >= 5: + return '1' + elif 24 >= confidence_value >= 15: + return '2' + elif 34 >= confidence_value >= 25: + return '3' + elif 44 >= confidence_value >= 35: + return '4' + elif 54 >= confidence_value >= 45: + return '5' + elif 64 >= confidence_value >= 55: + return '6' + elif 74 >= confidence_value >= 65: + return '7' + elif 84 >= confidence_value >= 75: + return '8' + elif 94 >= confidence_value >= 85: + return '9' + elif 100 >= confidence_value >= 95: + return '10' + else: + raise ValueError("Range of values out of bounds: %s" % confidence_value) + + +def admiralty_credibility_to_value(scale_value): + """ + This method will transform a string value from the Admiralty Credibility + scale to its confidence integer representation. + + The scale for this confidence representation is the following: + + .. list-table:: Admiralty Credibility Scale to STIX Confidence + :header-rows: 1 + + * - Admiralty Credibility + - STIX Confidence Value + * - 6 - Truth cannot be judged + - (Not present) + * - 5 - Improbable + - 10 + * - 4 - Doubtful + - 30 + * - 3 - Possibly True + - 50 + * - 2 - Probably True + - 70 + * - 1 - Confirmed by other sources + - 90 + + Args: + scale_value (str): A string value from the scale. Accepted strings are + "6 - Truth cannot be judged", "5 - Improbable", "4 - Doubtful", + "3 - Possibly True", "2 - Probably True" and + "1 - Confirmed by other sources". Argument is case sensitive. + + Returns: + int: The numerical representation corresponding to values in the + Admiralty Credibility scale. + + Raises: + ValueError: If `scale_value` is not within the accepted strings. + + """ + if scale_value == '6 - Truth cannot be judged': + raise ValueError("STIX Confidence value cannot be determined for %s" % scale_value) + elif scale_value == '5 - Improbable': + return 10 + elif scale_value == '4 - Doubtful': + return 30 + elif scale_value == '3 - Possibly True': + return 50 + elif scale_value == '2 - Probably True': + return 70 + elif scale_value == '1 - Confirmed by other sources': + return 90 + else: + raise ValueError("STIX Confidence value cannot be determined for %s" % scale_value) + + +def value_to_admiralty_credibility(confidence_value): + """ + This method will transform an integer value into the Admiralty Credibility + scale string representation. + + The scale for this confidence representation is the following: + + .. list-table:: STIX Confidence to Admiralty Credibility Scale + :header-rows: 1 + + * - Range of Values + - Admiralty Credibility + * - N/A + - 6 - Truth cannot be judged + * - 0-19 + - 5 - Improbable + * - 20-39 + - 4 - Doubtful + * - 40-59 + - 3 - Possibly True + * - 60-79 + - 2 - Probably True + * - 80-100 + - 1 - Confirmed by other sources + + Args: + confidence_value (int): An integer value between 0 and 100. + + Returns: + str: A string corresponding to the Admiralty Credibility scale. + + Raises: + ValueError: If `confidence_value` is out of bounds. + + """ + if 19 >= confidence_value >= 0: + return '5 - Improbable' + elif 39 >= confidence_value >= 20: + return '4 - Doubtful' + elif 59 >= confidence_value >= 40: + return '3 - Possibly True' + elif 79 >= confidence_value >= 60: + return '2 - Probably True' + elif 100 >= confidence_value >= 80: + return '1 - Confirmed by other sources' + else: + raise ValueError("Range of values out of bounds: %s" % confidence_value) + + +def wep_to_value(scale_value): + """ + This method will transform a string value from the WEP scale to its + confidence integer representation. + + The scale for this confidence representation is the following: + + .. list-table:: WEP to STIX Confidence + :header-rows: 1 + + * - WEP + - STIX Confidence Value + * - Impossible + - 0 + * - Highly Unlikely/Almost Certainly Not + - 10 + * - Unlikely/Probably Not + - 20 + * - Even Chance + - 50 + * - Likely/Probable + - 70 + * - Highly likely/Almost Certain + - 90 + * - Certain + - 100 + + Args: + scale_value (str): A string value from the scale. Accepted strings are + "Impossible", "Highly Unlikely/Almost Certainly Not", + "Unlikely/Probably Not", "Even Chance", "Likely/Probable", + "Highly likely/Almost Certain" and "Certain". Argument is case + sensitive. + + Returns: + int: The numerical representation corresponding to values in the WEP + scale. + + Raises: + ValueError: If `scale_value` is not within the accepted strings. + + """ + if scale_value == 'Impossible': + return 0 + elif scale_value == 'Highly Unlikely/Almost Certainly Not': + return 10 + elif scale_value == 'Unlikely/Probably Not': + return 30 + elif scale_value == 'Even Chance': + return 50 + elif scale_value == 'Likely/Probable': + return 70 + elif scale_value == 'Highly likely/Almost Certain': + return 90 + elif scale_value == 'Certain': + return 100 + else: + raise ValueError("STIX Confidence value cannot be determined for %s" % scale_value) + + +def value_to_wep(confidence_value): + """ + This method will transform an integer value into the WEP scale string + representation. + + The scale for this confidence representation is the following: + + .. list-table:: STIX Confidence to WEP + :header-rows: 1 + + * - Range of Values + - WEP + * - 0 + - Impossible + * - 1-19 + - Highly Unlikely/Almost Certainly Not + * - 20-39 + - Unlikely/Probably Not + * - 40-59 + - Even Chance + * - 60-79 + - Likely/Probable + * - 80-99 + - Highly likely/Almost Certain + * - 100 + - Certain + + Args: + confidence_value (int): An integer value between 0 and 100. + + Returns: + str: A string corresponding to the WEP scale. + + Raises: + ValueError: If `confidence_value` is out of bounds. + + """ + if confidence_value == 0: + return 'Impossible' + elif 19 >= confidence_value >= 1: + return 'Highly Unlikely/Almost Certainly Not' + elif 39 >= confidence_value >= 20: + return 'Unlikely/Probably Not' + elif 59 >= confidence_value >= 40: + return 'Even Chance' + elif 79 >= confidence_value >= 60: + return 'Likely/Probable' + elif 99 >= confidence_value >= 80: + return 'Highly likely/Almost Certain' + elif confidence_value == 100: + return 'Certain' + else: + raise ValueError("Range of values out of bounds: %s" % confidence_value) + + +def dni_to_value(scale_value): + """ + This method will transform a string value from the DNI scale to its + confidence integer representation. + + The scale for this confidence representation is the following: + + .. list-table:: DNI Scale to STIX Confidence + :header-rows: 1 + + * - DNI Scale + - STIX Confidence Value + * - Almost No Chance / Remote + - 5 + * - Very Unlikely / Highly Improbable + - 15 + * - Unlikely / Improbable + - 30 + * - Roughly Even Chance / Roughly Even Odds + - 50 + * - Likely / Probable + - 70 + * - Very Likely / Highly Probable + - 85 + * - Almost Certain / Nearly Certain + - 95 + + Args: + scale_value (str): A string value from the scale. Accepted strings are + "Almost No Chance / Remote", "Very Unlikely / Highly Improbable", + "Unlikely / Improbable", "Roughly Even Chance / Roughly Even Odds", + "Likely / Probable", "Very Likely / Highly Probable" and + "Almost Certain / Nearly Certain". Argument is case sensitive. + + Returns: + int: The numerical representation corresponding to values in the DNI + scale. + + Raises: + ValueError: If `scale_value` is not within the accepted strings. + + """ + if scale_value == 'Almost No Chance / Remote': + return 5 + elif scale_value == 'Very Unlikely / Highly Improbable': + return 15 + elif scale_value == 'Unlikely / Improbable': + return 30 + elif scale_value == 'Roughly Even Chance / Roughly Even Odds': + return 50 + elif scale_value == 'Likely / Probable': + return 70 + elif scale_value == 'Very Likely / Highly Probable': + return 85 + elif scale_value == 'Almost Certain / Nearly Certain': + return 95 + else: + raise ValueError("STIX Confidence value cannot be determined for %s" % scale_value) + + +def value_to_dni(confidence_value): + """ + This method will transform an integer value into the DNI scale string + representation. + + The scale for this confidence representation is the following: + + .. list-table:: STIX Confidence to DNI Scale + :header-rows: 1 + + * - Range of Values + - DNI Scale + * - 0-9 + - Almost No Chance / Remote + * - 10-19 + - Very Unlikely / Highly Improbable + * - 20-39 + - Unlikely / Improbable + * - 40-59 + - Roughly Even Chance / Roughly Even Odds + * - 60-79 + - Likely / Probable + * - 80-89 + - Very Likely / Highly Probable + * - 90-100 + - Almost Certain / Nearly Certain + + Args: + confidence_value (int): An integer value between 0 and 100. + + Returns: + str: A string corresponding to the DNI scale. + + Raises: + ValueError: If `confidence_value` is out of bounds. + + """ + if 9 >= confidence_value >= 0: + return 'Almost No Chance / Remote' + elif 19 >= confidence_value >= 10: + return 'Very Unlikely / Highly Improbable' + elif 39 >= confidence_value >= 20: + return 'Unlikely / Improbable' + elif 59 >= confidence_value >= 40: + return 'Roughly Even Chance / Roughly Even Odds' + elif 79 >= confidence_value >= 60: + return 'Likely / Probable' + elif 89 >= confidence_value >= 80: + return 'Very Likely / Highly Probable' + elif 100 >= confidence_value >= 90: + return 'Almost Certain / Nearly Certain' + else: + raise ValueError("Range of values out of bounds: %s" % confidence_value) diff --git a/stix2/core.py b/stix2/core.py index af74a9e..830d98c 100644 --- a/stix2/core.py +++ b/stix2/core.py @@ -1,76 +1,28 @@ -"""STIX 2.0 Objects that are neither SDOs nor SROs.""" +"""STIX2 Core Objects and Methods.""" -from collections import OrderedDict +import copy import importlib import pkgutil +import re import stix2 -from . import exceptions from .base import _STIXBase -from .properties import IDProperty, ListProperty, Property, TypeProperty -from .utils import _get_dict, get_class_hierarchy_names - - -class STIXObjectProperty(Property): - - def __init__(self, allow_custom=False, *args, **kwargs): - self.allow_custom = allow_custom - super(STIXObjectProperty, self).__init__(*args, **kwargs) - - def clean(self, value): - # Any STIX Object (SDO, SRO, or Marking Definition) can be added to - # a bundle with no further checks. - if any(x in ('STIXDomainObject', 'STIXRelationshipObject', 'MarkingDefinition') - for x in get_class_hierarchy_names(value)): - return value - try: - dictified = _get_dict(value) - except ValueError: - raise ValueError("This property may only contain a dictionary or object") - if dictified == {}: - raise ValueError("This property may only contain a non-empty dictionary or object") - if 'type' in dictified and dictified['type'] == 'bundle': - raise ValueError('This property may not contain a Bundle object') - - if self.allow_custom: - parsed_obj = parse(dictified, allow_custom=True) - else: - parsed_obj = parse(dictified) - return parsed_obj - - -class Bundle(_STIXBase): - """For more detailed information on this object's properties, see - `the STIX 2.0 specification `__. - """ - - _type = 'bundle' - _properties = OrderedDict() - _properties.update([ - ('type', TypeProperty(_type)), - ('id', IDProperty(_type)), - ('spec_version', Property(fixed="2.0")), - ('objects', ListProperty(STIXObjectProperty)), - ]) - - def __init__(self, *args, **kwargs): - # Add any positional arguments to the 'objects' kwarg. - if args: - if isinstance(args[0], list): - kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', []) - else: - kwargs['objects'] = list(args) + kwargs.get('objects', []) - - self.__allow_custom = kwargs.get('allow_custom', False) - self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False) - - super(Bundle, self).__init__(**kwargs) - +from .exceptions import CustomContentError, ParseError +from .markings import _MarkingsMixin +from .utils import _get_dict STIX2_OBJ_MAPS = {} +class STIXDomainObject(_STIXBase, _MarkingsMixin): + pass + + +class STIXRelationshipObject(_STIXBase, _MarkingsMixin): + pass + + def parse(data, allow_custom=False, version=None): """Convert a string, dict or file-like object into a STIX object. @@ -79,18 +31,22 @@ def parse(data, allow_custom=False, version=None): allow_custom (bool): Whether to allow custom properties as well unknown custom objects. Note that unknown custom objects cannot be parsed into STIX objects, and will be returned as is. Default: False. - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. If none of the above are + possible, it will use the default version specified by the library. Returns: An instantiated Python STIX object. - WARNING: 'allow_custom=True' will allow for the return of any supplied STIX - dict(s) that cannot be found to map to any known STIX object types (both STIX2 - domain objects or defined custom STIX2 objects); NO validation is done. This is - done to allow the processing of possibly unknown custom STIX objects (example - scenario: I need to query a third-party TAXII endpoint that could provide custom - STIX objects that I dont know about ahead of time) + Warnings: + 'allow_custom=True' will allow for the return of any supplied STIX + dict(s) that cannot be found to map to any known STIX object types + (both STIX2 domain objects or defined custom STIX2 objects); NO + validation is done. This is done to allow the processing of possibly + unknown custom STIX objects (example scenario: I need to query a + third-party TAXII endpoint that could provide custom STIX objects that + I don't know about ahead of time) """ # convert STIX object to dict, if not already @@ -105,35 +61,55 @@ def parse(data, allow_custom=False, version=None): def dict_to_stix2(stix_dict, allow_custom=False, version=None): """convert dictionary to full python-stix2 object - Args: - stix_dict (dict): a python dictionary of a STIX object - that (presumably) is semantically correct to be parsed - into a full python-stix2 obj - allow_custom (bool): Whether to allow custom properties as well unknown - custom objects. Note that unknown custom objects cannot be parsed - into STIX objects, and will be returned as is. Default: False. + Args: + stix_dict (dict): a python dictionary of a STIX object + that (presumably) is semantically correct to be parsed + into a full python-stix2 obj + allow_custom (bool): Whether to allow custom properties as well + unknown custom objects. Note that unknown custom objects cannot + be parsed into STIX objects, and will be returned as is. + Default: False. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. If none of the above are + possible, it will use the default version specified by the library. - Returns: - An instantiated Python STIX object + Returns: + An instantiated Python STIX object - WARNING: 'allow_custom=True' will allow for the return of any supplied STIX - dict(s) that cannot be found to map to any known STIX object types (both STIX2 - domain objects or defined custom STIX2 objects); NO validation is done. This is - done to allow the processing of possibly unknown custom STIX objects (example - scenario: I need to query a third-party TAXII endpoint that could provide custom - STIX objects that I dont know about ahead of time) + Warnings: + 'allow_custom=True' will allow for the return of any supplied STIX + dict(s) that cannot be found to map to any known STIX object types + (both STIX2 domain objects or defined custom STIX2 objects); NO + validation is done. This is done to allow the processing of + possibly unknown custom STIX objects (example scenario: I need to + query a third-party TAXII endpoint that could provide custom STIX + objects that I don't know about ahead of time) """ - if not version: - # Use latest version - v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') - else: - v = 'v' + version.replace('.', '') - - OBJ_MAP = STIX2_OBJ_MAPS[v] - if 'type' not in stix_dict: - raise exceptions.ParseError("Can't parse object with no 'type' property: %s" % str(stix_dict)) + raise ParseError("Can't parse object with no 'type' property: %s" % str(stix_dict)) + + if version: + # If the version argument was passed, override other approaches. + v = 'v' + version.replace('.', '') + elif 'spec_version' in stix_dict: + # For STIX 2.0, applies to bundles only. + # For STIX 2.1+, applies to SDOs, SROs, and markings only. + v = 'v' + stix_dict['spec_version'].replace('.', '') + elif stix_dict['type'] == 'bundle': + # bundles without spec_version are ambiguous. + if any('spec_version' in x for x in stix_dict['objects']): + # Only on 2.1 we are allowed to have 'spec_version' in SDOs/SROs. + v = 'v21' + else: + v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') + else: + # The spec says that SDO/SROs without spec_version will default to a + # '2.0' representation. + v = 'v20' + + OBJ_MAP = STIX2_OBJ_MAPS[v]['objects'] try: obj_class = OBJ_MAP[stix_dict['type']] @@ -142,39 +118,187 @@ def dict_to_stix2(stix_dict, allow_custom=False, version=None): # flag allows for unknown custom objects too, but will not # be parsed into STIX object, returned as is return stix_dict - raise exceptions.ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % stix_dict['type']) + raise ParseError("Can't parse unknown object type '%s'! For custom types, use the CustomObject decorator." % stix_dict['type']) return obj_class(allow_custom=allow_custom, **stix_dict) -def _register_type(new_type, version=None): +def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): + """Deserialize a string or file-like object into a STIX Cyber Observable + object. + + Args: + data (str, dict, file-like object): The STIX2 content to be parsed. + _valid_refs: A list of object references valid for the scope of the + object being parsed. Use empty list if no valid refs are present. + allow_custom (bool): Whether to allow custom properties or not. + Default: False. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the default version specified by the library + will be used. + + Returns: + An instantiated Python STIX Cyber Observable object. + + """ + obj = _get_dict(data) + # get deep copy since we are going modify the dict and might + # modify the original dict as _get_dict() does not return new + # dict when passed a dict + obj = copy.deepcopy(obj) + + obj['_valid_refs'] = _valid_refs or [] + + if version: + # If the version argument was passed, override other approaches. + v = 'v' + version.replace('.', '') + else: + # Use default version (latest) if no version was provided. + v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') + + if 'type' not in obj: + raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj)) + try: + OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables'] + obj_class = OBJ_MAP_OBSERVABLE[obj['type']] + except KeyError: + if allow_custom: + # flag allows for unknown custom objects too, but will not + # be parsed into STIX observable object, just returned as is + return obj + raise CustomContentError("Can't parse unknown observable type '%s'! For custom observables, " + "use the CustomObservable decorator." % obj['type']) + + EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions'] + + if 'extensions' in obj and obj['type'] in EXT_MAP: + for name, ext in obj['extensions'].items(): + try: + ext_class = EXT_MAP[obj['type']][name] + except KeyError: + if not allow_custom: + raise CustomContentError("Can't parse unknown extension type '%s'" + "for observable type '%s'!" % (name, obj['type'])) + else: # extension was found + obj['extensions'][name] = ext_class(allow_custom=allow_custom, **obj['extensions'][name]) + + return obj_class(allow_custom=allow_custom, **obj) + + +def _register_object(new_type, version=None): """Register a custom STIX Object type. Args: new_type (class): A class to register in the Object map. version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If None, use latest version. - """ - if not version: - # Use latest version - v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') - else: - v = 'v' + version.replace('.', '') - OBJ_MAP = STIX2_OBJ_MAPS[v] + """ + if version: + v = 'v' + version.replace('.', '') + else: + # Use default version (latest) if no version was provided. + v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') + + OBJ_MAP = STIX2_OBJ_MAPS[v]['objects'] OBJ_MAP[new_type._type] = new_type -def _collect_stix2_obj_maps(): - """Navigate the package once and retrieve all OBJ_MAP dicts for each v2X - package.""" +def _register_marking(new_marking, version=None): + """Register a custom STIX Marking Definition type. + + Args: + new_marking (class): A class to register in the Marking map. + version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If + None, use latest version. + + """ + if version: + v = 'v' + version.replace('.', '') + else: + # Use default version (latest) if no version was provided. + v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') + + OBJ_MAP_MARKING = STIX2_OBJ_MAPS[v]['markings'] + OBJ_MAP_MARKING[new_marking._type] = new_marking + + +def _register_observable(new_observable, version=None): + """Register a custom STIX Cyber Observable type. + + Args: + new_observable (class): A class to register in the Observables map. + version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If + None, use latest version. + + """ + if version: + v = 'v' + version.replace('.', '') + else: + # Use default version (latest) if no version was provided. + v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') + + OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables'] + OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable + + +def _register_observable_extension(observable, new_extension, version=None): + """Register a custom extension to a STIX Cyber Observable type. + + Args: + observable: An observable object + new_extension (class): A class to register in the Observables + Extensions map. + version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If + None, use latest version. + + """ + if version: + v = 'v' + version.replace('.', '') + else: + # Use default version (latest) if no version was provided. + v = 'v' + stix2.DEFAULT_VERSION.replace('.', '') + + try: + observable_type = observable._type + except AttributeError: + raise ValueError( + "Unknown observable type. Custom observables must be " + "created with the @CustomObservable decorator.", + ) + + OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables'] + EXT_MAP = STIX2_OBJ_MAPS[v]['observable-extensions'] + + try: + EXT_MAP[observable_type][new_extension._type] = new_extension + except KeyError: + if observable_type not in OBJ_MAP_OBSERVABLE: + raise ValueError( + "Unknown observable type '%s'. Custom observables " + "must be created with the @CustomObservable decorator." + % observable_type, + ) + else: + EXT_MAP[observable_type] = {new_extension._type: new_extension} + + +def _collect_stix2_mappings(): + """Navigate the package once and retrieve all object mapping dicts for each + v2X package. Includes OBJ_MAP, OBJ_MAP_OBSERVABLE, EXT_MAP.""" if not STIX2_OBJ_MAPS: top_level_module = importlib.import_module('stix2') path = top_level_module.__path__ prefix = str(top_level_module.__name__) + '.' - for module_loader, name, is_pkg in pkgutil.walk_packages(path=path, - prefix=prefix): - if name.startswith('stix2.v2') and is_pkg: + for module_loader, name, is_pkg in pkgutil.walk_packages(path=path, prefix=prefix): + ver = name.split('.')[1] + if re.match(r'^stix2\.v2[0-9]$', name) and is_pkg: mod = importlib.import_module(name, str(top_level_module.__name__)) - STIX2_OBJ_MAPS[name.split('.')[-1]] = mod.OBJ_MAP + STIX2_OBJ_MAPS[ver] = {} + STIX2_OBJ_MAPS[ver]['objects'] = mod.OBJ_MAP + STIX2_OBJ_MAPS[ver]['observables'] = mod.OBJ_MAP_OBSERVABLE + STIX2_OBJ_MAPS[ver]['observable-extensions'] = mod.EXT_MAP + elif re.match(r'^stix2\.v2[0-9]\.common$', name) and is_pkg is False: + mod = importlib.import_module(name, str(top_level_module.__name__)) + STIX2_OBJ_MAPS[ver]['markings'] = mod.OBJ_MAP_MARKING diff --git a/stix2/custom.py b/stix2/custom.py new file mode 100644 index 0000000..484cbb0 --- /dev/null +++ b/stix2/custom.py @@ -0,0 +1,120 @@ +from collections import OrderedDict +import re + +from .base import _cls_init, _Extension, _Observable, _STIXBase +from .core import ( + STIXDomainObject, _register_marking, _register_object, + _register_observable, _register_observable_extension, +) +from .utils import TYPE_REGEX, get_class_hierarchy_names + + +def _custom_object_builder(cls, type, properties, version): + class _CustomObject(cls, STIXDomainObject): + + if not re.match(TYPE_REGEX, type): + raise ValueError( + "Invalid type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type, + ) + elif len(type) < 3 or len(type) > 250: + raise ValueError( + "Invalid type name '%s': must be between 3 and 250 characters." % type, + ) + + if not properties or not isinstance(properties, list): + raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") + + _type = type + _properties = OrderedDict(properties) + + def __init__(self, **kwargs): + _STIXBase.__init__(self, **kwargs) + _cls_init(cls, self, kwargs) + + _register_object(_CustomObject, version=version) + return _CustomObject + + +def _custom_marking_builder(cls, type, properties, version): + class _CustomMarking(cls, _STIXBase): + + if not properties or not isinstance(properties, list): + raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") + + _type = type + _properties = OrderedDict(properties) + + def __init__(self, **kwargs): + _STIXBase.__init__(self, **kwargs) + _cls_init(cls, self, kwargs) + + _register_marking(_CustomMarking, version=version) + return _CustomMarking + + +def _custom_observable_builder(cls, type, properties, version): + class _CustomObservable(cls, _Observable): + + if not re.match(TYPE_REGEX, type): + raise ValueError( + "Invalid observable type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type, + ) + elif len(type) < 3 or len(type) > 250: + raise ValueError("Invalid observable type name '%s': must be between 3 and 250 characters." % type) + + if not properties or not isinstance(properties, list): + raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") + + # Check properties ending in "_ref/s" are ObjectReferenceProperties + for prop_name, prop in properties: + if prop_name.endswith('_ref') and ('ObjectReferenceProperty' not in get_class_hierarchy_names(prop)): + raise ValueError( + "'%s' is named like an object reference property but " + "is not an ObjectReferenceProperty." % prop_name, + ) + elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) + or 'ObjectReferenceProperty' not in get_class_hierarchy_names(prop.contained))): + raise ValueError( + "'%s' is named like an object reference list property but " + "is not a ListProperty containing ObjectReferenceProperty." % prop_name, + ) + + _type = type + _properties = OrderedDict(properties) + + def __init__(self, **kwargs): + _Observable.__init__(self, **kwargs) + _cls_init(cls, self, kwargs) + + _register_observable(_CustomObservable, version=version) + return _CustomObservable + + +def _custom_extension_builder(cls, observable, type, properties, version): + if not observable or not issubclass(observable, _Observable): + raise ValueError("'observable' must be a valid Observable class!") + + class _CustomExtension(cls, _Extension): + + if not re.match(TYPE_REGEX, type): + raise ValueError( + "Invalid extension type name '%s': must only contain the " + "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type, + ) + elif len(type) < 3 or len(type) > 250: + raise ValueError("Invalid extension type name '%s': must be between 3 and 250 characters." % type) + + if not properties or not isinstance(properties, list): + raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") + + _type = type + _properties = OrderedDict(properties) + + def __init__(self, **kwargs): + _Extension.__init__(self, **kwargs) + _cls_init(cls, self, kwargs) + + _register_observable_extension(observable, _CustomExtension, version=version) + return _CustomExtension diff --git a/stix2/datastore/__init__.py b/stix2/datastore/__init__.py index c2963e2..561fe9e 100644 --- a/stix2/datastore/__init__.py +++ b/stix2/datastore/__init__.py @@ -1,4 +1,5 @@ -"""Python STIX 2.0 DataStore API. +""" +Python STIX2 DataStore API. .. autosummary:: :toctree: datastore @@ -83,7 +84,8 @@ class DataStoreMixin(object): try: return self.source.get(*args, **kwargs) except AttributeError: - raise AttributeError('%s has no data source to query' % self.__class__.__name__) + msg = "%s has no data source to query" + raise AttributeError(msg % self.__class__.__name__) def all_versions(self, *args, **kwargs): """Retrieve all versions of a single STIX object by ID. @@ -100,7 +102,8 @@ class DataStoreMixin(object): try: return self.source.all_versions(*args, **kwargs) except AttributeError: - raise AttributeError('%s has no data source to query' % self.__class__.__name__) + msg = "%s has no data source to query" + raise AttributeError(msg % self.__class__.__name__) def query(self, *args, **kwargs): """Retrieve STIX objects matching a set of filters. @@ -118,7 +121,8 @@ class DataStoreMixin(object): try: return self.source.query(*args, **kwargs) except AttributeError: - raise AttributeError('%s has no data source to query' % self.__class__.__name__) + msg = "%s has no data source to query" + raise AttributeError(msg % self.__class__.__name__) def creator_of(self, *args, **kwargs): """Retrieve the Identity refered to by the object's `created_by_ref`. @@ -137,7 +141,8 @@ class DataStoreMixin(object): try: return self.source.creator_of(*args, **kwargs) except AttributeError: - raise AttributeError('%s has no data source to query' % self.__class__.__name__) + msg = "%s has no data source to query" + raise AttributeError(msg % self.__class__.__name__) def relationships(self, *args, **kwargs): """Retrieve Relationships involving the given STIX object. @@ -163,7 +168,8 @@ class DataStoreMixin(object): try: return self.source.relationships(*args, **kwargs) except AttributeError: - raise AttributeError('%s has no data source to query' % self.__class__.__name__) + msg = "%s has no data source to query" + raise AttributeError(msg % self.__class__.__name__) def related_to(self, *args, **kwargs): """Retrieve STIX Objects that have a Relationship involving the given @@ -193,7 +199,8 @@ class DataStoreMixin(object): try: return self.source.related_to(*args, **kwargs) except AttributeError: - raise AttributeError('%s has no data source to query' % self.__class__.__name__) + msg = "%s has no data source to query" + raise AttributeError(msg % self.__class__.__name__) def add(self, *args, **kwargs): """Method for storing STIX objects. @@ -208,7 +215,8 @@ class DataStoreMixin(object): try: return self.sink.add(*args, **kwargs) except AttributeError: - raise AttributeError('%s has no data sink to put objects in' % self.__class__.__name__) + msg = "%s has no data sink to put objects in" + raise AttributeError(msg % self.__class__.__name__) class DataSink(with_metaclass(ABCMeta)): @@ -301,7 +309,7 @@ class DataSource(with_metaclass(ABCMeta)): """ def creator_of(self, obj): - """Retrieve the Identity refered to by the object's `created_by_ref`. + """Retrieve the Identity referred to by the object's `created_by_ref`. Args: obj: The STIX object whose `created_by_ref` property will be looked @@ -457,7 +465,7 @@ class CompositeDataSource(DataSource): """ if not self.has_data_sources(): - raise AttributeError('CompositeDataSource has no data sources') + raise AttributeError("CompositeDataSource has no data sources") all_data = [] all_filters = FilterSet() @@ -504,7 +512,7 @@ class CompositeDataSource(DataSource): """ if not self.has_data_sources(): - raise AttributeError('CompositeDataSource has no data sources') + raise AttributeError("CompositeDataSource has no data sources") all_data = [] all_filters = FilterSet() @@ -543,7 +551,7 @@ class CompositeDataSource(DataSource): """ if not self.has_data_sources(): - raise AttributeError('CompositeDataSource has no data sources') + raise AttributeError("CompositeDataSource has no data sources") if not query: # don't mess with the query (i.e. deduplicate, as that's done @@ -594,7 +602,7 @@ class CompositeDataSource(DataSource): """ if not self.has_data_sources(): - raise AttributeError('CompositeDataSource has no data sources') + raise AttributeError("CompositeDataSource has no data sources") results = [] for ds in self.data_sources: @@ -634,7 +642,7 @@ class CompositeDataSource(DataSource): """ if not self.has_data_sources(): - raise AttributeError('CompositeDataSource has no data sources') + raise AttributeError("CompositeDataSource has no data sources") results = [] for ds in self.data_sources: diff --git a/stix2/datastore/filesystem.py b/stix2/datastore/filesystem.py index ac1bedc..b4f3d15 100644 --- a/stix2/datastore/filesystem.py +++ b/stix2/datastore/filesystem.py @@ -1,21 +1,23 @@ -""" -Python STIX 2.0 FileSystem Source/Sink - -""" +"""Python STIX2 FileSystem Source/Sink""" +# Temporary while we address TODO statement +from __future__ import print_function import errno +import io import json import os +import re import stat +import sys -import pytz import six +from stix2 import v20, v21 from stix2.base import _STIXBase -from stix2.core import Bundle, parse +from stix2.core import parse from stix2.datastore import DataSink, DataSource, DataStoreMixin from stix2.datastore.filters import Filter, FilterSet, apply_common_filters -from stix2.utils import get_type_from_id, is_marking +from stix2.utils import format_datetime, get_type_from_id, is_marking def _timestamp2filename(timestamp): @@ -23,15 +25,14 @@ def _timestamp2filename(timestamp): Encapsulates a way to create unique filenames based on an object's "modified" property value. This should not include an extension. - :param timestamp: A timestamp, as a datetime.datetime object. - """ - # Different times will only produce different file names if all timestamps - # are in the same time zone! So if timestamp is timezone-aware convert - # to UTC just to be safe. If naive, just use as-is. - if timestamp.tzinfo is not None: - timestamp = timestamp.astimezone(pytz.utc) + Args: + timestamp: A timestamp, as a datetime.datetime object. - return timestamp.strftime("%Y%m%d%H%M%S%f") + """ + # The format_datetime will determine the correct level of precision. + ts = format_datetime(timestamp) + ts = re.sub(r"[-T:\.Z ]", "", ts) + return ts class AuthSet(object): @@ -45,8 +46,8 @@ class AuthSet(object): anywhere, which means the query was impossible to match, so you can skip searching altogether. For a blacklist, this means nothing is excluded and you must search everywhere. - """ + """ BLACK = 0 WHITE = 1 @@ -56,9 +57,11 @@ class AuthSet(object): prohibited values. The type of set (black or white) is determined from the allowed and/or prohibited values given. - :param allowed: A set of allowed values (or None if no allow filters - were found in the query) - :param prohibited: A set of prohibited values (not None) + Args: + allowed: A set of allowed values (or None if no allow filters + were found in the query) + prohibited: A set of prohibited values (not None) + """ if allowed is None: self.__values = prohibited @@ -88,7 +91,7 @@ class AuthSet(object): def __repr__(self): return "{}list: {}".format( "white" if self.auth_type == AuthSet.WHITE else "black", - self.values + self.values, ) @@ -103,9 +106,13 @@ def _update_allow(allow_set, value): implicitly AND'd, the given values are intersected with the existing allow set, which may remove values. At the end, it may even wind up empty. - :param allow_set: The allow set, or None - :param value: The value(s) to add (single value, or iterable of values) - :return: The updated allow set (not None) + Args: + allow_set: The allow set, or None + value: The value(s) to add (single value, or iterable of values) + + Returns: + The updated allow set (not None) + """ adding_seq = hasattr(value, "__iter__") and \ not isinstance(value, six.string_types) @@ -116,7 +123,6 @@ def _update_allow(allow_set, value): allow_set.update(value) else: allow_set.add(value) - else: # strangely, the "&=" operator requires a set on the RHS # whereas the method allows any iterable. @@ -133,11 +139,14 @@ def _find_search_optimizations(filters): Searches through all the filters, and creates white/blacklists of types and IDs, which can be used to optimize the filesystem search. - :param filters: An iterable of filter objects representing a query - :return: A 2-tuple of AuthSet objects: the first is for object types, and - the second is for object IDs. - """ + Args: + filters: An iterable of filter objects representing a query + Returns: + A 2-tuple of AuthSet objects: the first is for object types, and + the second is for object IDs. + + """ # The basic approach to this is to determine what is allowed and # prohibited, independently, and then combine them to create the final # white/blacklists. @@ -158,15 +167,19 @@ def _find_search_optimizations(filters): # An "allow" ID filter implies a type filter too, since IDs # contain types within them. allowed_ids = _update_allow(allowed_ids, filter_.value) - allowed_types = _update_allow(allowed_types, - get_type_from_id(filter_.value)) + allowed_types = _update_allow( + allowed_types, + get_type_from_id(filter_.value), + ) elif filter_.op == "!=": prohibited_ids.add(filter_.value) elif filter_.op == "in": allowed_ids = _update_allow(allowed_ids, filter_.value) - allowed_types = _update_allow(allowed_types, ( - get_type_from_id(id_) for id_ in filter_.value - )) + allowed_types = _update_allow( + allowed_types, ( + get_type_from_id(id_) for id_ in filter_.value + ), + ) opt_types = AuthSet(allowed_types, prohibited_types) opt_ids = AuthSet(allowed_ids, prohibited_ids) @@ -196,30 +209,35 @@ def _get_matching_dir_entries(parent_dir, auth_set, st_mode_test=None, ext=""): Search a directory (non-recursively), and find entries which match the given criteria. - :param parent_dir: The directory to search - :param auth_set: an AuthSet instance, which represents a black/whitelist - filter on filenames - :param st_mode_test: A callable allowing filtering based on the type of - directory entry. E.g. just get directories, or just get files. It - will be passed the st_mode field of a stat() structure and should - return True to include the file, or False to exclude it. Easy thing to - do is pass one of the stat module functions, e.g. stat.S_ISREG. If - None, don't filter based on entry type. - :param ext: Determines how names from auth_set match up to directory - entries, and allows filtering by extension. The extension is added - to auth_set values to obtain directory entries; it is removed from - directory entries to obtain auth_set values. In this way, auth_set - may be treated as having only "basenames" of the entries. Only entries - having the given extension will be included in the results. If not - empty, the extension MUST include a leading ".". The default is the - empty string, which will result in direct comparisons, and no - extension-based filtering. - :return: A list of directory entries matching the criteria. These will not - have any path info included; they will just be bare names. - :raises OSError: If there are errors accessing directory contents or - stat()'ing files - """ + Args: + parent_dir: The directory to search + auth_set: an AuthSet instance, which represents a black/whitelist + filter on filenames + st_mode_test: A callable allowing filtering based on the type of + directory entry. E.g. just get directories, or just get files. It + will be passed the st_mode field of a stat() structure and should + return True to include the file, or False to exclude it. Easy thing to + do is pass one of the stat module functions, e.g. stat.S_ISREG. If + None, don't filter based on entry type. + ext: Determines how names from auth_set match up to directory + entries, and allows filtering by extension. The extension is added + to auth_set values to obtain directory entries; it is removed from + directory entries to obtain auth_set values. In this way, auth_set + may be treated as having only "basenames" of the entries. Only entries + having the given extension will be included in the results. If not + empty, the extension MUST include a leading ".". The default is the + empty string, which will result in direct comparisons, and no + extension-based filtering. + Returns: + (list): A list of directory entries matching the criteria. These will not + have any path info included; they will just be bare names. + + Raises: + OSError: If there are errors accessing directory contents or stat()'ing + files + + """ results = [] if auth_set.auth_type == AuthSet.WHITE: for value in auth_set.values: @@ -237,7 +255,6 @@ def _get_matching_dir_entries(parent_dir, auth_set, st_mode_test=None, ext=""): if e.errno != errno.ENOENT: raise # else, file-not-found is ok, just skip - else: # auth_set is a blacklist for entry in os.listdir(parent_dir): if ext: @@ -272,28 +289,34 @@ def _check_object_from_file(query, filepath, allow_custom, version): Read a STIX object from the given file, and check it against the given filters. - :param query: Iterable of filters - :param filepath: Path to file to read - :param allow_custom: Whether to allow custom properties as well unknown + Args: + query: Iterable of filters + filepath: Path to file to read + allow_custom: Whether to allow custom properties as well unknown custom objects. - :param version: Which STIX2 version to use. (e.g. "2.0", "2.1"). If None, - use latest version. - :return: The (parsed) STIX object, if the object passes the filters. If + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. + + Returns: + The (parsed) STIX object, if the object passes the filters. If not, None is returned. - :raises TypeError: If the file had invalid JSON - :raises IOError: If there are problems opening/reading the file - :raises stix2.exceptions.STIXError: If there were problems creating a STIX - object from the JSON + + Raises: + TypeError: If the file had invalid JSON + IOError: If there are problems opening/reading the file + stix2.exceptions.STIXError: If there were problems creating a STIX + object from the JSON + """ try: - with open(filepath, "r") as f: + with io.open(filepath, "r") as f: stix_json = json.load(f) - except ValueError: # not a JSON file raise TypeError( "STIX JSON object at '{0}' could either not be parsed " - "to JSON or was not valid STIX JSON".format( - filepath)) + "to JSON or was not valid STIX JSON".format(filepath), + ) stix_obj = parse(stix_json, allow_custom, version) @@ -312,35 +335,49 @@ def _search_versioned(query, type_path, auth_ids, allow_custom, version): particular versioned type (i.e. not markings), and return any which match the query. - :param query: The query to match against - :param type_path: The directory with type-specific STIX object files - :param auth_ids: Search optimization based on object ID - :param allow_custom: Whether to allow custom properties as well unknown - custom objects. - :param version: Which STIX2 version to use. (e.g. "2.0", "2.1"). If None, - use latest version. - :return: A list of all matching objects - :raises TypeError, stix2.exceptions.STIXError: If any objects had invalid - content - :raises IOError, OSError: If there were any problems opening/reading files + Args: + query: The query to match against + type_path: The directory with type-specific STIX object files + auth_ids: Search optimization based on object ID + allow_custom: Whether to allow custom properties as well unknown + custom objects. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. + + Returns: + A list of all matching objects + + Raises: + stix2.exceptions.STIXError: If any objects had invalid content + TypeError: If any objects had invalid content + IOError: If there were any problems opening/reading files + OSError: If there were any problems opening/reading files + """ results = [] - id_dirs = _get_matching_dir_entries(type_path, auth_ids, - stat.S_ISDIR) + id_dirs = _get_matching_dir_entries( + type_path, auth_ids, + stat.S_ISDIR, + ) for id_dir in id_dirs: id_path = os.path.join(type_path, id_dir) # This leverages a more sophisticated function to do a simple thing: # get all the JSON files from a directory. I guess it does give us # file type checking, ensuring we only get regular files. - version_files = _get_matching_dir_entries(id_path, _AUTHSET_ANY, - stat.S_ISREG, ".json") + version_files = _get_matching_dir_entries( + id_path, _AUTHSET_ANY, + stat.S_ISREG, ".json", + ) for version_file in version_files: version_path = os.path.join(id_path, version_file) try: - stix_obj = _check_object_from_file(query, version_path, - allow_custom, version) + stix_obj = _check_object_from_file( + query, version_path, + allow_custom, version, + ) if stix_obj: results.append(stix_obj) except IOError as e: @@ -350,14 +387,18 @@ def _search_versioned(query, type_path, auth_ids, allow_custom, version): # For backward-compatibility, also search for plain files named after # object IDs, in the type directory. - id_files = _get_matching_dir_entries(type_path, auth_ids, stat.S_ISREG, - ".json") + id_files = _get_matching_dir_entries( + type_path, auth_ids, stat.S_ISREG, + ".json", + ) for id_file in id_files: id_path = os.path.join(type_path, id_file) try: - stix_obj = _check_object_from_file(query, id_path, allow_custom, - version) + stix_obj = _check_object_from_file( + query, id_path, allow_custom, + version, + ) if stix_obj: results.append(stix_obj) except IOError as e: @@ -373,27 +414,39 @@ def _search_markings(query, markings_path, auth_ids, allow_custom, version): Searches the given directory, which contains markings data, and return any which match the query. - :param query: The query to match against - :param markings_path: The directory with STIX markings files - :param auth_ids: Search optimization based on object ID - :param allow_custom: Whether to allow custom properties as well unknown - custom objects. - :param version: Which STIX2 version to use. (e.g. "2.0", "2.1"). If None, - use latest version. - :return: A list of all matching objects - :raises TypeError, stix2.exceptions.STIXError: If any objects had invalid - content - :raises IOError, OSError: If there were any problems opening/reading files + Args: + query: The query to match against + markings_path: The directory with STIX markings files + auth_ids: Search optimization based on object ID + allow_custom: Whether to allow custom properties as well unknown + custom objects. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. + + Returns: + A list of all matching objects + + Raises: + stix2.exceptions.STIXError: If any objects had invalid content + TypeError: If any objects had invalid content + IOError: If there were any problems opening/reading files + OSError: If there were any problems opening/reading files + """ results = [] - id_files = _get_matching_dir_entries(markings_path, auth_ids, stat.S_ISREG, - ".json") + id_files = _get_matching_dir_entries( + markings_path, auth_ids, stat.S_ISREG, + ".json", + ) for id_file in id_files: id_path = os.path.join(markings_path, id_file) try: - stix_obj = _check_object_from_file(query, id_path, allow_custom, - version) + stix_obj = _check_object_from_file( + query, id_path, allow_custom, + version, + ) if stix_obj: results.append(stix_obj) except IOError as e: @@ -413,12 +466,12 @@ class FileSystemStore(DataStoreMixin): Args: stix_dir (str): path to directory of STIX objects allow_custom (bool): whether to allow custom STIX content to be - pushed/retrieved. Defaults to True for FileSystemSource side(retrieving data) - and False for FileSystemSink side(pushing data). However, when - parameter is supplied, it will be applied to both FileSystemSource - and FileSystemSink. - bundlify (bool): whether to wrap objects in bundles when saving them. - Default: False. + pushed/retrieved. Defaults to True for FileSystemSource side + (retrieving data) and False for FileSystemSink + side(pushing data). However, when parameter is supplied, it + will be applied to both FileSystemSource and FileSystemSink. + bundlify (bool): whether to wrap objects in bundles when saving + them. Default: False. Attributes: source (FileSystemSource): FileSystemSource @@ -434,7 +487,7 @@ class FileSystemStore(DataStoreMixin): super(FileSystemStore, self).__init__( source=FileSystemSource(stix_dir=stix_dir, allow_custom=allow_custom_source), - sink=FileSystemSink(stix_dir=stix_dir, allow_custom=allow_custom_sink, bundlify=bundlify) + sink=FileSystemSink(stix_dir=stix_dir, allow_custom=allow_custom_sink, bundlify=bundlify), ) @@ -466,7 +519,7 @@ class FileSystemSink(DataSink): def stix_dir(self): return self._stix_dir - def _check_path_and_write(self, stix_obj): + def _check_path_and_write(self, stix_obj, encoding='utf-8'): """Write the given STIX object to a file in the STIX file directory. """ type_dir = os.path.join(self._stix_dir, stix_obj["type"]) @@ -483,10 +536,21 @@ class FileSystemSink(DataSink): os.makedirs(obj_dir) if self.bundlify: - stix_obj = Bundle(stix_obj, allow_custom=self.allow_custom) + if 'spec_version' in stix_obj: + # Assuming future specs will allow multiple SDO/SROs + # versions in a single bundle we won't need to check this + # and just use the latest supported Bundle version. + stix_obj = v21.Bundle(stix_obj, allow_custom=self.allow_custom) + else: + stix_obj = v20.Bundle(stix_obj, allow_custom=self.allow_custom) - with open(file_path, "w") as f: - f.write(str(stix_obj)) + # TODO: Better handling of the overwriting case. + if os.path.isfile(file_path): + print("Attempted to overwrite file!", file_path, file=sys.stderr) + else: + with io.open(file_path, 'w', encoding=encoding) as f: + stix_obj = stix_obj.serialize(pretty=True, encoding=encoding, ensure_ascii=False) + f.write(stix_obj) def add(self, stix_data=None, version=None): """Add STIX objects to file directory. @@ -495,8 +559,9 @@ class FileSystemSink(DataSink): stix_data (STIX object OR dict OR str OR list): valid STIX 2.0 content in a STIX object (or list of), dict (or list of), or a STIX 2.0 json encoded string. - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. Note: ``stix_data`` can be a Bundle object, but each object in it will be @@ -504,7 +569,7 @@ class FileSystemSink(DataSink): the Bundle contained, but not the Bundle itself. """ - if isinstance(stix_data, Bundle): + if isinstance(stix_data, (v20.Bundle, v21.Bundle)): # recursively add individual STIX objects for stix_obj in stix_data.get("objects", []): self.add(stix_obj, version=version) @@ -520,12 +585,14 @@ class FileSystemSink(DataSink): elif isinstance(stix_data, list): # recursively add individual STIX objects for stix_obj in stix_data: - self.add(stix_obj, version=version) + self.add(stix_obj) else: - raise TypeError("stix_data must be a STIX object (or list of), " - "JSON formatted STIX (or list of), " - "or a JSON formatted STIX bundle") + raise TypeError( + "stix_data must be a STIX object (or list of), " + "JSON formatted STIX (or list of), " + "or a JSON formatted STIX bundle", + ) class FileSystemSource(DataSource): @@ -560,8 +627,9 @@ class FileSystemSource(DataSource): stix_id (str): The STIX ID of the STIX object to be retrieved. _composite_filters (FilterSet): collection of filters passed from the parent CompositeDataSource, not user supplied - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. Returns: (STIX object): STIX object that has the supplied STIX ID. @@ -591,10 +659,11 @@ class FileSystemSource(DataSource): Args: stix_id (str): The STIX ID of the STIX objects to be retrieved. - _composite_filters (FilterSet): collection of filters passed from the parent - CompositeDataSource, not user supplied - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + _composite_filters (FilterSet): collection of filters passed from + the parent CompositeDataSource, not user supplied + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. Returns: (list): of STIX objects that has the supplied STIX ID. @@ -614,10 +683,11 @@ class FileSystemSource(DataSource): Args: query (list): list of filters to search on - _composite_filters (FilterSet): collection of filters passed from the - CompositeDataSource, not user supplied - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + _composite_filters (FilterSet): collection of filters passed from + the CompositeDataSource, not user supplied + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. Returns: (list): list of STIX objects that matches the supplied @@ -625,9 +695,7 @@ class FileSystemSource(DataSource): parsed into a python STIX objects and then returned. """ - all_data = [] - query = FilterSet(query) # combine all query filters @@ -637,19 +705,22 @@ class FileSystemSource(DataSource): query.add(_composite_filters) auth_types, auth_ids = _find_search_optimizations(query) - - type_dirs = _get_matching_dir_entries(self._stix_dir, auth_types, - stat.S_ISDIR) + type_dirs = _get_matching_dir_entries( + self._stix_dir, auth_types, + stat.S_ISDIR, + ) for type_dir in type_dirs: type_path = os.path.join(self._stix_dir, type_dir) - if type_dir == "marking-definition": - type_results = _search_markings(query, type_path, auth_ids, - self.allow_custom, version) + type_results = _search_markings( + query, type_path, auth_ids, + self.allow_custom, version, + ) else: - type_results = _search_versioned(query, type_path, auth_ids, - self.allow_custom, version) - + type_results = _search_versioned( + query, type_path, auth_ids, + self.allow_custom, version, + ) all_data.extend(type_results) return all_data diff --git a/stix2/datastore/filters.py b/stix2/datastore/filters.py index fbcca1b..4f72b82 100644 --- a/stix2/datastore/filters.py +++ b/stix2/datastore/filters.py @@ -1,7 +1,4 @@ -""" -Filters for Python STIX 2.0 DataSources, DataSinks, DataStores - -""" +"""Filters for Python STIX2 DataSources, DataSinks, DataStores""" import collections from datetime import datetime @@ -14,8 +11,10 @@ import stix2.utils FILTER_OPS = ['=', '!=', 'in', '>', '<', '>=', '<=', 'contains'] """Supported filter value types""" -FILTER_VALUE_TYPES = (bool, dict, float, int, list, tuple, six.string_types, - datetime) +FILTER_VALUE_TYPES = ( + bool, dict, float, int, list, tuple, six.string_types, + datetime, +) def _check_filter_components(prop, op, value): @@ -38,14 +37,14 @@ def _check_filter_components(prop, op, value): # check filter value type is supported raise TypeError("Filter value of '%s' is not supported. The type must be a Python immutable type or dictionary" % type(value)) - if prop == "type" and "_" in value: + if prop == 'type' and '_' in value: # check filter where the property is type, value (type name) cannot have underscores raise ValueError("Filter for property 'type' cannot have its value '%s' include underscores" % value) return True -class Filter(collections.namedtuple("Filter", ['property', 'op', 'value'])): +class Filter(collections.namedtuple('Filter', ['property', 'op', 'value'])): """STIX 2 filters that support the querying functionality of STIX 2 DataStores and DataSources. @@ -157,7 +156,7 @@ def _check_filter(filter_, stix_obj): """ # For properties like granular_markings and external_references # need to extract the first property from the string. - prop = filter_.property.split(".")[0] + prop = filter_.property.split('.')[0] if prop not in stix_obj.keys(): # check filter "property" is in STIX object - if cant be @@ -165,9 +164,9 @@ def _check_filter(filter_, stix_obj): # (i.e. did not make it through the filter) return False - if "." in filter_.property: + if '.' in filter_.property: # Check embedded properties, from e.g. granular_markings or external_references - sub_property = filter_.property.split(".", 1)[1] + sub_property = filter_.property.split('.', 1)[1] sub_filter = filter_._replace(property=sub_property) if isinstance(stix_obj[prop], list): @@ -222,8 +221,9 @@ class FilterSet(object): Operates like set, only adding unique stix2.Filters to the FilterSet - NOTE: method designed to be very accomodating (i.e. even accepting filters=None) - as it allows for blind calls (very useful in DataStore) + Note: + method designed to be very accomodating (i.e. even accepting filters=None) + as it allows for blind calls (very useful in DataStore) Args: filters: stix2.Filter OR list of stix2.Filter OR stix2.FilterSet @@ -244,11 +244,13 @@ class FilterSet(object): def remove(self, filters=None): """Remove a Filter, list of Filters, or FilterSet from the FilterSet. - NOTE: method designed to be very accomodating (i.e. even accepting filters=None) - as it allows for blind calls (very useful in DataStore) + Note: + method designed to be very accomodating (i.e. even accepting filters=None) + as it allows for blind calls (very useful in DataStore) Args: filters: stix2.Filter OR list of stix2.Filter or stix2.FilterSet + """ if not filters: # so remove() can be called blindly, useful for diff --git a/stix2/datastore/memory.py b/stix2/datastore/memory.py index 4c91d55..eff914d 100644 --- a/stix2/datastore/memory.py +++ b/stix2/datastore/memory.py @@ -1,28 +1,33 @@ -""" -Python STIX 2.0 Memory Source/Sink -""" +"""Python STIX2 Memory Source/Sink""" +import io import itertools import json import os +from stix2 import v20, v21 from stix2.base import _STIXBase -from stix2.core import Bundle, parse +from stix2.core import parse from stix2.datastore import DataSink, DataSource, DataStoreMixin from stix2.datastore.filters import FilterSet, apply_common_filters from stix2.utils import is_marking -def _add(store, stix_data=None, allow_custom=True, version=None): +def _add(store, stix_data, allow_custom=True, version=None): """Add STIX objects to MemoryStore/Sink. Adds STIX objects to an in-memory dictionary for fast lookup. Recursive function, breaks down STIX Bundles and lists. Args: + store: A MemoryStore, MemorySink or MemorySource object. stix_data (list OR dict OR STIX object): STIX objects to be added - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + allow_custom (bool): Whether to allow custom properties as well unknown + custom objects. Note that unknown custom objects cannot be parsed + into STIX objects, and will be returned as is. Default: False. + version (str): Which STIX2 version to lock the parser to. (e.g. "2.0", + "2.1"). If None, the library makes the best effort to figure + out the spec representation of the object. """ if isinstance(stix_data, list): @@ -70,13 +75,15 @@ class _ObjectFamily(object): def add(self, obj): self.all_versions[obj["modified"]] = obj - if self.latest_version is None or \ - obj["modified"] > self.latest_version["modified"]: + if (self.latest_version is None or + obj["modified"] > self.latest_version["modified"]): self.latest_version = obj def __str__(self): - return "<<{}; latest={}>>".format(self.all_versions, - self.latest_version["modified"]) + return "<<{}; latest={}>>".format( + self.all_versions, + self.latest_version["modified"], + ) def __repr__(self): return str(self) @@ -96,8 +103,6 @@ class MemoryStore(DataStoreMixin): allow_custom (bool): whether to allow custom STIX content. Only applied when export/input functions called, i.e. load_from_file() and save_to_file(). Defaults to True. - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. Attributes: _data (dict): the in-memory dict that holds STIX objects @@ -109,19 +114,21 @@ class MemoryStore(DataStoreMixin): self._data = {} if stix_data: - _add(self, stix_data, allow_custom, version=version) + _add(self, stix_data, allow_custom, version) super(MemoryStore, self).__init__( source=MemorySource(stix_data=self._data, allow_custom=allow_custom, version=version, _store=True), - sink=MemorySink(stix_data=self._data, allow_custom=allow_custom, version=version, _store=True) + sink=MemorySink(stix_data=self._data, allow_custom=allow_custom, version=version, _store=True), ) def save_to_file(self, *args, **kwargs): """Write SITX objects from in-memory dictionary to JSON file, as a STIX - Bundle. + Bundle. If a directory is given, the Bundle 'id' will be used as + filename. Otherwise, the provided value will be used. Args: - file_path (str): file path to write STIX data to + path (str): file path to write STIX data to. + encoding (str): The file encoding. Default utf-8. """ return self.sink.save_to_file(*args, **kwargs) @@ -129,13 +136,11 @@ class MemoryStore(DataStoreMixin): def load_from_file(self, *args, **kwargs): """Load STIX data from JSON file. - File format is expected to be a single JSON - STIX object or JSON STIX bundle. + File format is expected to be a single JSON STIX object or JSON STIX + bundle. Args: - file_path (str): file path to load STIX data from - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + path (str): file path to load STIX data from """ return self.source.load_from_file(*args, **kwargs) @@ -156,6 +161,9 @@ class MemorySink(DataSink): allow_custom (bool): whether to allow custom objects/properties when exporting STIX content to file. Default: True. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. Attributes: _data (dict): the in-memory dict that holds STIX objects. @@ -171,25 +179,41 @@ class MemorySink(DataSink): else: self._data = {} if stix_data: - _add(self, stix_data, allow_custom, version=version) + _add(self, stix_data, allow_custom, version) def add(self, stix_data, version=None): _add(self, stix_data, self.allow_custom, version) add.__doc__ = _add.__doc__ - def save_to_file(self, file_path): - file_path = os.path.abspath(file_path) + def save_to_file(self, path, encoding="utf-8"): + path = os.path.abspath(path) - all_objs = itertools.chain.from_iterable( + all_objs = list(itertools.chain.from_iterable( value.all_versions.values() if isinstance(value, _ObjectFamily) else [value] for value in self._data.values() - ) + )) - if not os.path.exists(os.path.dirname(file_path)): - os.makedirs(os.path.dirname(file_path)) - with open(file_path, "w") as f: - f.write(str(Bundle(list(all_objs), allow_custom=self.allow_custom))) + if any("spec_version" in x for x in all_objs): + bundle = v21.Bundle(all_objs, allow_custom=self.allow_custom) + else: + bundle = v20.Bundle(all_objs, allow_custom=self.allow_custom) + + if path.endswith(".json"): + if not os.path.exists(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + else: + if not os.path.exists(path): + os.makedirs(path) + + # if the user only provided a directory, use the bundle id for filename + path = os.path.join(path, bundle["id"] + ".json") + + with io.open(path, "w", encoding=encoding) as f: + bundle = bundle.serialize(pretty=True, encoding=encoding, ensure_ascii=False) + f.write(bundle) + + return path save_to_file.__doc__ = MemoryStore.save_to_file.__doc__ @@ -209,6 +233,9 @@ class MemorySource(DataSource): allow_custom (bool): whether to allow custom objects/properties when importing STIX content from file. Default: True. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. Attributes: _data (dict): the in-memory dict that holds STIX objects. @@ -224,7 +251,7 @@ class MemorySource(DataSource): else: self._data = {} if stix_data: - _add(self, stix_data, allow_custom, version=version) + _add(self, stix_data, allow_custom, version) def get(self, stix_id, _composite_filters=None): """Retrieve STIX object from in-memory dict via STIX ID. @@ -251,8 +278,8 @@ class MemorySource(DataSource): all_filters = list( itertools.chain( _composite_filters or [], - self.filters - ) + self.filters, + ), ) stix_obj = next(apply_common_filters([stix_obj], all_filters), None) @@ -260,15 +287,13 @@ class MemorySource(DataSource): return stix_obj def all_versions(self, stix_id, _composite_filters=None): - """Retrieve STIX objects from in-memory dict via STIX ID, all versions of it - - Note: Since Memory sources/sinks don't handle multiple versions of a - STIX object, this operation is unnecessary. Translate call to get(). + """Retrieve STIX objects from in-memory dict via STIX ID, all versions + of it. Args: stix_id (str): The STIX ID of the STIX 2 object to retrieve. - _composite_filters (FilterSet): collection of filters passed from the parent - CompositeDataSource, not user supplied + _composite_filters (FilterSet): collection of filters passed from + the parent CompositeDataSource, not user supplied Returns: (list): list of STIX objects that have the supplied ID. @@ -289,12 +314,12 @@ class MemorySource(DataSource): all_filters = list( itertools.chain( _composite_filters or [], - self.filters - ) + self.filters, + ), ) results.extend( - apply_common_filters(stix_objs_to_filter, all_filters) + apply_common_filters(stix_objs_to_filter, all_filters), ) return results @@ -308,8 +333,8 @@ class MemorySource(DataSource): Args: query (list): list of filters to search on - _composite_filters (FilterSet): collection of filters passed from the - CompositeDataSource, not user supplied + _composite_filters (FilterSet): collection of filters passed from + the CompositeDataSource, not user supplied Returns: (list): list of STIX objects that match the supplied query. @@ -335,12 +360,8 @@ class MemorySource(DataSource): return all_data def load_from_file(self, file_path, version=None): - with open(os.path.abspath(file_path), "r") as f: + with io.open(os.path.abspath(file_path), "r") as f: stix_data = json.load(f) - # Override user version selection if loading a bundle - if stix_data["type"] == "bundle": - version = stix_data["spec_version"] - _add(self, stix_data, self.allow_custom, version) load_from_file.__doc__ = MemoryStore.load_from_file.__doc__ diff --git a/stix2/datastore/taxii.py b/stix2/datastore/taxii.py index c815e12..41c968f 100644 --- a/stix2/datastore/taxii.py +++ b/stix2/datastore/taxii.py @@ -1,12 +1,13 @@ -""" -Python STIX 2.x TAXIICollectionStore -""" +"""Python STIX2 TAXIICollection Source/Sink""" + from requests.exceptions import HTTPError +from stix2 import v20, v21 from stix2.base import _STIXBase -from stix2.core import Bundle, parse -from stix2.datastore import (DataSink, DataSource, DataSourceError, - DataStoreMixin) +from stix2.core import parse +from stix2.datastore import ( + DataSink, DataSource, DataSourceError, DataStoreMixin, +) from stix2.datastore.filters import Filter, FilterSet, apply_common_filters from stix2.utils import deduplicate @@ -43,7 +44,7 @@ class TAXIICollectionStore(DataStoreMixin): super(TAXIICollectionStore, self).__init__( source=TAXIICollectionSource(collection, allow_custom=allow_custom_source), - sink=TAXIICollectionSink(collection, allow_custom=allow_custom_sink) + sink=TAXIICollectionSink(collection, allow_custom=allow_custom_sink), ) @@ -66,12 +67,16 @@ class TAXIICollectionSink(DataSink): if collection.can_write: self.collection = collection else: - raise DataSourceError("The TAXII Collection object provided does not have write access" - " to the underlying linked Collection resource") + raise DataSourceError( + "The TAXII Collection object provided does not have write access" + " to the underlying linked Collection resource", + ) except (HTTPError, ValidationError) as e: - raise DataSourceError("The underlying TAXII Collection resource defined in the supplied TAXII" - " Collection object provided could not be reached. Receved error:", e) + raise DataSourceError( + "The underlying TAXII Collection resource defined in the supplied TAXII" + " Collection object provided could not be reached. Receved error:", e, + ) self.allow_custom = allow_custom @@ -79,26 +84,34 @@ class TAXIICollectionSink(DataSink): """Add/push STIX content to TAXII Collection endpoint Args: - stix_data (STIX object OR dict OR str OR list): valid STIX 2.0 content - in a STIX object (or Bundle), STIX onject dict (or Bundle dict), or a STIX 2.0 - json encoded string, or list of any of the following - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + stix_data (STIX object OR dict OR str OR list): valid STIX2 + content in a STIX object (or Bundle), STIX object dict (or + Bundle dict), or a STIX2 json encoded string, or list of + any of the following. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. """ if isinstance(stix_data, _STIXBase): # adding python STIX object - if stix_data["type"] == "bundle": - bundle = stix_data.serialize(encoding="utf-8") + if stix_data['type'] == 'bundle': + bundle = stix_data.serialize(encoding='utf-8', ensure_ascii=False) + elif 'spec_version' in stix_data: + # If the spec_version is present, use new Bundle object... + bundle = v21.Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding='utf-8', ensure_ascii=False) else: - bundle = Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding="utf-8") + bundle = v20.Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding='utf-8', ensure_ascii=False) elif isinstance(stix_data, dict): # adding python dict (of either Bundle or STIX obj) - if stix_data["type"] == "bundle": - bundle = parse(stix_data, allow_custom=self.allow_custom, version=version).serialize(encoding="utf-8") + if stix_data['type'] == 'bundle': + bundle = parse(stix_data, allow_custom=self.allow_custom, version=version).serialize(encoding='utf-8', ensure_ascii=False) + elif 'spec_version' in stix_data: + # If the spec_version is present, use new Bundle object... + bundle = v21.Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding='utf-8', ensure_ascii=False) else: - bundle = Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding="utf-8") + bundle = v20.Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding='utf-8', ensure_ascii=False) elif isinstance(stix_data, list): # adding list of something - recurse on each @@ -109,10 +122,13 @@ class TAXIICollectionSink(DataSink): elif isinstance(stix_data, str): # adding json encoded string of STIX content stix_data = parse(stix_data, allow_custom=self.allow_custom, version=version) - if stix_data["type"] == "bundle": - bundle = stix_data.serialize(encoding="utf-8") + if stix_data['type'] == 'bundle': + bundle = stix_data.serialize(encoding='utf-8', ensure_ascii=False) + elif 'spec_version' in stix_data: + # If the spec_version is present, use new Bundle object... + bundle = v21.Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding='utf-8', ensure_ascii=False) else: - bundle = Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding="utf-8") + bundle = v20.Bundle(stix_data, allow_custom=self.allow_custom).serialize(encoding='utf-8', ensure_ascii=False) else: raise TypeError("stix_data must be as STIX object(or list of),json formatted STIX (or list of), or a json formatted STIX bundle") @@ -139,12 +155,16 @@ class TAXIICollectionSource(DataSource): if collection.can_read: self.collection = collection else: - raise DataSourceError("The TAXII Collection object provided does not have read access" - " to the underlying linked Collection resource") + raise DataSourceError( + "The TAXII Collection object provided does not have read access" + " to the underlying linked Collection resource", + ) except (HTTPError, ValidationError) as e: - raise DataSourceError("The underlying TAXII Collection resource defined in the supplied TAXII" - " Collection object provided could not be reached. Recieved error:", e) + raise DataSourceError( + "The underlying TAXII Collection resource defined in the supplied TAXII" + " Collection object provided could not be reached. Recieved error:", e, + ) self.allow_custom = allow_custom @@ -154,10 +174,11 @@ class TAXIICollectionSource(DataSource): Args: stix_id (str): The STIX ID of the STIX object to be retrieved. - _composite_filters (FilterSet): collection of filters passed from the parent - CompositeDataSource, not user supplied - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. + _composite_filters (FilterSet): collection of filters passed from + the parent CompositeDataSource, not user supplied Returns: (STIX object): STIX object that has the supplied STIX ID. @@ -173,15 +194,16 @@ class TAXIICollectionSource(DataSource): if _composite_filters: query.add(_composite_filters) - # dont extract TAXII filters from query (to send to TAXII endpoint) - # as directly retrieveing a STIX object by ID + # don't extract TAXII filters from query (to send to TAXII endpoint) + # as directly retrieving a STIX object by ID try: - stix_objs = self.collection.get_object(stix_id)["objects"] + stix_objs = self.collection.get_object(stix_id)['objects'] stix_obj = list(apply_common_filters(stix_objs, query)) except HTTPError as e: if e.response.status_code == 404: - # if resource not found or access is denied from TAXII server, return None + # if resource not found or access is denied from TAXII server, + # return None stix_obj = [] else: raise DataSourceError("TAXII Collection resource returned error", e) @@ -202,10 +224,11 @@ class TAXIICollectionSource(DataSource): Args: stix_id (str): The STIX ID of the STIX objects to be retrieved. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. _composite_filters (FilterSet): collection of filters passed from the parent CompositeDataSource, not user supplied - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. Returns: (see query() as all_versions() is just a wrapper) @@ -213,8 +236,8 @@ class TAXIICollectionSource(DataSource): """ # make query in TAXII query format since 'id' is TAXII field query = [ - Filter("id", "=", stix_id), - Filter("version", "=", "all") + Filter('id', '=', stix_id), + Filter('version', '=', 'all'), ] all_data = self.query(query=query, _composite_filters=_composite_filters) @@ -236,10 +259,11 @@ class TAXIICollectionSource(DataSource): Args: query (list): list of filters to search on - _composite_filters (FilterSet): collection of filters passed from the - CompositeDataSource, not user supplied - version (str): Which STIX2 version to use. (e.g. "2.0", "2.1"). If - None, use latest version. + version (str): If present, it forces the parser to use the version + provided. Otherwise, the library will make the best effort based + on checking the "spec_version" property. + _composite_filters (FilterSet): collection of filters passed from + the CompositeDataSource, not user supplied Returns: (list): list of STIX objects that matches the supplied @@ -263,7 +287,7 @@ class TAXIICollectionSource(DataSource): # query TAXII collection try: - all_data = self.collection.get_objects(**taxii_filters_dict)["objects"] + all_data = self.collection.get_objects(**taxii_filters_dict)['objects'] # deduplicate data (before filtering as reduces wasted filtering) all_data = deduplicate(all_data) @@ -275,9 +299,11 @@ class TAXIICollectionSource(DataSource): except HTTPError as e: # if resources not found or access is denied from TAXII server, return empty list if e.response.status_code == 404: - raise DataSourceError("The requested STIX objects for the TAXII Collection resource defined in" - " the supplied TAXII Collection object are either not found or access is" - " denied. Received error: ", e) + raise DataSourceError( + "The requested STIX objects for the TAXII Collection resource defined in" + " the supplied TAXII Collection object are either not found or access is" + " denied. Received error: ", e, + ) # parse python STIX objects from the STIX object dicts stix_objs = [parse(stix_obj_dict, allow_custom=self.allow_custom, version=version) for stix_obj_dict in all_data] @@ -290,18 +316,17 @@ class TAXIICollectionSource(DataSource): Does not put in TAXII spec format as the TAXII2Client (that we use) does this for us. - Notes: + Note: Currently, the TAXII2Client can handle TAXII filters where the - filter value is list, as both a comma-seperated string or python list + filter value is list, as both a comma-seperated string or python + list. For instance - "?match[type]=indicator,sighting" can be in a filter in any of these formats: Filter("type", "", "indicator,sighting") - Filter("type", "", ["indicator", "sighting"]) - Args: query (list): list of filters to extract which ones are TAXII specific. diff --git a/stix2/environment.py b/stix2/environment.py index cc589ae..104fdb2 100644 --- a/stix2/environment.py +++ b/stix2/environment.py @@ -1,5 +1,4 @@ -"""Python STIX 2.0 Environment API. -""" +"""Python STIX2 Environment API.""" import copy @@ -27,9 +26,11 @@ class ObjectFactory(object): default. Defaults to True. """ - def __init__(self, created_by_ref=None, created=None, - external_references=None, object_marking_refs=None, - list_append=True): + def __init__( + self, created_by_ref=None, created=None, + external_references=None, object_marking_refs=None, + list_append=True, + ): self._defaults = {} if created_by_ref: @@ -166,3 +167,22 @@ class Environment(DataStoreMixin): def parse(self, *args, **kwargs): return _parse(*args, **kwargs) parse.__doc__ = _parse.__doc__ + + def creator_of(self, obj): + """Retrieve the Identity refered to by the object's `created_by_ref`. + + Args: + obj: The STIX object whose `created_by_ref` property will be looked + up. + + Returns: + str: The STIX object's creator, or None, if the object contains no + `created_by_ref` property or the object's creator cannot be + found. + + """ + creator_id = obj.get('created_by_ref', '') + if creator_id: + return self.get(creator_id) + else: + return None diff --git a/stix2/exceptions.py b/stix2/exceptions.py index 79c5a81..231eeb6 100644 --- a/stix2/exceptions.py +++ b/stix2/exceptions.py @@ -1,5 +1,4 @@ -"""STIX 2 error classes. -""" +"""STIX2 Error Classes.""" class STIXError(Exception): @@ -30,8 +29,10 @@ class MissingPropertiesError(STIXError, ValueError): def __str__(self): msg = "No values for required properties for {0}: ({1})." - return msg.format(self.cls.__name__, - ", ".join(x for x in self.properties)) + return msg.format( + self.cls.__name__, + ", ".join(x for x in self.properties), + ) class ExtraPropertiesError(STIXError, TypeError): @@ -44,8 +45,10 @@ class ExtraPropertiesError(STIXError, TypeError): def __str__(self): msg = "Unexpected properties for {0}: ({1})." - return msg.format(self.cls.__name__, - ", ".join(x for x in self.properties)) + return msg.format( + self.cls.__name__, + ", ".join(x for x in self.properties), + ) class ImmutableError(STIXError, ValueError): @@ -110,8 +113,10 @@ class MutuallyExclusivePropertiesError(STIXError, TypeError): def __str__(self): msg = "The ({1}) properties for {0} are mutually exclusive." - return msg.format(self.cls.__name__, - ", ".join(x for x in self.properties)) + return msg.format( + self.cls.__name__, + ", ".join(x for x in self.properties), + ) class DependentPropertiesError(STIXError, TypeError): @@ -124,8 +129,10 @@ class DependentPropertiesError(STIXError, TypeError): def __str__(self): msg = "The property dependencies for {0}: ({1}) are not met." - return msg.format(self.cls.__name__, - ", ".join(name for x in self.dependencies for name in x)) + return msg.format( + self.cls.__name__, + ", ".join(name for x in self.dependencies for name in x), + ) class AtLeastOnePropertyError(STIXError, TypeError): @@ -138,8 +145,10 @@ class AtLeastOnePropertyError(STIXError, TypeError): def __str__(self): msg = "At least one of the ({1}) properties for {0} must be populated." - return msg.format(self.cls.__name__, - ", ".join(x for x in self.properties)) + return msg.format( + self.cls.__name__, + ", ".join(x for x in self.properties), + ) class RevokeError(STIXError, ValueError): diff --git a/stix2/markings/__init__.py b/stix2/markings/__init__.py index c8dbdbc..79d1012 100644 --- a/stix2/markings/__init__.py +++ b/stix2/markings/__init__.py @@ -9,7 +9,6 @@ Note: Definitions. The corresponding methods on those classes are identical to these functions except that the `obj` parameter is omitted. - .. autosummary:: :toctree: markings @@ -51,7 +50,7 @@ def get_markings(obj, selectors=None, inherited=False, descendants=False): obj, selectors, inherited, - descendants + descendants, ) if inherited: @@ -208,7 +207,7 @@ def is_marked(obj, marking=None, selectors=None, inherited=False, descendants=Fa marking, selectors, inherited, - descendants + descendants, ) if inherited: @@ -221,7 +220,7 @@ def is_marked(obj, marking=None, selectors=None, inherited=False, descendants=Fa granular_marks, selectors, inherited, - descendants + descendants, ) result = result or object_markings.is_marked(obj, object_marks) diff --git a/stix2/markings/granular_markings.py b/stix2/markings/granular_markings.py index 7c227d9..09c3d37 100644 --- a/stix2/markings/granular_markings.py +++ b/stix2/markings/granular_markings.py @@ -1,5 +1,4 @@ -"""Functions for working with STIX 2.0 granular markings. -""" +"""Functions for working with STIX2 granular markings.""" from stix2 import exceptions from stix2.markings import utils @@ -29,7 +28,7 @@ def get_markings(obj, selectors, inherited=False, descendants=False): selectors = utils.convert_to_list(selectors) utils.validate(obj, selectors) - granular_markings = obj.get("granular_markings", []) + granular_markings = obj.get('granular_markings', []) if not granular_markings: return [] @@ -38,11 +37,13 @@ def get_markings(obj, selectors, inherited=False, descendants=False): for marking in granular_markings: for user_selector in selectors: - for marking_selector in marking.get("selectors", []): - if any([(user_selector == marking_selector), # Catch explicit selectors. - (user_selector.startswith(marking_selector) and inherited), # Catch inherited selectors. - (marking_selector.startswith(user_selector) and descendants)]): # Catch descendants selectors - refs = marking.get("marking_ref", []) + for marking_selector in marking.get('selectors', []): + if any([ + (user_selector == marking_selector), # Catch explicit selectors. + (user_selector.startswith(marking_selector) and inherited), # Catch inherited selectors. + (marking_selector.startswith(user_selector) and descendants), + ]): # Catch descendants selectors + refs = marking.get('marking_ref', []) results.update([refs]) return list(results) @@ -93,7 +94,7 @@ def remove_markings(obj, marking, selectors): marking = utils.convert_to_marking_list(marking) utils.validate(obj, selectors) - granular_markings = obj.get("granular_markings") + granular_markings = obj.get('granular_markings') if not granular_markings: return obj @@ -102,9 +103,9 @@ def remove_markings(obj, marking, selectors): to_remove = [] for m in marking: - to_remove.append({"marking_ref": m, "selectors": selectors}) + to_remove.append({'marking_ref': m, 'selectors': selectors}) - remove = utils.build_granular_marking(to_remove).get("granular_markings") + remove = utils.build_granular_marking(to_remove).get('granular_markings') if not any(marking in granular_markings for marking in remove): raise exceptions.MarkingNotFoundError(obj, remove) @@ -145,10 +146,10 @@ def add_markings(obj, marking, selectors): granular_marking = [] for m in marking: - granular_marking.append({"marking_ref": m, "selectors": sorted(selectors)}) + granular_marking.append({'marking_ref': m, 'selectors': sorted(selectors)}) - if obj.get("granular_markings"): - granular_marking.extend(obj.get("granular_markings")) + if obj.get('granular_markings'): + granular_marking.extend(obj.get('granular_markings')) granular_marking = utils.expand_markings(granular_marking) granular_marking = utils.compress_markings(granular_marking) @@ -176,7 +177,7 @@ def clear_markings(obj, selectors): selectors = utils.convert_to_list(selectors) utils.validate(obj, selectors) - granular_markings = obj.get("granular_markings") + granular_markings = obj.get('granular_markings') if not granular_markings: return obj @@ -184,25 +185,26 @@ def clear_markings(obj, selectors): granular_markings = utils.expand_markings(granular_markings) sdo = utils.build_granular_marking( - [{"selectors": selectors, "marking_ref": "N/A"}] + [{'selectors': selectors, 'marking_ref': 'N/A'}], ) - clear = sdo.get("granular_markings", []) + clear = sdo.get('granular_markings', []) - if not any(clear_selector in sdo_selectors.get("selectors", []) - for sdo_selectors in granular_markings - for clear_marking in clear - for clear_selector in clear_marking.get("selectors", []) - ): + if not any( + clear_selector in sdo_selectors.get('selectors', []) + for sdo_selectors in granular_markings + for clear_marking in clear + for clear_selector in clear_marking.get('selectors', []) + ): raise exceptions.MarkingNotFoundError(obj, clear) for granular_marking in granular_markings: for s in selectors: - if s in granular_marking.get("selectors", []): - marking_refs = granular_marking.get("marking_ref") + if s in granular_marking.get('selectors', []): + marking_refs = granular_marking.get('marking_ref') if marking_refs: - granular_marking["marking_ref"] = "" + granular_marking['marking_ref'] = '' granular_markings = utils.compress_markings(granular_markings) @@ -245,19 +247,21 @@ def is_marked(obj, marking=None, selectors=None, inherited=False, descendants=Fa marking = utils.convert_to_marking_list(marking) utils.validate(obj, selectors) - granular_markings = obj.get("granular_markings", []) + granular_markings = obj.get('granular_markings', []) marked = False markings = set() for granular_marking in granular_markings: for user_selector in selectors: - for marking_selector in granular_marking.get("selectors", []): + for marking_selector in granular_marking.get('selectors', []): - if any([(user_selector == marking_selector), # Catch explicit selectors. - (user_selector.startswith(marking_selector) and inherited), # Catch inherited selectors. - (marking_selector.startswith(user_selector) and descendants)]): # Catch descendants selectors - marking_ref = granular_marking.get("marking_ref", "") + if any([ + (user_selector == marking_selector), # Catch explicit selectors. + (user_selector.startswith(marking_selector) and inherited), # Catch inherited selectors. + (marking_selector.startswith(user_selector) and descendants), + ]): # Catch descendants selectors + marking_ref = granular_marking.get('marking_ref', '') if marking and any(x == marking_ref for x in marking): markings.update([marking_ref]) diff --git a/stix2/markings/object_markings.py b/stix2/markings/object_markings.py index a169fe3..dc85dfa 100644 --- a/stix2/markings/object_markings.py +++ b/stix2/markings/object_markings.py @@ -1,5 +1,4 @@ -"""Functions for working with STIX 2.0 object markings. -""" +"""Functions for working with STIX2 object markings.""" from stix2 import exceptions from stix2.markings import utils @@ -18,7 +17,7 @@ def get_markings(obj): markings are present in `object_marking_refs`. """ - return obj.get("object_marking_refs", []) + return obj.get('object_marking_refs', []) def add_markings(obj, marking): @@ -35,7 +34,7 @@ def add_markings(obj, marking): """ marking = utils.convert_to_marking_list(marking) - object_markings = set(obj.get("object_marking_refs", []) + marking) + object_markings = set(obj.get('object_marking_refs', []) + marking) return new_version(obj, object_marking_refs=list(object_markings), allow_custom=True) @@ -59,12 +58,12 @@ def remove_markings(obj, marking): """ marking = utils.convert_to_marking_list(marking) - object_markings = obj.get("object_marking_refs", []) + object_markings = obj.get('object_marking_refs', []) if not object_markings: return obj - if any(x not in obj["object_marking_refs"] for x in marking): + if any(x not in obj['object_marking_refs'] for x in marking): raise exceptions.MarkingNotFoundError(obj, marking) new_markings = [x for x in object_markings if x not in marking] @@ -124,7 +123,7 @@ def is_marked(obj, marking=None): """ marking = utils.convert_to_marking_list(marking) - object_markings = obj.get("object_marking_refs", []) + object_markings = obj.get('object_marking_refs', []) if marking: return any(x in object_markings for x in marking) diff --git a/stix2/markings/utils.py b/stix2/markings/utils.py index 429311b..4b4841c 100644 --- a/stix2/markings/utils.py +++ b/stix2/markings/utils.py @@ -1,5 +1,4 @@ -"""Utility functions for STIX 2.0 data markings. -""" +"""Utility functions for STIX2 data markings.""" import collections @@ -23,7 +22,7 @@ def _evaluate_expression(obj, selector): """ for items, value in iterpath(obj): - path = ".".join(items) + path = '.'.join(items) if path == selector and value: return [value] @@ -119,12 +118,12 @@ def compress_markings(granular_markings): map_ = collections.defaultdict(set) for granular_marking in granular_markings: - if granular_marking.get("marking_ref"): - map_[granular_marking.get("marking_ref")].update(granular_marking.get("selectors")) + if granular_marking.get('marking_ref'): + map_[granular_marking.get('marking_ref')].update(granular_marking.get('selectors')) compressed = \ [ - {"marking_ref": marking_ref, "selectors": sorted(selectors)} + {'marking_ref': marking_ref, 'selectors': sorted(selectors)} for marking_ref, selectors in six.iteritems(map_) ] @@ -173,14 +172,14 @@ def expand_markings(granular_markings): expanded = [] for marking in granular_markings: - selectors = marking.get("selectors") - marking_ref = marking.get("marking_ref") + selectors = marking.get('selectors') + marking_ref = marking.get('marking_ref') expanded.extend( [ - {"marking_ref": marking_ref, "selectors": [selector]} + {'marking_ref': marking_ref, 'selectors': [selector]} for selector in selectors - ] + ], ) return expanded @@ -189,7 +188,7 @@ def expand_markings(granular_markings): def build_granular_marking(granular_marking): """Return a dictionary with the required structure for a granular marking. """ - return {"granular_markings": expand_markings(granular_marking)} + return {'granular_markings': expand_markings(granular_marking)} def iterpath(obj, path=None): @@ -229,7 +228,7 @@ def iterpath(obj, path=None): elif isinstance(varobj, list): for item in varobj: - index = "[{0}]".format(varobj.index(item)) + index = '[{0}]'.format(varobj.index(item)) path.append(index) yield (path, item) diff --git a/stix2/pattern_visitor.py b/stix2/pattern_visitor.py index 94c8559..16e4d3c 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -4,8 +4,9 @@ import inspect from antlr4 import CommonTokenStream, InputStream import six from stix2patterns.grammars.STIXPatternLexer import STIXPatternLexer -from stix2patterns.grammars.STIXPatternParser import (STIXPatternParser, - TerminalNode) +from stix2patterns.grammars.STIXPatternParser import ( + STIXPatternParser, TerminalNode, +) from stix2patterns.grammars.STIXPatternVisitor import STIXPatternVisitor from stix2patterns.validator import STIXPatternErrorListener @@ -149,25 +150,35 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): children = self.visitChildren(ctx) operator = children[1].symbol.type negated = operator != STIXPatternParser.EQ - return self.instantiate("EqualityComparisonExpression", children[0], children[3 if len(children) > 3 else 2], - negated) + return self.instantiate( + "EqualityComparisonExpression", children[0], children[3 if len(children) > 3 else 2], + negated, + ) # Visit a parse tree produced by STIXPatternParser#propTestOrder. def visitPropTestOrder(self, ctx): children = self.visitChildren(ctx) operator = children[1].symbol.type if operator == STIXPatternParser.GT: - return self.instantiate("GreaterThanComparisonExpression", children[0], - children[3 if len(children) > 3 else 2], False) + return self.instantiate( + "GreaterThanComparisonExpression", children[0], + children[3 if len(children) > 3 else 2], False, + ) elif operator == STIXPatternParser.LT: - return self.instantiate("LessThanComparisonExpression", children[0], - children[3 if len(children) > 3 else 2], False) + return self.instantiate( + "LessThanComparisonExpression", children[0], + children[3 if len(children) > 3 else 2], False, + ) elif operator == STIXPatternParser.GE: - return self.instantiate("GreaterThanEqualComparisonExpression", children[0], - children[3 if len(children) > 3 else 2], False) + return self.instantiate( + "GreaterThanEqualComparisonExpression", children[0], + children[3 if len(children) > 3 else 2], False, + ) elif operator == STIXPatternParser.LE: - return self.instantiate("LessThanEqualComparisonExpression", children[0], - children[3 if len(children) > 3 else 2], False) + return self.instantiate( + "LessThanEqualComparisonExpression", children[0], + children[3 if len(children) > 3 else 2], False, + ) # Visit a parse tree produced by STIXPatternParser#propTestSet. def visitPropTestSet(self, ctx): @@ -182,8 +193,10 @@ class STIXPatternVisitorForSTIX2(STIXPatternVisitor): # Visit a parse tree produced by STIXPatternParser#propTestRegex. def visitPropTestRegex(self, ctx): children = self.visitChildren(ctx) - return self.instantiate("MatchesComparisonExpression", children[0], children[3 if len(children) > 3 else 2], - False) + return self.instantiate( + "MatchesComparisonExpression", children[0], children[3 if len(children) > 3 else 2], + False, + ) # Visit a parse tree produced by STIXPatternParser#propTestIsSubset. def visitPropTestIsSubset(self, ctx): diff --git a/stix2/patterns.py b/stix2/patterns.py index 1e0e03f..9656ff1 100644 --- a/stix2/patterns.py +++ b/stix2/patterns.py @@ -1,5 +1,4 @@ -"""Classes to aid in working with the STIX 2 patterning language. -""" +"""Classes to aid in working with the STIX 2 patterning language.""" import base64 import binascii @@ -187,7 +186,6 @@ class HexConstant(_Constant): Args: value (str): hexadecimal value """ - def __init__(self, value, from_parse_tree=False): # support with or without an 'h' if not from_parse_tree and re.match('^([a-fA-F0-9]{2})+$', value): diff --git a/stix2/properties.py b/stix2/properties.py index b52c026..24549aa 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,8 +1,9 @@ -"""Classes for representing properties of STIX Objects and Cyber Observables. -""" +"""Classes for representing properties of STIX Objects and Cyber Observables.""" + import base64 import binascii import collections +import copy import inspect import re import uuid @@ -11,19 +12,22 @@ from six import string_types, text_type from stix2patterns.validator import run_validator from .base import _STIXBase -from .exceptions import DictionaryKeyError -from .utils import _get_dict, parse_into_datetime +from .core import STIX2_OBJ_MAPS, parse, parse_observable +from .exceptions import CustomContentError, DictionaryKeyError +from .utils import _get_dict, get_class_hierarchy_names, parse_into_datetime # This uses the regular expression for a RFC 4122, Version 4 UUID. In the # 8-4-4-4-12 hexadecimal representation, the first hex digit of the third -# component must be a 4, and the first hex digit of the fourth component must be -# 8, 9, a, or b (10xx bit pattern). -ID_REGEX = re.compile("^[a-z0-9][a-z0-9-]+[a-z0-9]--" # object type - "[0-9a-fA-F]{8}-" - "[0-9a-fA-F]{4}-" - "4[0-9a-fA-F]{3}-" - "[89abAB][0-9a-fA-F]{3}-" - "[0-9a-fA-F]{12}$") +# component must be a 4, and the first hex digit of the fourth component +# must be 8, 9, a, or b (10xx bit pattern). +ID_REGEX = re.compile( + r"^[a-z0-9][a-z0-9-]+[a-z0-9]--" # object type + "[0-9a-fA-F]{8}-" + "[0-9a-fA-F]{4}-" + "4[0-9a-fA-F]{3}-" + "[89abAB][0-9a-fA-F]{3}-" + "[0-9a-fA-F]{12}$", +) ERROR_INVALID_ID = ( "not a valid STIX identifier, must match --" @@ -37,14 +41,15 @@ class Property(object): ``__init__()``. Args: - required (bool): If ``True``, the property must be provided when creating an - object with that property. No default value exists for these properties. - (Default: ``False``) + required (bool): If ``True``, the property must be provided when + creating an object with that property. No default value exists for + these properties. (Default: ``False``) fixed: This provides a constant default value. Users are free to - provide this value explicity when constructing an object (which allows - you to copy **all** values from an existing object to a new object), but - if the user provides a value other than the ``fixed`` value, it will raise - an error. This is semantically equivalent to defining both: + provide this value explicity when constructing an object (which + allows you to copy **all** values from an existing object to a new + object), but if the user provides a value other than the ``fixed`` + value, it will raise an error. This is semantically equivalent to + defining both: - a ``clean()`` function that checks if the value matches the fixed value, and @@ -55,29 +60,31 @@ class Property(object): - ``def clean(self, value) -> any:`` - Return a value that is valid for this property. If ``value`` is not valid for this property, this will attempt to transform it first. If - ``value`` is not valid and no such transformation is possible, it should - raise a ValueError. + ``value`` is not valid and no such transformation is possible, it + should raise a ValueError. - ``def default(self):`` - provide a default value for this property. - ``default()`` can return the special value ``NOW`` to use the current - time. This is useful when several timestamps in the same object need - to use the same default value, so calling now() for each property-- - likely several microseconds apart-- does not work. + time. This is useful when several timestamps in the same object + need to use the same default value, so calling now() for each + property-- likely several microseconds apart-- does not work. - Subclasses can instead provide a lambda function for ``default`` as a keyword - argument. ``clean`` should not be provided as a lambda since lambdas cannot - raise their own exceptions. + Subclasses can instead provide a lambda function for ``default`` as a + keyword argument. ``clean`` should not be provided as a lambda since + lambdas cannot raise their own exceptions. + + When instantiating Properties, ``required`` and ``default`` should not be + used together. ``default`` implies that the property is required in the + specification so this function will be used to supply a value if none is + provided. ``required`` means that the user must provide this; it is + required in the specification and we can't or don't want to create a + default value. - When instantiating Properties, ``required`` and ``default`` should not be used - together. ``default`` implies that the property is required in the specification - so this function will be used to supply a value if none is provided. - ``required`` means that the user must provide this; it is required in the - specification and we can't or don't want to create a default value. """ def _default_clean(self, value): if value != self._fixed_value: - raise ValueError("must equal '{0}'.".format(self._fixed_value)) + raise ValueError("must equal '{}'.".format(self._fixed_value)) return value def __init__(self, required=False, fixed=None, default=None): @@ -136,7 +143,7 @@ class ListProperty(Property): if type(self.contained) is EmbeddedObjectProperty: obj_type = self.contained.type - elif type(self.contained).__name__ is 'STIXObjectProperty': + elif type(self.contained).__name__ is "STIXObjectProperty": # ^ this way of checking doesn't require a circular import # valid is already an instance of a python-stix2 class; no need # to turn it into a dictionary and then pass it to the class @@ -184,7 +191,7 @@ class IDProperty(Property): def clean(self, value): if not value.startswith(self.required_prefix): - raise ValueError("must start with '{0}'.".format(self.required_prefix)) + raise ValueError("must start with '{}'.".format(self.required_prefix)) if not ID_REGEX.match(value): raise ValueError(ERROR_INVALID_ID) return value @@ -195,21 +202,51 @@ class IDProperty(Property): class IntegerProperty(Property): + def __init__(self, min=None, max=None, **kwargs): + self.min = min + self.max = max + super(IntegerProperty, self).__init__(**kwargs) + def clean(self, value): try: - return int(value) + value = int(value) except Exception: raise ValueError("must be an integer.") + if self.min is not None and value < self.min: + msg = "minimum value is {}. received {}".format(self.min, value) + raise ValueError(msg) + + if self.max is not None and value > self.max: + msg = "maximum value is {}. received {}".format(self.max, value) + raise ValueError(msg) + + return value + class FloatProperty(Property): + def __init__(self, min=None, max=None, **kwargs): + self.min = min + self.max = max + super(FloatProperty, self).__init__(**kwargs) + def clean(self, value): try: - return float(value) + value = float(value) except Exception: raise ValueError("must be a float.") + if self.min is not None and value < self.min: + msg = "minimum value is {}. received {}".format(self.min, value) + raise ValueError(msg) + + if self.max is not None and value > self.max: + msg = "maximum value is {}. received {}".format(self.max, value) + raise ValueError(msg) + + return value + class BooleanProperty(Property): @@ -217,8 +254,8 @@ class BooleanProperty(Property): if isinstance(value, bool): return value - trues = ['true', 't'] - falses = ['false', 'f'] + trues = ['true', 't', '1'] + falses = ['false', 'f', '0'] try: if value.lower() in trues: return True @@ -245,6 +282,10 @@ class TimestampProperty(Property): class DictionaryProperty(Property): + def __init__(self, spec_version='2.0', **kwargs): + self.spec_version = spec_version + super(DictionaryProperty, self).__init__(**kwargs) + def clean(self, value): try: dictified = _get_dict(value) @@ -252,35 +293,40 @@ class DictionaryProperty(Property): raise ValueError("The dictionary property must contain a dictionary") if dictified == {}: raise ValueError("The dictionary property must contain a non-empty dictionary") - for k in dictified.keys(): - if len(k) < 3: - raise DictionaryKeyError(k, "shorter than 3 characters") - elif len(k) > 256: - raise DictionaryKeyError(k, "longer than 256 characters") - if not re.match('^[a-zA-Z0-9_-]+$', k): - raise DictionaryKeyError(k, "contains characters other than" - "lowercase a-z, uppercase A-Z, " - "numerals 0-9, hyphen (-), or " - "underscore (_)") + if self.spec_version == '2.0': + if len(k) < 3: + raise DictionaryKeyError(k, "shorter than 3 characters") + elif len(k) > 256: + raise DictionaryKeyError(k, "longer than 256 characters") + elif self.spec_version == '2.1': + if len(k) > 250: + raise DictionaryKeyError(k, "longer than 250 characters") + if not re.match(r"^[a-zA-Z0-9_-]+$", k): + msg = ( + "contains characters other than lowercase a-z, " + "uppercase A-Z, numerals 0-9, hyphen (-), or " + "underscore (_)" + ) + raise DictionaryKeyError(k, msg) return dictified HASHES_REGEX = { - "MD5": ("^[a-fA-F0-9]{32}$", "MD5"), - "MD6": ("^[a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{56}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}$", "MD6"), - "RIPEMD160": ("^[a-fA-F0-9]{40}$", "RIPEMD-160"), - "SHA1": ("^[a-fA-F0-9]{40}$", "SHA-1"), - "SHA224": ("^[a-fA-F0-9]{56}$", "SHA-224"), - "SHA256": ("^[a-fA-F0-9]{64}$", "SHA-256"), - "SHA384": ("^[a-fA-F0-9]{96}$", "SHA-384"), - "SHA512": ("^[a-fA-F0-9]{128}$", "SHA-512"), - "SHA3224": ("^[a-fA-F0-9]{56}$", "SHA3-224"), - "SHA3256": ("^[a-fA-F0-9]{64}$", "SHA3-256"), - "SHA3384": ("^[a-fA-F0-9]{96}$", "SHA3-384"), - "SHA3512": ("^[a-fA-F0-9]{128}$", "SHA3-512"), - "SSDEEP": ("^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"), - "WHIRLPOOL": ("^[a-fA-F0-9]{128}$", "WHIRLPOOL"), + "MD5": (r"^[a-fA-F0-9]{32}$", "MD5"), + "MD6": (r"^[a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{56}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}$", "MD6"), + "RIPEMD160": (r"^[a-fA-F0-9]{40}$", "RIPEMD-160"), + "SHA1": (r"^[a-fA-F0-9]{40}$", "SHA-1"), + "SHA224": (r"^[a-fA-F0-9]{56}$", "SHA-224"), + "SHA256": (r"^[a-fA-F0-9]{64}$", "SHA-256"), + "SHA384": (r"^[a-fA-F0-9]{96}$", "SHA-384"), + "SHA512": (r"^[a-fA-F0-9]{128}$", "SHA-512"), + "SHA3224": (r"^[a-fA-F0-9]{56}$", "SHA3-224"), + "SHA3256": (r"^[a-fA-F0-9]{64}$", "SHA3-256"), + "SHA3384": (r"^[a-fA-F0-9]{96}$", "SHA3-384"), + "SHA3512": (r"^[a-fA-F0-9]{128}$", "SHA3-512"), + "SSDEEP": (r"^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"), + "WHIRLPOOL": (r"^[a-fA-F0-9]{128}$", "WHIRLPOOL"), } @@ -293,7 +339,7 @@ class HashesProperty(DictionaryProperty): if key in HASHES_REGEX: vocab_key = HASHES_REGEX[key][1] if not re.match(HASHES_REGEX[key][0], v): - raise ValueError("'%s' is not a valid %s hash" % (v, vocab_key)) + raise ValueError("'{0}' is not a valid {1} hash".format(v, vocab_key)) if k != vocab_key: clean_dict[vocab_key] = clean_dict[k] del clean_dict[k] @@ -313,7 +359,7 @@ class BinaryProperty(Property): class HexProperty(Property): def clean(self, value): - if not re.match('^([a-fA-F0-9]{2})+$', value): + if not re.match(r"^([a-fA-F0-9]{2})+$", value): raise ValueError("must contain an even number of hexadecimal characters") return value @@ -333,13 +379,13 @@ class ReferenceProperty(Property): value = str(value) if self.type: if not value.startswith(self.type): - raise ValueError("must start with '{0}'.".format(self.type)) + raise ValueError("must start with '{}'.".format(self.type)) if not ID_REGEX.match(value): raise ValueError(ERROR_INVALID_ID) return value -SELECTOR_REGEX = re.compile("^[a-z0-9_-]{3,250}(\\.(\\[\\d+\\]|[a-z0-9_-]{1,250}))*$") +SELECTOR_REGEX = re.compile(r"^[a-z0-9_-]{3,250}(\.(\[\d+\]|[a-z0-9_-]{1,250}))*$") class SelectorProperty(Property): @@ -369,7 +415,7 @@ class EmbeddedObjectProperty(Property): if type(value) is dict: value = self.type(**value) elif not isinstance(value, self.type): - raise ValueError("must be of type %s." % self.type.__name__) + raise ValueError("must be of type {}.".format(self.type.__name__)) return value @@ -384,7 +430,7 @@ class EnumProperty(StringProperty): def clean(self, value): value = super(EnumProperty, self).clean(value) if value not in self.allowed: - raise ValueError("value '%s' is not valid for this enumeration." % value) + raise ValueError("value '{}' is not valid for this enumeration.".format(value)) return self.string_type(value) @@ -397,3 +443,126 @@ class PatternProperty(StringProperty): raise ValueError(str(errors[0])) return self.string_type(value) + + +class ObservableProperty(Property): + """Property for holding Cyber Observable Objects. + """ + + def __init__(self, spec_version='2.0', allow_custom=False, *args, **kwargs): + self.allow_custom = allow_custom + self.spec_version = spec_version + super(ObservableProperty, self).__init__(*args, **kwargs) + + def clean(self, value): + try: + dictified = _get_dict(value) + # get deep copy since we are going modify the dict and might + # modify the original dict as _get_dict() does not return new + # dict when passed a dict + dictified = copy.deepcopy(dictified) + except ValueError: + raise ValueError("The observable property must contain a dictionary") + if dictified == {}: + raise ValueError("The observable property must contain a non-empty dictionary") + + valid_refs = dict((k, v['type']) for (k, v) in dictified.items()) + + for key, obj in dictified.items(): + parsed_obj = parse_observable( + obj, + valid_refs, + allow_custom=self.allow_custom, + version=self.spec_version, + ) + dictified[key] = parsed_obj + + return dictified + + +class ExtensionsProperty(DictionaryProperty): + """Property for representing extensions on Observable objects. + """ + + def __init__(self, spec_version='2.0', allow_custom=False, enclosing_type=None, required=False): + self.allow_custom = allow_custom + self.enclosing_type = enclosing_type + super(ExtensionsProperty, self).__init__(spec_version=spec_version, required=required) + + def clean(self, value): + try: + dictified = _get_dict(value) + # get deep copy since we are going modify the dict and might + # modify the original dict as _get_dict() does not return new + # dict when passed a dict + dictified = copy.deepcopy(dictified) + except ValueError: + raise ValueError("The extensions property must contain a dictionary") + if dictified == {}: + raise ValueError("The extensions property must contain a non-empty dictionary") + + v = 'v' + self.spec_version.replace('.', '') + + specific_type_map = STIX2_OBJ_MAPS[v]['observable-extensions'].get(self.enclosing_type, {}) + for key, subvalue in dictified.items(): + if key in specific_type_map: + cls = specific_type_map[key] + if type(subvalue) is dict: + if self.allow_custom: + subvalue['allow_custom'] = True + dictified[key] = cls(**subvalue) + else: + dictified[key] = cls(**subvalue) + elif type(subvalue) is cls: + # If already an instance of an _Extension class, assume it's valid + dictified[key] = subvalue + else: + raise ValueError("Cannot determine extension type.") + else: + raise CustomContentError("Can't parse unknown extension type: {}".format(key)) + return dictified + + +class STIXObjectProperty(Property): + + def __init__(self, spec_version='2.0', allow_custom=False, *args, **kwargs): + self.allow_custom = allow_custom + self.spec_version = spec_version + super(STIXObjectProperty, self).__init__(*args, **kwargs) + + def clean(self, value): + # Any STIX Object (SDO, SRO, or Marking Definition) can be added to + # a bundle with no further checks. + if any(x in ('STIXDomainObject', 'STIXRelationshipObject', 'MarkingDefinition') + for x in get_class_hierarchy_names(value)): + # A simple "is this a spec version 2.1+ object" test. For now, + # limit 2.0 bundles to 2.0 objects. It's not possible yet to + # have validation co-constraints among properties, e.g. have + # validation here depend on the value of another property + # (spec_version). So this is a hack, and not technically spec- + # compliant. + if 'spec_version' in value and self.spec_version == '2.0': + raise ValueError( + "Spec version 2.0 bundles don't yet support " + "containing objects of a different spec " + "version.", + ) + return value + try: + dictified = _get_dict(value) + except ValueError: + raise ValueError("This property may only contain a dictionary or object") + if dictified == {}: + raise ValueError("This property may only contain a non-empty dictionary or object") + if 'type' in dictified and dictified['type'] == 'bundle': + raise ValueError("This property may not contain a Bundle object") + if 'spec_version' in dictified and self.spec_version == '2.0': + # See above comment regarding spec_version. + raise ValueError( + "Spec version 2.0 bundles don't yet support " + "containing objects of a different spec version.", + ) + + parsed_obj = parse(dictified, allow_custom=self.allow_custom) + + return parsed_obj diff --git a/stix2/test/test_utils.py b/stix2/test/test_utils.py deleted file mode 100644 index 400ce84..0000000 --- a/stix2/test/test_utils.py +++ /dev/null @@ -1,210 +0,0 @@ -# -*- coding: utf-8 -*- - -import datetime as dt -from io import StringIO - -import pytest -import pytz - -import stix2.utils - -amsterdam = pytz.timezone('Europe/Amsterdam') -eastern = pytz.timezone('US/Eastern') - - -@pytest.mark.parametrize('dttm, timestamp', [ - (dt.datetime(2017, 1, 1, tzinfo=pytz.utc), '2017-01-01T00:00:00Z'), - (amsterdam.localize(dt.datetime(2017, 1, 1)), '2016-12-31T23:00:00Z'), - (eastern.localize(dt.datetime(2017, 1, 1, 12, 34, 56)), '2017-01-01T17:34:56Z'), - (eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'), - (dt.datetime(2017, 7, 1), '2017-07-01T00:00:00Z'), - (dt.datetime(2017, 7, 1, 0, 0, 0, 1), '2017-07-01T00:00:00.000001Z'), - (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='millisecond'), '2017-07-01T00:00:00.000Z'), - (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='second'), '2017-07-01T00:00:00Z'), -]) -def test_timestamp_formatting(dttm, timestamp): - assert stix2.utils.format_datetime(dttm) == timestamp - - -@pytest.mark.parametrize('timestamp, dttm', [ - (dt.datetime(2017, 1, 1, 0, tzinfo=pytz.utc), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), - (dt.date(2017, 1, 1), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), - ('2017-01-01T00:00:00Z', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), - ('2017-01-01T02:00:00+2:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), - ('2017-01-01T00:00:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), -]) -def test_parse_datetime(timestamp, dttm): - assert stix2.utils.parse_into_datetime(timestamp) == dttm - - -@pytest.mark.parametrize('timestamp, dttm, precision', [ - ('2017-01-01T01:02:03.000001', dt.datetime(2017, 1, 1, 1, 2, 3, 0, tzinfo=pytz.utc), 'millisecond'), - ('2017-01-01T01:02:03.001', dt.datetime(2017, 1, 1, 1, 2, 3, 1000, tzinfo=pytz.utc), 'millisecond'), - ('2017-01-01T01:02:03.1', dt.datetime(2017, 1, 1, 1, 2, 3, 100000, tzinfo=pytz.utc), 'millisecond'), - ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, 450000, tzinfo=pytz.utc), 'millisecond'), - ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, tzinfo=pytz.utc), 'second'), -]) -def test_parse_datetime_precision(timestamp, dttm, precision): - assert stix2.utils.parse_into_datetime(timestamp, precision) == dttm - - -@pytest.mark.parametrize('ts', [ - 'foobar', - 1, -]) -def test_parse_datetime_invalid(ts): - with pytest.raises(ValueError): - stix2.utils.parse_into_datetime('foobar') - - -@pytest.mark.parametrize('data', [ - {"a": 1}, - '{"a": 1}', - StringIO(u'{"a": 1}'), - [("a", 1,)], -]) -def test_get_dict(data): - assert stix2.utils._get_dict(data) - - -@pytest.mark.parametrize('data', [ - 1, - [1], - ['a', 1], - "foobar", -]) -def test_get_dict_invalid(data): - with pytest.raises(ValueError): - stix2.utils._get_dict(data) - - -@pytest.mark.parametrize('stix_id, type', [ - ('malware--d69c8146-ab35-4d50-8382-6fc80e641d43', 'malware'), - ('intrusion-set--899ce53f-13a0-479b-a0e4-67d46e241542', 'intrusion-set') -]) -def test_get_type_from_id(stix_id, type): - assert stix2.utils.get_type_from_id(stix_id) == type - - -def test_deduplicate(stix_objs1): - unique = stix2.utils.deduplicate(stix_objs1) - - # Only 3 objects are unique - # 2 id's vary - # 2 modified times vary for a particular id - - assert len(unique) == 3 - - ids = [obj['id'] for obj in unique] - mods = [obj['modified'] for obj in unique] - - assert "indicator--00000000-0000-4000-8000-000000000001" in ids - assert "indicator--00000000-0000-4000-8000-000000000001" in ids - assert "2017-01-27T13:49:53.935Z" in mods - assert "2017-01-27T13:49:53.936Z" in mods - - -@pytest.mark.parametrize('object, tuple_to_find, expected_index', [ - (stix2.ObservedData( - id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", - created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T19:58:16.000Z", - modified="2016-04-06T19:58:16.000Z", - first_observed="2015-12-21T19:00:00Z", - last_observed="2015-12-21T19:00:00Z", - number_observed=50, - objects={ - "0": { - "name": "foo.exe", - "type": "file" - }, - "1": { - "type": "ipv4-addr", - "value": "198.51.100.3" - }, - "2": { - "type": "network-traffic", - "src_ref": "1", - "protocols": [ - "tcp", - "http" - ], - "extensions": { - "http-request-ext": { - "request_method": "get", - "request_value": "/download.html", - "request_version": "http/1.1", - "request_header": { - "Accept-Encoding": "gzip,deflate", - "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", - "Host": "www.example.com" - } - } - } - } - }, - ), ('1', {"type": "ipv4-addr", "value": "198.51.100.3"}), 1), - ({ - "type": "x-example", - "id": "x-example--d5413db2-c26c-42e0-b0e0-ec800a310bfb", - "created": "2018-06-11T01:25:22.063Z", - "modified": "2018-06-11T01:25:22.063Z", - "dictionary": { - "key": { - "key_one": "value", - "key_two": "value" - } - } - }, ('key', {'key_one': 'value', 'key_two': 'value'}), 0), - ({ - "type": "language-content", - "id": "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d", - "created": "2017-02-08T21:31:22.007Z", - "modified": "2017-02-08T21:31:22.007Z", - "object_ref": "campaign--12a111f0-b824-4baf-a224-83b80237a094", - "object_modified": "2017-02-08T21:31:22.007Z", - "contents": { - "de": { - "name": "Bank Angriff 1", - "description": "Weitere Informationen über Banküberfall" - }, - "fr": { - "name": "Attaque Bank 1", - "description": "Plus d'informations sur la crise bancaire" - } - } - }, ('fr', {"name": "Attaque Bank 1", "description": "Plus d'informations sur la crise bancaire"}), 1) -]) -def test_find_property_index(object, tuple_to_find, expected_index): - assert stix2.utils.find_property_index( - object, - *tuple_to_find - ) == expected_index - - -@pytest.mark.parametrize('dict_value, tuple_to_find, expected_index', [ - ({ - "contents": { - "de": { - "name": "Bank Angriff 1", - "description": "Weitere Informationen über Banküberfall" - }, - "fr": { - "name": "Attaque Bank 1", - "description": "Plus d'informations sur la crise bancaire" - }, - "es": { - "name": "Ataque al Banco", - "description": "Mas informacion sobre el ataque al banco" - } - } - }, ('es', {"name": "Ataque al Banco", "description": "Mas informacion sobre el ataque al banco"}), 1), # Sorted alphabetically - ({ - 'my_list': [ - {"key_one": 1}, - {"key_two": 2} - ] - }, ('key_one', 1), 0) -]) -def test_iterate_over_values(dict_value, tuple_to_find, expected_index): - assert stix2.utils._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index diff --git a/stix2/test/v20/__init__.py b/stix2/test/v20/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stix2/test/conftest.py b/stix2/test/v20/conftest.py similarity index 79% rename from stix2/test/conftest.py rename to stix2/test/v20/conftest.py index 92a3ef2..48e4532 100644 --- a/stix2/test/conftest.py +++ b/stix2/test/v20/conftest.py @@ -4,8 +4,9 @@ import pytest import stix2 -from .constants import (FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS, - RELATIONSHIP_KWARGS) +from .constants import ( + FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS, RELATIONSHIP_KWARGS, +) # Inspired by: http://stackoverflow.com/a/24006251 @@ -35,17 +36,17 @@ def uuid4(monkeypatch): @pytest.fixture def indicator(uuid4, clock): - return stix2.Indicator(**INDICATOR_KWARGS) + return stix2.v20.Indicator(**INDICATOR_KWARGS) @pytest.fixture def malware(uuid4, clock): - return stix2.Malware(**MALWARE_KWARGS) + return stix2.v20.Malware(**MALWARE_KWARGS) @pytest.fixture def relationship(uuid4, clock): - return stix2.Relationship(**RELATIONSHIP_KWARGS) + return stix2.v20.Relationship(**RELATIONSHIP_KWARGS) @pytest.fixture @@ -54,61 +55,61 @@ def stix_objs1(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } ind2 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } ind3 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.936Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } ind4 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } ind5 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } return [ind1, ind2, ind3, ind4, ind5] @@ -119,41 +120,41 @@ def stix_objs2(): "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-31T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } ind7 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } ind8 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } return [ind6, ind7, ind8] @pytest.fixture def real_stix_objs2(stix_objs2): - return [stix2.parse(x) for x in stix_objs2] + return [stix2.parse(x, version="2.0") for x in stix_objs2] diff --git a/stix2/test/constants.py b/stix2/test/v20/constants.py similarity index 95% rename from stix2/test/constants.py rename to stix2/test/v20/constants.py index 42f7e94..8d439f1 100644 --- a/stix2/test/constants.py +++ b/stix2/test/v20/constants.py @@ -12,6 +12,7 @@ INDICATOR_ID = "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7" INTRUSION_SET_ID = "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29" MALWARE_ID = "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e" MARKING_DEFINITION_ID = "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" +NOTE_ID = "note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061" OBSERVED_DATA_ID = "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf" RELATIONSHIP_ID = "relationship--df7c87eb-75d2-4948-af81-9d49d246f301" REPORT_ID = "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3" @@ -31,7 +32,7 @@ MARKING_IDS = [ RELATIONSHIP_IDS = [ 'relationship--06520621-5352-4e6a-b976-e8fa3d437ffd', 'relationship--181c9c09-43e6-45dd-9374-3bec192f05ef', - 'relationship--a0cbb21c-8daf-4a7f-96aa-7155a4ef8f70' + 'relationship--a0cbb21c-8daf-4a7f-96aa-7155a4ef8f70', ] # *_KWARGS contains all required arguments to create an instance of that STIX object @@ -86,7 +87,7 @@ MALWARE_MORE_KWARGS = dict( modified="2016-04-06T20:03:00.000Z", labels=['ransomware'], name="Cryptolocker", - description="A ransomware related to ..." + description="A ransomware related to ...", ) OBSERVED_DATA_KWARGS = dict( @@ -97,8 +98,8 @@ OBSERVED_DATA_KWARGS = dict( "0": { "type": "windows-registry-key", "key": "HKEY_LOCAL_MACHINE\\System\\Foo\\Bar", - } - } + }, + }, ) REPORT_KWARGS = dict( diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22/20170531213019735010.json b/stix2/test/v20/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22/20170531213019735010.json similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22/20170531213019735010.json rename to stix2/test/v20/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22/20170531213019735010.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b/20170531213026496201.json b/stix2/test/v20/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b/20170531213026496201.json similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b/20170531213026496201.json rename to stix2/test/v20/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b/20170531213026496201.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9/20170531213029458940.json b/stix2/test/v20/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9/20170531213029458940.json similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9/20170531213029458940.json rename to stix2/test/v20/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9/20170531213029458940.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475/20170531213045139269.json b/stix2/test/v20/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475/20170531213045139269.json similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475/20170531213045139269.json rename to stix2/test/v20/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475/20170531213045139269.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c/20170531213041022897.json b/stix2/test/v20/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c/20170531213041022897.json similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c/20170531213041022897.json rename to stix2/test/v20/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c/20170531213041022897.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a/20170531213032662702.json b/stix2/test/v20/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a/20170531213032662702.json similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a/20170531213032662702.json rename to stix2/test/v20/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a/20170531213032662702.json diff --git a/stix2/test/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f/20170531213026495974.json b/stix2/test/v20/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f/20170531213026495974.json similarity index 100% rename from stix2/test/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f/20170531213026495974.json rename to stix2/test/v20/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f/20170531213026495974.json diff --git a/stix2/test/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd/20170531213041022744.json b/stix2/test/v20/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd/20170531213041022744.json similarity index 100% rename from stix2/test/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd/20170531213041022744.json rename to stix2/test/v20/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd/20170531213041022744.json diff --git a/stix2/test/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20170601000000000000.json b/stix2/test/v20/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20170601000000000000.json similarity index 100% rename from stix2/test/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20170601000000000000.json rename to stix2/test/v20/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20170601000000000000.json diff --git a/stix2/test/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20181101232448446000.json b/stix2/test/v20/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20181101232448446000.json similarity index 100% rename from stix2/test/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20181101232448446000.json rename to stix2/test/v20/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20181101232448446000.json diff --git a/stix2/test/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064/20170531213149412497.json b/stix2/test/v20/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064/20170531213149412497.json similarity index 100% rename from stix2/test/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064/20170531213149412497.json rename to stix2/test/v20/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064/20170531213149412497.json diff --git a/stix2/test/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a/20170531213153197755.json b/stix2/test/v20/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a/20170531213153197755.json similarity index 100% rename from stix2/test/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a/20170531213153197755.json rename to stix2/test/v20/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a/20170531213153197755.json diff --git a/stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json b/stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json similarity index 100% rename from stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json rename to stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json diff --git a/stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json b/stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json similarity index 100% rename from stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json rename to stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json diff --git a/stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json b/stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json similarity index 100% rename from stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json rename to stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json diff --git a/stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json b/stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json similarity index 100% rename from stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json rename to stix2/test/v20/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json diff --git a/stix2/test/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json b/stix2/test/v20/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json similarity index 100% rename from stix2/test/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json rename to stix2/test/v20/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json diff --git a/stix2/test/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json b/stix2/test/v20/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json similarity index 100% rename from stix2/test/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json rename to stix2/test/v20/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json diff --git a/stix2/test/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json b/stix2/test/v20/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json similarity index 100% rename from stix2/test/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json rename to stix2/test/v20/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json diff --git a/stix2/test/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json b/stix2/test/v20/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json rename to stix2/test/v20/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json diff --git a/stix2/test/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883/20170531213327182784.json b/stix2/test/v20/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883/20170531213327182784.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883/20170531213327182784.json rename to stix2/test/v20/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883/20170531213327182784.json diff --git a/stix2/test/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227/20170531213327082801.json b/stix2/test/v20/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227/20170531213327082801.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227/20170531213327082801.json rename to stix2/test/v20/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227/20170531213327082801.json diff --git a/stix2/test/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432/20170531213327018782.json b/stix2/test/v20/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432/20170531213327018782.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432/20170531213327018782.json rename to stix2/test/v20/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432/20170531213327018782.json diff --git a/stix2/test/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e/20170531213327100701.json b/stix2/test/v20/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e/20170531213327100701.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e/20170531213327100701.json rename to stix2/test/v20/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e/20170531213327100701.json diff --git a/stix2/test/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1/20170531213327143973.json b/stix2/test/v20/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1/20170531213327143973.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1/20170531213327143973.json rename to stix2/test/v20/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1/20170531213327143973.json diff --git a/stix2/test/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719/20170531213327021562.json b/stix2/test/v20/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719/20170531213327021562.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719/20170531213327021562.json rename to stix2/test/v20/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719/20170531213327021562.json diff --git a/stix2/test/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1/20170531213327044387.json b/stix2/test/v20/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1/20170531213327044387.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1/20170531213327044387.json rename to stix2/test/v20/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1/20170531213327044387.json diff --git a/stix2/test/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d/20170531213327051532.json b/stix2/test/v20/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d/20170531213327051532.json similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d/20170531213327051532.json rename to stix2/test/v20/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d/20170531213327051532.json diff --git a/stix2/test/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23/20170531213231601148.json b/stix2/test/v20/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23/20170531213231601148.json similarity index 100% rename from stix2/test/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23/20170531213231601148.json rename to stix2/test/v20/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23/20170531213231601148.json diff --git a/stix2/test/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966/20170531213212684914.json b/stix2/test/v20/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966/20170531213212684914.json similarity index 100% rename from stix2/test/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966/20170531213212684914.json rename to stix2/test/v20/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966/20170531213212684914.json diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/v20/test_attack_pattern.py similarity index 69% rename from stix2/test/test_attack_pattern.py rename to stix2/test/v20/test_attack_pattern.py index 0be118c..f071d3a 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/v20/test_attack_pattern.py @@ -24,14 +24,14 @@ EXPECTED = """{ def test_attack_pattern_example(): - ap = stix2.AttackPattern( + ap = stix2.v20.AttackPattern( id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", created="2016-05-12T08:17:27.000Z", modified="2016-05-12T08:17:27.000Z", name="Spear Phishing", external_references=[{ "source_name": "capec", - "external_id": "CAPEC-163" + "external_id": "CAPEC-163", }], description="...", ) @@ -39,25 +39,27 @@ def test_attack_pattern_example(): assert str(ap) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "type": "attack-pattern", - "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "created": "2016-05-12T08:17:27.000Z", - "modified": "2016-05-12T08:17:27.000Z", - "description": "...", - "external_references": [ - { - "external_id": "CAPEC-163", - "source_name": "capec" - } - ], - "name": "Spear Phishing", - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "type": "attack-pattern", + "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "description": "...", + "external_references": [ + { + "external_id": "CAPEC-163", + "source_name": "capec", + }, + ], + "name": "Spear Phishing", + }, + ], +) def test_parse_attack_pattern(data): - ap = stix2.parse(data) + ap = stix2.parse(data, version="2.0") assert ap.type == 'attack-pattern' assert ap.id == ATTACK_PATTERN_ID @@ -71,12 +73,12 @@ def test_parse_attack_pattern(data): def test_attack_pattern_invalid_labels(): with pytest.raises(stix2.exceptions.InvalidValueError): - stix2.AttackPattern( + stix2.v20.AttackPattern( id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", created="2016-05-12T08:17:27Z", modified="2016-05-12T08:17:27Z", name="Spear Phishing", - labels=1 + labels=1, ) # TODO: Add other examples diff --git a/stix2/test/test_base.py b/stix2/test/v20/test_base.py similarity index 100% rename from stix2/test/test_base.py rename to stix2/test/v20/test_base.py diff --git a/stix2/test/v20/test_bundle.py b/stix2/test/v20/test_bundle.py new file mode 100644 index 0000000..907f632 --- /dev/null +++ b/stix2/test/v20/test_bundle.py @@ -0,0 +1,236 @@ +import json + +import pytest + +import stix2 + +EXPECTED_BUNDLE = """{ + "type": "bundle", + "id": "bundle--00000000-0000-4000-8000-000000000007", + "spec_version": "2.0", + "objects": [ + { + "type": "indicator", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z", + "labels": [ + "malicious-activity" + ] + }, + { + "type": "malware", + "id": "malware--00000000-0000-4000-8000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware" + ] + }, + { + "type": "relationship", + "id": "relationship--00000000-0000-4000-8000-000000000005", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e" + } + ] +}""" + +EXPECTED_BUNDLE_DICT = { + "type": "bundle", + "id": "bundle--00000000-0000-4000-8000-000000000007", + "spec_version": "2.0", + "objects": [ + { + "type": "indicator", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z", + "labels": [ + "malicious-activity", + ], + }, + { + "type": "malware", + "id": "malware--00000000-0000-4000-8000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware", + ], + }, + { + "type": "relationship", + "id": "relationship--00000000-0000-4000-8000-000000000005", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + }, + ], +} + + +def test_empty_bundle(): + bundle = stix2.v20.Bundle() + + assert bundle.type == "bundle" + assert bundle.id.startswith("bundle--") + with pytest.raises(AttributeError): + assert bundle.objects + + +def test_bundle_with_wrong_type(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v20.Bundle(type="not-a-bundle") + + assert excinfo.value.cls == stix2.v20.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v20.Bundle(id='my-prefix--') + + assert excinfo.value.cls == stix2.v20.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_create_bundle1(indicator, malware, relationship): + bundle = stix2.v20.Bundle(objects=[indicator, malware, relationship]) + + assert str(bundle) == EXPECTED_BUNDLE + assert bundle.serialize(pretty=True) == EXPECTED_BUNDLE + + +def test_create_bundle2(indicator, malware, relationship): + bundle = stix2.v20.Bundle(objects=[indicator, malware, relationship]) + + assert json.loads(bundle.serialize()) == EXPECTED_BUNDLE_DICT + + +def test_create_bundle_with_positional_args(indicator, malware, relationship): + bundle = stix2.v20.Bundle(indicator, malware, relationship) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_positional_listarg(indicator, malware, relationship): + bundle = stix2.v20.Bundle([indicator, malware, relationship]) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_listarg_and_positional_arg(indicator, malware, relationship): + bundle = stix2.v20.Bundle([indicator, malware], relationship) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_listarg_and_kwarg(indicator, malware, relationship): + bundle = stix2.v20.Bundle([indicator, malware], objects=[relationship]) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_arg_listarg_and_kwarg(indicator, malware, relationship): + bundle = stix2.v20.Bundle([indicator], malware, objects=[relationship]) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_invalid(indicator, malware, relationship): + with pytest.raises(ValueError) as excinfo: + stix2.v20.Bundle(objects=[1]) + assert excinfo.value.reason == "This property may only contain a dictionary or object" + + with pytest.raises(ValueError) as excinfo: + stix2.v20.Bundle(objects=[{}]) + assert excinfo.value.reason == "This property may only contain a non-empty dictionary or object" + + with pytest.raises(ValueError) as excinfo: + stix2.v20.Bundle(objects=[{'type': 'bundle'}]) + assert excinfo.value.reason == 'This property may not contain a Bundle object' + + +@pytest.mark.parametrize("version", ["2.0"]) +def test_parse_bundle(version): + bundle = stix2.parse(EXPECTED_BUNDLE, version=version) + + assert bundle.type == "bundle" + assert bundle.id.startswith("bundle--") + assert type(bundle.objects[0]) is stix2.v20.Indicator + assert bundle.objects[0].type == 'indicator' + assert bundle.objects[1].type == 'malware' + assert bundle.objects[2].type == 'relationship' + + +def test_parse_unknown_type(): + unknown = { + "type": "other", + "id": "other--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created": "2016-04-06T20:03:00Z", + "modified": "2016-04-06T20:03:00Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "name": "Green Group Attacks Against Finance", + } + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse(unknown, version="2.0") + assert str(excinfo.value) == "Can't parse unknown object type 'other'! For custom types, use the CustomObject decorator." + + +def test_stix_object_property(): + prop = stix2.properties.STIXObjectProperty(spec_version='2.0') + + identity = stix2.v20.Identity(name="test", identity_class="individual") + assert prop.clean(identity) is identity + + +def test_bundle_with_different_spec_objects(): + # This is a 2.0 case only... + + data = [ + { + "spec_version": "2.1", + "type": "indicator", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z", + "labels": [ + "malicious-activity", + ], + }, + { + "type": "malware", + "id": "malware--00000000-0000-4000-8000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware", + ], + }, + ] + + with pytest.raises(ValueError) as excinfo: + stix2.v20.Bundle(objects=data) + + assert "Spec version 2.0 bundles don't yet support containing objects of a different spec version." in str(excinfo.value) diff --git a/stix2/test/test_campaign.py b/stix2/test/v20/test_campaign.py similarity index 69% rename from stix2/test/test_campaign.py rename to stix2/test/v20/test_campaign.py index b226478..57dbfd2 100644 --- a/stix2/test/test_campaign.py +++ b/stix2/test/v20/test_campaign.py @@ -19,32 +19,34 @@ EXPECTED = """{ def test_campaign_example(): - campaign = stix2.Campaign( + campaign = stix2.v20.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:00Z", modified="2016-04-06T20:03:00Z", name="Green Group Attacks Against Finance", - description="Campaign by Green Group against a series of targets in the financial services sector." + description="Campaign by Green Group against a series of targets in the financial services sector.", ) assert str(campaign) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "type": "campaign", - "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "created": "2016-04-06T20:03:00Z", - "modified": "2016-04-06T20:03:00Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "description": "Campaign by Green Group against a series of targets in the financial services sector.", - "name": "Green Group Attacks Against Finance", - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "type": "campaign", + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created": "2016-04-06T20:03:00Z", + "modified": "2016-04-06T20:03:00Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "name": "Green Group Attacks Against Finance", + }, + ], +) def test_parse_campaign(data): - cmpn = stix2.parse(data) + cmpn = stix2.parse(data, version="2.0") assert cmpn.type == 'campaign' assert cmpn.id == CAMPAIGN_ID diff --git a/stix2/test/v20/test_core.py b/stix2/test/v20/test_core.py new file mode 100644 index 0000000..017344f --- /dev/null +++ b/stix2/test/v20/test_core.py @@ -0,0 +1,172 @@ +import pytest + +import stix2 +from stix2 import core, exceptions + +BUNDLE = { + "type": "bundle", + "spec_version": "2.0", + "id": "bundle--00000000-0000-4000-8000-000000000007", + "objects": [ + { + "type": "indicator", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z", + "labels": [ + "malicious-activity", + ], + }, + { + "type": "malware", + "id": "malware--00000000-0000-4000-8000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware", + ], + }, + { + "type": "relationship", + "id": "relationship--00000000-0000-4000-8000-000000000005", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + }, + ], +} + + +def test_dict_to_stix2_bundle_with_version(): + with pytest.raises(exceptions.ExtraPropertiesError) as excinfo: + core.dict_to_stix2(BUNDLE, version='2.1') + + assert str(excinfo.value) == "Unexpected properties for Bundle: (spec_version)." + + +def test_parse_observable_with_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = core.parse_observable(observable, version='2.0') + v = 'v20' + + assert v in str(obs_obj.__class__) + + +@pytest.mark.xfail(reason="The default version is no longer 2.0", condition=stix2.DEFAULT_VERSION != "2.0") +def test_parse_observable_with_no_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = core.parse_observable(observable) + v = 'v20' + + assert v in str(obs_obj.__class__) + + +def test_register_object_with_version(): + bundle = core.dict_to_stix2(BUNDLE, version='2.0') + core._register_object(bundle.objects[0].__class__, version='2.0') + v = 'v20' + + assert bundle.objects[0].type in core.STIX2_OBJ_MAPS[v]['objects'] + assert v in str(bundle.objects[0].__class__) + + +def test_register_marking_with_version(): + core._register_marking(stix2.v20.TLP_WHITE.__class__, version='2.0') + v = 'v20' + + assert stix2.v20.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] + assert v in str(stix2.v20.TLP_WHITE.__class__) + + +@pytest.mark.xfail(reason="The default version is no longer 2.0", condition=stix2.DEFAULT_VERSION != "2.0") +def test_register_marking_with_no_version(): + # Uses default version (2.0 in this case) + core._register_marking(stix2.v20.TLP_WHITE.__class__) + v = 'v20' + + assert stix2.v20.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] + assert v in str(stix2.v20.TLP_WHITE.__class__) + + +def test_register_observable_with_version(): + observed_data = stix2.v20.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + "extensions": { + "ntfs-ext": { + "alternate_data_streams": [ + { + "name": "second.stream", + "size": 25536, + }, + ], + }, + }, + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["0"], + }, + }, + ) + core._register_observable(observed_data.objects['0'].__class__, version='2.0') + v = 'v20' + + assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] + assert v in str(observed_data.objects['0'].__class__) + + +def test_register_observable_extension_with_version(): + observed_data = stix2.v20.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + "extensions": { + "ntfs-ext": { + "alternate_data_streams": [ + { + "name": "second.stream", + "size": 25536, + }, + ], + }, + }, + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["0"], + }, + }, + ) + core._register_observable_extension(observed_data.objects['0'], observed_data.objects['0'].extensions['ntfs-ext'].__class__, version='2.0') + v = 'v20' + + assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] + assert v in str(observed_data.objects['0'].__class__) + + assert observed_data.objects['0'].extensions['ntfs-ext']._type in core.STIX2_OBJ_MAPS[v]['observable-extensions']['file'] + assert v in str(observed_data.objects['0'].extensions['ntfs-ext'].__class__) diff --git a/stix2/test/test_course_of_action.py b/stix2/test/v20/test_course_of_action.py similarity index 71% rename from stix2/test/test_course_of_action.py rename to stix2/test/v20/test_course_of_action.py index e376f0d..d1c0fb7 100644 --- a/stix2/test/test_course_of_action.py +++ b/stix2/test/v20/test_course_of_action.py @@ -19,32 +19,34 @@ EXPECTED = """{ def test_course_of_action_example(): - coa = stix2.CourseOfAction( + coa = stix2.v20.CourseOfAction( id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:48.000Z", modified="2016-04-06T20:03:48.000Z", name="Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", - description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." + description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", ) assert str(coa) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "created": "2016-04-06T20:03:48.000Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", - "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "modified": "2016-04-06T20:03:48.000Z", - "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", - "type": "course-of-action" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", + "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", + "type": "course-of-action", + }, + ], +) def test_parse_course_of_action(data): - coa = stix2.parse(data) + coa = stix2.parse(data, version="2.0") assert coa.type == 'course-of-action' assert coa.id == COURSE_OF_ACTION_ID diff --git a/stix2/test/test_custom.py b/stix2/test/v20/test_custom.py similarity index 74% rename from stix2/test/test_custom.py rename to stix2/test/v20/test_custom.py index 18b8f18..40ffa88 100644 --- a/stix2/test/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -4,7 +4,7 @@ import stix2 from .constants import FAKE_TIME, MARKING_DEFINITION_ID -IDENTITY_CUSTOM_PROP = stix2.Identity( +IDENTITY_CUSTOM_PROP = stix2.v20.Identity( name="John Smith", identity_class="individual", x_foo="bar", @@ -14,7 +14,7 @@ IDENTITY_CUSTOM_PROP = stix2.Identity( def test_identity_custom_property(): with pytest.raises(ValueError) as excinfo: - stix2.Identity( + stix2.v20.Identity( id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", created="2015-12-21T19:59:11Z", modified="2015-12-21T19:59:11Z", @@ -25,7 +25,7 @@ def test_identity_custom_property(): assert str(excinfo.value) == "'custom_properties' must be a dictionary" with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: - stix2.Identity( + stix2.v20.Identity( id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", created="2015-12-21T19:59:11Z", modified="2015-12-21T19:59:11Z", @@ -35,10 +35,10 @@ def test_identity_custom_property(): "foo": "bar", }, foo="bar", - ) + ) assert "Unexpected properties for Identity" in str(excinfo.value) - identity = stix2.Identity( + identity = stix2.v20.Identity( id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", created="2015-12-21T19:59:11Z", modified="2015-12-21T19:59:11Z", @@ -53,7 +53,7 @@ def test_identity_custom_property(): def test_identity_custom_property_invalid(): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: - stix2.Identity( + stix2.v20.Identity( id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", created="2015-12-21T19:59:11Z", modified="2015-12-21T19:59:11Z", @@ -61,13 +61,13 @@ def test_identity_custom_property_invalid(): identity_class="individual", x_foo="bar", ) - assert excinfo.value.cls == stix2.Identity + assert excinfo.value.cls == stix2.v20.Identity assert excinfo.value.properties == ['x_foo'] assert "Unexpected properties for" in str(excinfo.value) def test_identity_custom_property_allowed(): - identity = stix2.Identity( + identity = stix2.v20.Identity( id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", created="2015-12-21T19:59:11Z", modified="2015-12-21T19:59:11Z", @@ -79,8 +79,9 @@ def test_identity_custom_property_allowed(): assert identity.x_foo == "bar" -@pytest.mark.parametrize("data", [ - """{ +@pytest.mark.parametrize( + "data", [ + """{ "type": "identity", "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", "created": "2015-12-21T19:59:11Z", @@ -89,34 +90,35 @@ def test_identity_custom_property_allowed(): "identity_class": "individual", "foo": "bar" }""", -]) + ], +) def test_parse_identity_custom_property(data): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: - identity = stix2.parse(data) - assert excinfo.value.cls == stix2.Identity + stix2.parse(data, version="2.0") + assert excinfo.value.cls == stix2.v20.Identity assert excinfo.value.properties == ['foo'] assert "Unexpected properties for" in str(excinfo.value) - identity = stix2.parse(data, allow_custom=True) + identity = stix2.parse(data, version="2.0", allow_custom=True) assert identity.foo == "bar" def test_custom_property_object_in_bundled_object(): - bundle = stix2.Bundle(IDENTITY_CUSTOM_PROP, allow_custom=True) + bundle = stix2.v20.Bundle(IDENTITY_CUSTOM_PROP, allow_custom=True) assert bundle.objects[0].x_foo == "bar" assert '"x_foo": "bar"' in str(bundle) def test_custom_properties_object_in_bundled_object(): - obj = stix2.Identity( + obj = stix2.v20.Identity( name="John Smith", identity_class="individual", custom_properties={ "x_foo": "bar", - } + }, ) - bundle = stix2.Bundle(obj, allow_custom=True) + bundle = stix2.v20.Bundle(obj, allow_custom=True) assert bundle.objects[0].x_foo == "bar" assert '"x_foo": "bar"' in str(bundle) @@ -132,9 +134,9 @@ def test_custom_property_dict_in_bundled_object(): 'x_foo': 'bar', } with pytest.raises(stix2.exceptions.ExtraPropertiesError): - bundle = stix2.Bundle(custom_identity) + stix2.v20.Bundle(custom_identity) - bundle = stix2.Bundle(custom_identity, allow_custom=True) + bundle = stix2.v20.Bundle(custom_identity, allow_custom=True) assert bundle.objects[0].x_foo == "bar" assert '"x_foo": "bar"' in str(bundle) @@ -150,23 +152,23 @@ def test_custom_properties_dict_in_bundled_object(): 'x_foo': 'bar', }, } - bundle = stix2.Bundle(custom_identity) + bundle = stix2.v20.Bundle(custom_identity) assert bundle.objects[0].x_foo == "bar" assert '"x_foo": "bar"' in str(bundle) def test_custom_property_in_observed_data(): - artifact = stix2.File( + artifact = stix2.v20.File( allow_custom=True, name='test', - x_foo='bar' + x_foo='bar', ) - observed_data = stix2.ObservedData( + observed_data = stix2.v20.ObservedData( allow_custom=True, first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", - number_observed=0, + number_observed=1, objects={"0": artifact}, ) @@ -175,20 +177,20 @@ def test_custom_property_in_observed_data(): def test_custom_property_object_in_observable_extension(): - ntfs = stix2.NTFSExt( + ntfs = stix2.v20.NTFSExt( allow_custom=True, sid=1, x_foo='bar', ) - artifact = stix2.File( + artifact = stix2.v20.File( name='test', extensions={'ntfs-ext': ntfs}, ) - observed_data = stix2.ObservedData( + observed_data = stix2.v20.ObservedData( allow_custom=True, first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", - number_observed=0, + number_observed=1, objects={"0": artifact}, ) @@ -198,17 +200,17 @@ def test_custom_property_object_in_observable_extension(): def test_custom_property_dict_in_observable_extension(): with pytest.raises(stix2.exceptions.ExtraPropertiesError): - artifact = stix2.File( + stix2.v20.File( name='test', extensions={ 'ntfs-ext': { 'sid': 1, 'x_foo': 'bar', - } + }, }, ) - artifact = stix2.File( + artifact = stix2.v20.File( allow_custom=True, name='test', extensions={ @@ -216,14 +218,14 @@ def test_custom_property_dict_in_observable_extension(): 'allow_custom': True, 'sid': 1, 'x_foo': 'bar', - } + }, }, ) - observed_data = stix2.ObservedData( + observed_data = stix2.v20.ObservedData( allow_custom=True, first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", - number_observed=0, + number_observed=1, objects={"0": artifact}, ) @@ -237,15 +239,15 @@ def test_identity_custom_property_revoke(): def test_identity_custom_property_edit_markings(): - marking_obj = stix2.MarkingDefinition( + marking_obj = stix2.v20.MarkingDefinition( id=MARKING_DEFINITION_ID, definition_type="statement", - definition=stix2.StatementMarking(statement="Copyright 2016, Example Corp") + definition=stix2.v20.StatementMarking(statement="Copyright 2016, Example Corp"), ) - marking_obj2 = stix2.MarkingDefinition( + marking_obj2 = stix2.v20.MarkingDefinition( id=MARKING_DEFINITION_ID, definition_type="statement", - definition=stix2.StatementMarking(statement="Another one") + definition=stix2.v20.StatementMarking(statement="Another one"), ) # None of the following should throw exceptions @@ -258,9 +260,11 @@ def test_identity_custom_property_edit_markings(): def test_custom_marking_no_init_1(): - @stix2.CustomMarking('x-new-obj', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomMarking( + 'x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewObj(): pass @@ -269,9 +273,11 @@ def test_custom_marking_no_init_1(): def test_custom_marking_no_init_2(): - @stix2.CustomMarking('x-new-obj2', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomMarking( + 'x-new-obj2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewObj2(object): pass @@ -279,10 +285,12 @@ def test_custom_marking_no_init_2(): assert no2.property1 == 'something' -@stix2.sdo.CustomObject('x-new-type', [ - ('property1', stix2.properties.StringProperty(required=True)), - ('property2', stix2.properties.IntegerProperty()), -]) +@stix2.v20.CustomObject( + 'x-new-type', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], +) class NewType(object): def __init__(self, property2=None, **kwargs): if property2 and property2 < 10: @@ -312,9 +320,11 @@ def test_custom_object_type(): def test_custom_object_no_init_1(): - @stix2.sdo.CustomObject('x-new-obj', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomObject( + 'x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewObj(): pass @@ -323,9 +333,11 @@ def test_custom_object_no_init_1(): def test_custom_object_no_init_2(): - @stix2.sdo.CustomObject('x-new-obj2', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomObject( + 'x-new-obj2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewObj2(object): pass @@ -335,17 +347,21 @@ def test_custom_object_no_init_2(): def test_custom_object_invalid_type_name(): with pytest.raises(ValueError) as excinfo: - @stix2.sdo.CustomObject('x', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomObject( + 'x', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewObj(object): pass # pragma: no cover assert "Invalid type name 'x': " in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - @stix2.sdo.CustomObject('x_new_object', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomObject( + 'x_new_object', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewObj2(object): pass # pragma: no cover assert "Invalid type name 'x_new_object':" in str(excinfo.value) @@ -358,8 +374,8 @@ def test_parse_custom_object_type(): "property1": "something" }""" - nt = stix2.parse(nt_string) - assert nt.property1 == 'something' + nt = stix2.parse(nt_string, version="2.0", allow_custom=True) + assert nt["property1"] == 'something' def test_parse_unregistered_custom_object_type(): @@ -370,7 +386,7 @@ def test_parse_unregistered_custom_object_type(): }""" with pytest.raises(stix2.exceptions.ParseError) as excinfo: - stix2.parse(nt_string) + stix2.parse(nt_string, version="2.0") assert "Can't parse unknown object type" in str(excinfo.value) assert "use the CustomObject decorator." in str(excinfo.value) @@ -385,15 +401,17 @@ def test_parse_unregistered_custom_object_type_w_allow_custom(): "property1": "something" }""" - custom_obj = stix2.parse(nt_string, allow_custom=True) + custom_obj = stix2.parse(nt_string, version="2.0", allow_custom=True) assert custom_obj["type"] == "x-foobar-observable" -@stix2.observables.CustomObservable('x-new-observable', [ - ('property1', stix2.properties.StringProperty(required=True)), - ('property2', stix2.properties.IntegerProperty()), - ('x_property3', stix2.properties.BooleanProperty()), -]) +@stix2.v20.CustomObservable( + 'x-new-observable', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ('x_property3', stix2.properties.BooleanProperty()), + ], +) class NewObservable(): def __init__(self, property2=None, **kwargs): if property2 and property2 < 10: @@ -428,9 +446,11 @@ def test_custom_observable_raises_exception(): def test_custom_observable_object_no_init_1(): - @stix2.observables.CustomObservable('x-new-observable', [ - ('property1', stix2.properties.StringProperty()), - ]) + @stix2.v20.CustomObservable( + 'x-new-observable', [ + ('property1', stix2.properties.StringProperty()), + ], + ) class NewObs(): pass @@ -439,9 +459,11 @@ def test_custom_observable_object_no_init_1(): def test_custom_observable_object_no_init_2(): - @stix2.observables.CustomObservable('x-new-obs2', [ - ('property1', stix2.properties.StringProperty()), - ]) + @stix2.v20.CustomObservable( + 'x-new-obs2', [ + ('property1', stix2.properties.StringProperty()), + ], + ) class NewObs2(object): pass @@ -451,17 +473,21 @@ def test_custom_observable_object_no_init_2(): def test_custom_observable_object_invalid_type_name(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomObservable('x', [ - ('property1', stix2.properties.StringProperty()), - ]) + @stix2.v20.CustomObservable( + 'x', [ + ('property1', stix2.properties.StringProperty()), + ], + ) class NewObs(object): pass # pragma: no cover assert "Invalid observable type name 'x':" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomObservable('x_new_obs', [ - ('property1', stix2.properties.StringProperty()), - ]) + @stix2.v20.CustomObservable( + 'x_new_obs', [ + ('property1', stix2.properties.StringProperty()), + ], + ) class NewObs2(object): pass # pragma: no cover assert "Invalid observable type name 'x_new_obs':" in str(excinfo.value) @@ -469,9 +495,11 @@ def test_custom_observable_object_invalid_type_name(): def test_custom_observable_object_invalid_ref_property(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomObservable('x-new-obs', [ - ('property_ref', stix2.properties.StringProperty()), - ]) + @stix2.v20.CustomObservable( + 'x-new-obs', [ + ('property_ref', stix2.properties.StringProperty()), + ], + ) class NewObs(): pass assert "is named like an object reference property but is not an ObjectReferenceProperty" in str(excinfo.value) @@ -479,9 +507,11 @@ def test_custom_observable_object_invalid_ref_property(): def test_custom_observable_object_invalid_refs_property(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomObservable('x-new-obs', [ - ('property_refs', stix2.properties.StringProperty()), - ]) + @stix2.v20.CustomObservable( + 'x-new-obs', [ + ('property_refs', stix2.properties.StringProperty()), + ], + ) class NewObs(): pass assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) @@ -489,33 +519,39 @@ def test_custom_observable_object_invalid_refs_property(): def test_custom_observable_object_invalid_refs_list_property(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomObservable('x-new-obs', [ - ('property_refs', stix2.properties.ListProperty(stix2.properties.StringProperty)), - ]) + @stix2.v20.CustomObservable( + 'x-new-obs', [ + ('property_refs', stix2.properties.ListProperty(stix2.properties.StringProperty)), + ], + ) class NewObs(): pass assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) def test_custom_observable_object_invalid_valid_refs(): - @stix2.observables.CustomObservable('x-new-obs', [ - ('property1', stix2.properties.StringProperty(required=True)), - ('property_ref', stix2.properties.ObjectReferenceProperty(valid_types='email-addr')), - ]) + @stix2.v20.CustomObservable( + 'x-new-obs', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property_ref', stix2.properties.ObjectReferenceProperty(valid_types='email-addr')), + ], + ) class NewObs(): pass with pytest.raises(Exception) as excinfo: - NewObs(_valid_refs=['1'], - property1='something', - property_ref='1') + NewObs( + _valid_refs=['1'], + property1='something', + property_ref='1', + ) assert "must be created with _valid_refs as a dict, not a list" in str(excinfo.value) def test_custom_no_properties_raises_exception(): - with pytest.raises(ValueError): + with pytest.raises(TypeError): - @stix2.sdo.CustomObject('x-new-object-type') + @stix2.v20.CustomObject('x-new-object-type') class NewObject1(object): pass @@ -523,7 +559,7 @@ def test_custom_no_properties_raises_exception(): def test_custom_wrong_properties_arg_raises_exception(): with pytest.raises(ValueError): - @stix2.observables.CustomObservable('x-new-object-type', (("prop", stix2.properties.BooleanProperty()))) + @stix2.v20.CustomObservable('x-new-object-type', (("prop", stix2.properties.BooleanProperty()))) class NewObject2(object): pass @@ -534,8 +570,8 @@ def test_parse_custom_observable_object(): "property1": "something" }""" - nt = stix2.parse_observable(nt_string, []) - assert isinstance(nt, stix2.core._STIXBase) + nt = stix2.parse_observable(nt_string, [], version='2.0') + assert isinstance(nt, stix2.base._STIXBase) assert nt.property1 == 'something' @@ -546,14 +582,14 @@ def test_parse_unregistered_custom_observable_object(): }""" with pytest.raises(stix2.exceptions.CustomContentError) as excinfo: - stix2.parse_observable(nt_string) + stix2.parse_observable(nt_string, version='2.0') assert "Can't parse unknown observable type" in str(excinfo.value) - parsed_custom = stix2.parse_observable(nt_string, allow_custom=True) + parsed_custom = stix2.parse_observable(nt_string, allow_custom=True, version='2.0') assert parsed_custom['property1'] == 'something' with pytest.raises(AttributeError) as excinfo: assert parsed_custom.property1 == 'something' - assert not isinstance(parsed_custom, stix2.core._STIXBase) + assert not isinstance(parsed_custom, stix2.base._STIXBase) def test_parse_unregistered_custom_observable_object_with_no_type(): @@ -562,7 +598,7 @@ def test_parse_unregistered_custom_observable_object_with_no_type(): }""" with pytest.raises(stix2.exceptions.ParseError) as excinfo: - stix2.parse_observable(nt_string, allow_custom=True) + stix2.parse_observable(nt_string, allow_custom=True, version='2.0') assert "Can't parse observable with no 'type' property" in str(excinfo.value) @@ -582,7 +618,7 @@ def test_parse_observed_data_with_custom_observable(): } } }""" - parsed = stix2.parse(input_str, allow_custom=True) + parsed = stix2.parse(input_str, version="2.0", allow_custom=True) assert parsed.objects['0']['property1'] == 'something' @@ -592,7 +628,7 @@ def test_parse_invalid_custom_observable_object(): }""" with pytest.raises(stix2.exceptions.ParseError) as excinfo: - stix2.parse_observable(nt_string) + stix2.parse_observable(nt_string, version='2.0') assert "Can't parse observable with no 'type' property" in str(excinfo.value) @@ -634,7 +670,7 @@ def test_observable_custom_property_allowed(): def test_observed_data_with_custom_observable_object(): no = NewObservable(property1='something') - ob_data = stix2.ObservedData( + ob_data = stix2.v20.ObservedData( first_observed=FAKE_TIME, last_observed=FAKE_TIME, number_observed=1, @@ -644,10 +680,12 @@ def test_observed_data_with_custom_observable_object(): assert ob_data.objects['0'].property1 == 'something' -@stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext', [ - ('property1', stix2.properties.StringProperty(required=True)), - ('property2', stix2.properties.IntegerProperty()), -]) +@stix2.v20.CustomExtension( + stix2.v20.DomainName, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], +) class NewExtension(): def __init__(self, property2=None, **kwargs): if property2 and property2 < 10: @@ -670,7 +708,7 @@ def test_custom_extension(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: NewExtension(property2=42) assert excinfo.value.properties == ['property1'] - assert str(excinfo.value) == "No values for required properties for _Custom: (property1)." + assert str(excinfo.value) == "No values for required properties for _CustomExtension: (property1)." with pytest.raises(ValueError) as excinfo: NewExtension(property1='something', property2=4) @@ -681,16 +719,19 @@ def test_custom_extension_wrong_observable_type(): # NewExtension is an extension of DomainName, not File ext = NewExtension(property1='something') with pytest.raises(ValueError) as excinfo: - stix2.File(name="abc.txt", - extensions={ - "ntfs-ext": ext, - }) + stix2.v20.File( + name="abc.txt", + extensions={ + "ntfs-ext": ext, + }, + ) assert 'Cannot determine extension type' in excinfo.value.reason -@pytest.mark.parametrize("data", [ - """{ +@pytest.mark.parametrize( + "data", [ + """{ "keys": [ { "test123": 123, @@ -698,11 +739,14 @@ def test_custom_extension_wrong_observable_type(): } ] }""", -]) + ], +) def test_custom_extension_with_list_and_dict_properties_observable_type(data): - @stix2.observables.CustomExtension(stix2.UserAccount, 'some-extension', [ - ('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True)) - ]) + @stix2.v20.CustomExtension( + stix2.v20.UserAccount, 'some-extension', [ + ('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True)), + ], + ) class SomeCustomExtension: pass @@ -716,30 +760,36 @@ def test_custom_extension_invalid_observable(): class Foo(object): pass with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(Foo, 'x-new-ext', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomExtension( + Foo, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class FooExtension(): pass # pragma: no cover assert str(excinfo.value) == "'observable' must be a valid Observable class!" - class Bar(stix2.observables._Observable): + class Bar(stix2.v20.observables._Observable): pass with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(Bar, 'x-new-ext', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomExtension( + Bar, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class BarExtension(): pass assert "Unknown observable type" in str(excinfo.value) assert "Custom observables must be created with the @CustomObservable decorator." in str(excinfo.value) - class Baz(stix2.observables._Observable): + class Baz(stix2.v20.observables._Observable): _type = 'Baz' with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(Baz, 'x-new-ext', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomExtension( + Baz, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class BazExtension(): pass assert "Unknown observable type" in str(excinfo.value) @@ -748,17 +798,21 @@ def test_custom_extension_invalid_observable(): def test_custom_extension_invalid_type_name(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(stix2.File, 'x', { - 'property1': stix2.properties.StringProperty(required=True), - }) + @stix2.v20.CustomExtension( + stix2.v20.File, 'x', { + 'property1': stix2.properties.StringProperty(required=True), + }, + ) class FooExtension(): pass # pragma: no cover assert "Invalid extension type name 'x':" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(stix2.File, 'x_new_ext', { - 'property1': stix2.properties.StringProperty(required=True), - }) + @stix2.v20.CustomExtension( + stix2.File, 'x_new_ext', { + 'property1': stix2.properties.StringProperty(required=True), + }, + ) class BlaExtension(): pass # pragma: no cover assert "Invalid extension type name 'x_new_ext':" in str(excinfo.value) @@ -766,7 +820,7 @@ def test_custom_extension_invalid_type_name(): def test_custom_extension_no_properties(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', None) + @stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', None) class BarExtension(): pass assert "Must supply a list, containing tuples." in str(excinfo.value) @@ -774,7 +828,7 @@ def test_custom_extension_no_properties(): def test_custom_extension_empty_properties(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', []) + @stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', []) class BarExtension(): pass assert "Must supply a list, containing tuples." in str(excinfo.value) @@ -782,16 +836,18 @@ def test_custom_extension_empty_properties(): def test_custom_extension_dict_properties(): with pytest.raises(ValueError) as excinfo: - @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', {}) + @stix2.v20.CustomExtension(stix2.v20.DomainName, 'x-new-ext2', {}) class BarExtension(): pass assert "Must supply a list, containing tuples." in str(excinfo.value) def test_custom_extension_no_init_1(): - @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-extension', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomExtension( + stix2.v20.DomainName, 'x-new-extension', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewExt(): pass @@ -800,9 +856,11 @@ def test_custom_extension_no_init_1(): def test_custom_extension_no_init_2(): - @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', [ - ('property1', stix2.properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomExtension( + stix2.v20.DomainName, 'x-new-ext2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewExt2(object): pass @@ -822,13 +880,14 @@ def test_parse_observable_with_custom_extension(): } }""" - parsed = stix2.parse_observable(input_str) + parsed = stix2.parse_observable(input_str, version='2.0') assert parsed.extensions['x-new-ext'].property2 == 12 -@pytest.mark.parametrize("data", [ - # URL is not in EXT_MAP - """{ +@pytest.mark.parametrize( + "data", [ + # URL is not in EXT_MAP + """{ "type": "url", "value": "example.com", "extensions": { @@ -838,8 +897,8 @@ def test_parse_observable_with_custom_extension(): } } }""", - # File is in EXT_MAP - """{ + # File is in EXT_MAP + """{ "type": "file", "name": "foo.txt", "extensions": { @@ -849,15 +908,16 @@ def test_parse_observable_with_custom_extension(): } } }""", -]) + ], +) def test_parse_observable_with_unregistered_custom_extension(data): with pytest.raises(ValueError) as excinfo: - stix2.parse_observable(data) + stix2.parse_observable(data, version='2.0') assert "Can't parse unknown extension type" in str(excinfo.value) - parsed_ob = stix2.parse_observable(data, allow_custom=True) + parsed_ob = stix2.parse_observable(data, allow_custom=True, version='2.0') assert parsed_ob['extensions']['x-foobar-ext']['property1'] == 'foo' - assert not isinstance(parsed_ob['extensions']['x-foobar-ext'], stix2.core._STIXBase) + assert not isinstance(parsed_ob['extensions']['x-foobar-ext'], stix2.base._STIXBase) def test_register_custom_object(): @@ -865,18 +925,19 @@ def test_register_custom_object(): class CustomObject2(object): _type = 'awesome-object' - stix2._register_type(CustomObject2) + stix2.core._register_object(CustomObject2, version="2.0") # Note that we will always check against newest OBJ_MAP. - assert (CustomObject2._type, CustomObject2) in stix2.OBJ_MAP.items() + assert (CustomObject2._type, CustomObject2) in stix2.v20.OBJ_MAP.items() def test_extension_property_location(): - assert 'extensions' in stix2.v20.observables.OBJ_MAP_OBSERVABLE['x-new-observable']._properties - assert 'extensions' not in stix2.v20.observables.EXT_MAP['domain-name']['x-new-ext']._properties + assert 'extensions' in stix2.v20.OBJ_MAP_OBSERVABLE['x-new-observable']._properties + assert 'extensions' not in stix2.v20.EXT_MAP['domain-name']['x-new-ext']._properties -@pytest.mark.parametrize("data", [ - """{ +@pytest.mark.parametrize( + "data", [ + """{ "type": "x-example", "id": "x-example--336d8a9f-91f1-46c5-b142-6441bb9f8b8d", "created": "2018-06-12T16:20:58.059Z", @@ -888,18 +949,23 @@ def test_extension_property_location(): } } }""", -]) + ], +) def test_custom_object_nested_dictionary(data): - @stix2.sdo.CustomObject('x-example', [ - ('dictionary', stix2.properties.DictionaryProperty()), - ]) + @stix2.v20.CustomObject( + 'x-example', [ + ('dictionary', stix2.properties.DictionaryProperty()), + ], + ) class Example(object): def __init__(self, **kwargs): pass - example = Example(id='x-example--336d8a9f-91f1-46c5-b142-6441bb9f8b8d', - created='2018-06-12T16:20:58.059Z', - modified='2018-06-12T16:20:58.059Z', - dictionary={'key': {'key_b': 'value', 'key_a': 'value'}}) + example = Example( + id='x-example--336d8a9f-91f1-46c5-b142-6441bb9f8b8d', + created='2018-06-12T16:20:58.059Z', + modified='2018-06-12T16:20:58.059Z', + dictionary={'key': {'key_b': 'value', 'key_a': 'value'}}, + ) assert data == str(example) diff --git a/stix2/test/test_datastore.py b/stix2/test/v20/test_datastore.py similarity index 83% rename from stix2/test/test_datastore.py rename to stix2/test/v20/test_datastore.py index 5c159cf..8bb5494 100644 --- a/stix2/test/test_datastore.py +++ b/stix2/test/v20/test_datastore.py @@ -1,9 +1,11 @@ import pytest -from stix2.datastore import (CompositeDataSource, DataSink, DataSource, - DataStoreMixin) +from stix2.datastore import ( + CompositeDataSource, DataSink, DataSource, DataStoreMixin, +) from stix2.datastore.filters import Filter -from stix2.test.constants import CAMPAIGN_MORE_KWARGS + +from .constants import CAMPAIGN_MORE_KWARGS def test_datasource_abstract_class_raises_error(): @@ -46,15 +48,19 @@ def test_datastore_creator_of_raises(): def test_datastore_relationships_raises(): with pytest.raises(AttributeError) as excinfo: - DataStoreMixin().relationships(obj="indicator--00000000-0000-4000-8000-000000000001", - target_only=True) + DataStoreMixin().relationships( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) assert "DataStoreMixin has no data source to query" == str(excinfo.value) def test_datastore_related_to_raises(): with pytest.raises(AttributeError) as excinfo: - DataStoreMixin().related_to(obj="indicator--00000000-0000-4000-8000-000000000001", - target_only=True) + DataStoreMixin().related_to( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) assert "DataStoreMixin has no data source to query" == str(excinfo.value) @@ -84,15 +90,19 @@ def test_composite_datastore_query_raises_error(): def test_composite_datastore_relationships_raises_error(): with pytest.raises(AttributeError) as excinfo: - CompositeDataSource().relationships(obj="indicator--00000000-0000-4000-8000-000000000001", - target_only=True) + CompositeDataSource().relationships( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) assert "CompositeDataSource has no data sources" == str(excinfo.value) def test_composite_datastore_related_to_raises_error(): with pytest.raises(AttributeError) as excinfo: - CompositeDataSource().related_to(obj="indicator--00000000-0000-4000-8000-000000000001", - target_only=True) + CompositeDataSource().related_to( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) assert "CompositeDataSource has no data sources" == str(excinfo.value) diff --git a/stix2/test/test_datastore_memory.py b/stix2/test/v20/test_datastore_composite.py similarity index 91% rename from stix2/test/test_datastore_memory.py rename to stix2/test/v20/test_datastore_composite.py index cdb3818..640458d 100644 --- a/stix2/test/test_datastore_memory.py +++ b/stix2/test/v20/test_datastore_composite.py @@ -15,8 +15,10 @@ def test_add_remove_composite_datasource(): with pytest.raises(TypeError) as excinfo: cds.add_data_sources([ds1, ds2, ds1, ds3]) - assert str(excinfo.value) == ("DataSource (to be added) is not of type " - "stix2.DataSource. DataSource type is ''") + assert str(excinfo.value) == ( + "DataSource (to be added) is not of type " + "stix2.DataSource. DataSource type is ''" + ) cds.add_data_sources([ds1, ds2, ds1]) @@ -28,10 +30,12 @@ def test_add_remove_composite_datasource(): def test_composite_datasource_operations(stix_objs1, stix_objs2): - BUNDLE1 = dict(id="bundle--%s" % make_id(), - objects=stix_objs1, - spec_version="2.0", - type="bundle") + BUNDLE1 = dict( + id="bundle--%s" % make_id(), + objects=stix_objs1, + spec_version="2.0", + type="bundle", + ) cds1 = CompositeDataSource() ds1_1 = MemorySource(stix_data=BUNDLE1) ds1_2 = MemorySource(stix_data=stix_objs2) @@ -57,11 +61,11 @@ def test_composite_datasource_operations(stix_objs1, stix_objs2): assert indicator["type"] == "indicator" query1 = [ - Filter("type", "=", "indicator") + Filter("type", "=", "indicator"), ] query2 = [ - Filter("valid_from", "=", "2017-01-27T13:49:53.935382Z") + Filter("valid_from", "=", "2017-01-27T13:49:53.935382Z"), ] cds1.filters.add(query2) diff --git a/stix2/test/test_datastore_filesystem.py b/stix2/test/v20/test_datastore_filesystem.py similarity index 74% rename from stix2/test/test_datastore_filesystem.py rename to stix2/test/v20/test_datastore_filesystem.py index 1c51fa1..84a3034 100644 --- a/stix2/test/test_datastore_filesystem.py +++ b/stix2/test/v20/test_datastore_filesystem.py @@ -8,18 +8,17 @@ import stat import pytest import pytz -from stix2 import (Bundle, Campaign, CustomObject, FileSystemSink, - FileSystemSource, FileSystemStore, Filter, Identity, - Indicator, Malware, MarkingDefinition, Relationship, - TLPMarking, parse, properties) -from stix2.datastore.filesystem import (AuthSet, _find_search_optimizations, - _get_matching_dir_entries, - _timestamp2filename) +import stix2 +from stix2.datastore.filesystem import ( + AuthSet, _find_search_optimizations, _get_matching_dir_entries, + _timestamp2filename, +) from stix2.exceptions import STIXError -from stix2.test.constants import (CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, - IDENTITY_KWARGS, INDICATOR_ID, - INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, - RELATIONSHIP_IDS) + +from .constants import ( + CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, INDICATOR_ID, + INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS, +) FS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") @@ -27,7 +26,7 @@ FS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data" @pytest.fixture def fs_store(): # create - yield FileSystemStore(FS_PATH) + yield stix2.FileSystemStore(FS_PATH) # remove campaign dir shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) @@ -36,7 +35,7 @@ def fs_store(): @pytest.fixture def fs_source(): # create - fs = FileSystemSource(FS_PATH) + fs = stix2.FileSystemSource(FS_PATH) assert fs.stix_dir == FS_PATH yield fs @@ -47,7 +46,7 @@ def fs_source(): @pytest.fixture def fs_sink(): # create - fs = FileSystemSink(FS_PATH) + fs = stix2.FileSystemSink(FS_PATH) assert fs.stix_dir == FS_PATH yield fs @@ -78,7 +77,7 @@ def bad_stix_files(): # bad STIX object stix_obj = { "id": "intrusion-set--test-bad-stix", - "spec_version": "2.0" + "spec_version": "2.0", # no "type" field } @@ -92,22 +91,24 @@ def bad_stix_files(): @pytest.fixture(scope='module') def rel_fs_store(): - cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) - idy = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) - ind = Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) - mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) - rel1 = Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) - rel2 = Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) - rel3 = Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + cam = stix2.v20.Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = stix2.v20.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = stix2.v20.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = stix2.v20.Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = stix2.v20.Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = stix2.v20.Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = stix2.v20.Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] - fs = FileSystemStore(FS_PATH) + fs = stix2.FileSystemStore(FS_PATH) for o in stix_objs: fs.add(o) yield fs for o in stix_objs: - filepath = os.path.join(FS_PATH, o.type, o.id, - _timestamp2filename(o.modified) + '.json') + filepath = os.path.join( + FS_PATH, o.type, o.id, + _timestamp2filename(o.modified) + '.json', + ) # Some test-scoped fixtures (e.g. fs_store) delete all campaigns, so by # the time this module-scoped fixture tears itself down, it may find @@ -124,13 +125,13 @@ def rel_fs_store(): def test_filesystem_source_nonexistent_folder(): with pytest.raises(ValueError) as excinfo: - FileSystemSource('nonexistent-folder') + stix2.FileSystemSource('nonexistent-folder') assert "for STIX data does not exist" in str(excinfo) def test_filesystem_sink_nonexistent_folder(): with pytest.raises(ValueError) as excinfo: - FileSystemSink('nonexistent-folder') + stix2.FileSystemSink('nonexistent-folder') assert "for STIX data does not exist" in str(excinfo) @@ -158,8 +159,10 @@ def test_filesystem_source_get_object(fs_source): mal = fs_source.get("malware--6b616fc1-1505-48e3-8b2c-0d19337bff38") assert mal.id == "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38" assert mal.name == "Rover" - assert mal.modified == datetime.datetime(2018, 11, 16, 22, 54, 20, 390000, - pytz.utc) + assert mal.modified == datetime.datetime( + 2018, 11, 16, 22, 54, 20, 390000, + pytz.utc, + ) def test_filesystem_source_get_nonexistent_object(fs_source): @@ -169,18 +172,20 @@ def test_filesystem_source_get_nonexistent_object(fs_source): def test_filesystem_source_all_versions(fs_source): ids = fs_source.all_versions( - "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" + "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", ) assert len(ids) == 2 - assert all(id_.id == "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" - for id_ in ids) + assert all( + id_.id == "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" + for id_ in ids + ) assert all(id_.name == "The MITRE Corporation" for id_ in ids) assert all(id_.type == "identity" for id_ in ids) def test_filesystem_source_query_single(fs_source): # query2 - is_2 = fs_source.query([Filter("external_references.external_id", '=', "T1027")]) + is_2 = fs_source.query([stix2.Filter("external_references.external_id", '=', "T1027")]) assert len(is_2) == 1 is_2 = is_2[0] @@ -188,9 +193,9 @@ def test_filesystem_source_query_single(fs_source): assert is_2.type == "attack-pattern" -def test_filesytem_source_query_multiple(fs_source): +def test_filesystem_source_query_multiple(fs_source): # query - intrusion_sets = fs_source.query([Filter("type", '=', "intrusion-set")]) + intrusion_sets = fs_source.query([stix2.Filter("type", '=', "intrusion-set")]) assert len(intrusion_sets) == 2 assert "intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064" in [is_.id for is_ in intrusion_sets] assert "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a" in [is_.id for is_ in intrusion_sets] @@ -205,9 +210,9 @@ def test_filesystem_source_backward_compatible(fs_source): # it. modified = datetime.datetime(2018, 11, 16, 22, 54, 20, 390000, pytz.utc) results = fs_source.query([ - Filter("type", "=", "malware"), - Filter("id", "=", "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"), - Filter("modified", "=", modified) + stix2.Filter("type", "=", "malware"), + stix2.Filter("id", "=", "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"), + stix2.Filter("modified", "=", modified), ]) assert len(results) == 1 @@ -220,14 +225,18 @@ def test_filesystem_source_backward_compatible(fs_source): def test_filesystem_sink_add_python_stix_object(fs_sink, fs_source): # add python stix object - camp1 = Campaign(name="Hannibal", - objective="Targeting Italian and Spanish Diplomat internet accounts", - aliases=["War Elephant"]) + camp1 = stix2.v20.Campaign( + name="Hannibal", + objective="Targeting Italian and Spanish Diplomat internet accounts", + aliases=["War Elephant"], + ) fs_sink.add(camp1) - filepath = os.path.join(FS_PATH, "campaign", camp1.id, - _timestamp2filename(camp1.modified) + ".json") + filepath = os.path.join( + FS_PATH, "campaign", camp1.id, + _timestamp2filename(camp1.modified) + ".json", + ) assert os.path.exists(filepath) camp1_r = fs_source.get(camp1.id) @@ -247,7 +256,7 @@ def test_filesystem_sink_add_stix_object_dict(fs_sink, fs_source): "aliases": ["Purple Robes"], "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "created": "2017-05-31T21:31:53.197755Z", - "modified": "2017-05-31T21:31:53.197755Z" + "modified": "2017-05-31T21:31:53.197755Z", } fs_sink.add(camp2) @@ -258,9 +267,11 @@ def test_filesystem_sink_add_stix_object_dict(fs_sink, fs_source): # as what's in the dict, since the parsing process can enforce a precision # constraint (e.g. truncate to milliseconds), which results in a slightly # different name. - camp2obj = parse(camp2) - filepath = os.path.join(FS_PATH, "campaign", camp2obj["id"], - _timestamp2filename(camp2obj["modified"]) + ".json") + camp2obj = stix2.parse(camp2) + filepath = os.path.join( + FS_PATH, "campaign", camp2obj["id"], + _timestamp2filename(camp2obj["modified"]) + ".json", + ) assert os.path.exists(filepath) @@ -286,16 +297,18 @@ def test_filesystem_sink_add_stix_bundle_dict(fs_sink, fs_source): "aliases": ["Huns"], "id": "campaign--b8f86161-ccae-49de-973a-4ca320c62478", "created": "2017-05-31T21:31:53.197755Z", - "modified": "2017-05-31T21:31:53.197755Z" - } - ] + "modified": "2017-05-31T21:31:53.197755Z", + }, + ], } fs_sink.add(bund) - camp_obj = parse(bund["objects"][0]) - filepath = os.path.join(FS_PATH, "campaign", camp_obj["id"], - _timestamp2filename(camp_obj["modified"]) + ".json") + camp_obj = stix2.parse(bund["objects"][0]) + filepath = os.path.join( + FS_PATH, "campaign", camp_obj["id"], + _timestamp2filename(camp_obj["modified"]) + ".json", + ) assert os.path.exists(filepath) @@ -316,10 +329,12 @@ def test_filesystem_sink_add_json_stix_object(fs_sink, fs_source): fs_sink.add(camp4) - camp4obj = parse(camp4) - filepath = os.path.join(FS_PATH, "campaign", - "campaign--6a6ca372-ba07-42cc-81ef-9840fc1f963d", - _timestamp2filename(camp4obj["modified"]) + ".json") + camp4obj = stix2.parse(camp4) + filepath = os.path.join( + FS_PATH, "campaign", + "campaign--6a6ca372-ba07-42cc-81ef-9840fc1f963d", + _timestamp2filename(camp4obj["modified"]) + ".json", + ) assert os.path.exists(filepath) @@ -339,12 +354,14 @@ def test_filesystem_sink_json_stix_bundle(fs_sink, fs_source): ' "name": "Spartacus", "objective": "Oppressive regimes of Africa and Middle East"}]}' fs_sink.add(bund2) - bund2obj = parse(bund2) + bund2obj = stix2.parse(bund2) camp_obj = bund2obj["objects"][0] - filepath = os.path.join(FS_PATH, "campaign", - "campaign--2c03b8bf-82ee-433e-9918-ca2cb6e9534b", - _timestamp2filename(camp_obj["modified"]) + ".json") + filepath = os.path.join( + FS_PATH, "campaign", + "campaign--2c03b8bf-82ee-433e-9918-ca2cb6e9534b", + _timestamp2filename(camp_obj["modified"]) + ".json", + ) assert os.path.exists(filepath) @@ -357,9 +374,11 @@ def test_filesystem_sink_json_stix_bundle(fs_sink, fs_source): def test_filesystem_sink_add_objects_list(fs_sink, fs_source): # add list of objects - camp6 = Campaign(name="Comanche", - objective="US Midwest manufacturing firms, oil refineries, and businesses", - aliases=["Horse Warrior"]) + camp6 = stix2.v20.Campaign( + name="Comanche", + objective="US Midwest manufacturing firms, oil refineries, and businesses", + aliases=["Horse Warrior"], + ) camp7 = { "name": "Napolean", @@ -368,19 +387,22 @@ def test_filesystem_sink_add_objects_list(fs_sink, fs_source): "aliases": ["The Frenchmen"], "id": "campaign--122818b6-1112-4fb0-b11b-b111107ca70a", "created": "2017-05-31T21:31:53.197755Z", - "modified": "2017-05-31T21:31:53.197755Z" + "modified": "2017-05-31T21:31:53.197755Z", } fs_sink.add([camp6, camp7]) - camp7obj = parse(camp7) + camp7obj = stix2.parse(camp7) - camp6filepath = os.path.join(FS_PATH, "campaign", camp6.id, - _timestamp2filename(camp6["modified"]) + - ".json") + camp6filepath = os.path.join( + FS_PATH, "campaign", camp6.id, + _timestamp2filename(camp6["modified"]) + + ".json", + ) camp7filepath = os.path.join( FS_PATH, "campaign", "campaign--122818b6-1112-4fb0-b11b-b111107ca70a", - _timestamp2filename(camp7obj["modified"]) + ".json") + _timestamp2filename(camp7obj["modified"]) + ".json", + ) assert os.path.exists(camp6filepath) assert os.path.exists(camp7filepath) @@ -399,14 +421,14 @@ def test_filesystem_sink_add_objects_list(fs_sink, fs_source): def test_filesystem_sink_marking(fs_sink): - marking = MarkingDefinition( + marking = stix2.v20.MarkingDefinition( definition_type="tlp", - definition=TLPMarking(tlp="green") + definition=stix2.v20.TLPMarking(tlp="green"), ) fs_sink.add(marking) marking_filepath = os.path.join( - FS_PATH, "marking-definition", marking["id"] + ".json" + FS_PATH, "marking-definition", marking["id"] + ".json", ) assert os.path.exists(marking_filepath) @@ -436,14 +458,14 @@ def test_filesystem_store_all_versions(fs_store): def test_filesystem_store_query(fs_store): # query() - tools = fs_store.query([Filter("labels", "in", "tool")]) + tools = fs_store.query([stix2.Filter("labels", "in", "tool")]) assert len(tools) == 2 assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools] assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools] def test_filesystem_store_query_single_filter(fs_store): - query = Filter("labels", "in", "tool") + query = stix2.Filter("labels", "in", "tool") tools = fs_store.query(query) assert len(tools) == 2 assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools] @@ -458,45 +480,53 @@ def test_filesystem_store_empty_query(fs_store): def test_filesystem_store_query_multiple_filters(fs_store): - fs_store.source.filters.add(Filter("labels", "in", "tool")) - tools = fs_store.query(Filter("id", "=", "tool--242f3da3-4425-4d11-8f5c-b842886da966")) + fs_store.source.filters.add(stix2.Filter("labels", "in", "tool")) + tools = fs_store.query(stix2.Filter("id", "=", "tool--242f3da3-4425-4d11-8f5c-b842886da966")) assert len(tools) == 1 assert tools[0].id == "tool--242f3da3-4425-4d11-8f5c-b842886da966" def test_filesystem_store_query_dont_include_type_folder(fs_store): - results = fs_store.query(Filter("type", "!=", "tool")) + results = fs_store.query(stix2.Filter("type", "!=", "tool")) assert len(results) == 28 def test_filesystem_store_add(fs_store): # add() - camp1 = Campaign(name="Great Heathen Army", - objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", - aliases=["Ragnar"]) + camp1 = stix2.v20.Campaign( + name="Great Heathen Army", + objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", + aliases=["Ragnar"], + ) fs_store.add(camp1) camp1_r = fs_store.get(camp1.id) assert camp1_r.id == camp1.id assert camp1_r.name == camp1.name - filepath = os.path.join(FS_PATH, "campaign", camp1_r.id, - _timestamp2filename(camp1_r.modified) + ".json") + filepath = os.path.join( + FS_PATH, "campaign", camp1_r.id, + _timestamp2filename(camp1_r.modified) + ".json", + ) # remove os.remove(filepath) def test_filesystem_store_add_as_bundle(): - fs_store = FileSystemStore(FS_PATH, bundlify=True) + fs_store = stix2.FileSystemStore(FS_PATH, bundlify=True) - camp1 = Campaign(name="Great Heathen Army", - objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", - aliases=["Ragnar"]) + camp1 = stix2.v20.Campaign( + name="Great Heathen Army", + objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", + aliases=["Ragnar"], + ) fs_store.add(camp1) - filepath = os.path.join(FS_PATH, "campaign", camp1.id, - _timestamp2filename(camp1.modified) + ".json") + filepath = os.path.join( + FS_PATH, "campaign", camp1.id, + _timestamp2filename(camp1.modified) + ".json", + ) with open(filepath) as bundle_file: assert '"type": "bundle"' in bundle_file.read() @@ -509,7 +539,7 @@ def test_filesystem_store_add_as_bundle(): def test_filesystem_add_bundle_object(fs_store): - bundle = Bundle() + bundle = stix2.v20.Bundle() fs_store.add(bundle) @@ -524,14 +554,14 @@ def test_filesystem_store_add_invalid_object(fs_store): def test_filesystem_store_add_marking(fs_store): - marking = MarkingDefinition( + marking = stix2.v20.MarkingDefinition( definition_type="tlp", - definition=TLPMarking(tlp="green") + definition=stix2.v20.TLPMarking(tlp="green"), ) fs_store.add(marking) marking_filepath = os.path.join( - FS_PATH, "marking-definition", marking["id"] + ".json" + FS_PATH, "marking-definition", marking["id"] + ".json", ) assert os.path.exists(marking_filepath) @@ -544,12 +574,14 @@ def test_filesystem_store_add_marking(fs_store): def test_filesystem_object_with_custom_property(fs_store): - camp = Campaign(name="Scipio Africanus", - objective="Defeat the Carthaginians", - x_empire="Roman", - allow_custom=True) + camp = stix2.v20.Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) - fs_store.add(camp, True) + fs_store.add(camp) camp_r = fs_store.get(camp.id) assert camp_r.id == camp.id @@ -557,12 +589,14 @@ def test_filesystem_object_with_custom_property(fs_store): def test_filesystem_object_with_custom_property_in_bundle(fs_store): - camp = Campaign(name="Scipio Africanus", - objective="Defeat the Carthaginians", - x_empire="Roman", - allow_custom=True) + camp = stix2.v20.Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) - bundle = Bundle(camp, allow_custom=True) + bundle = stix2.v20.Bundle(camp, allow_custom=True) fs_store.add(bundle) camp_r = fs_store.get(camp.id) @@ -571,9 +605,11 @@ def test_filesystem_object_with_custom_property_in_bundle(fs_store): def test_filesystem_custom_object(fs_store): - @CustomObject('x-new-obj', [ - ('property1', properties.StringProperty(required=True)), - ]) + @stix2.v20.CustomObject( + 'x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) class NewObj(): pass @@ -581,8 +617,8 @@ def test_filesystem_custom_object(fs_store): fs_store.add(newobj) newobj_r = fs_store.get(newobj.id) - assert newobj_r.id == newobj.id - assert newobj_r.property1 == 'something' + assert newobj_r["id"] == newobj["id"] + assert newobj_r["property1"] == 'something' # remove dir shutil.rmtree(os.path.join(FS_PATH, "x-new-obj"), True) @@ -690,7 +726,7 @@ def test_auth_set_black1(): def test_optimize_types1(): filters = [ - Filter("type", "=", "foo") + stix2.Filter("type", "=", "foo"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -703,8 +739,8 @@ def test_optimize_types1(): def test_optimize_types2(): filters = [ - Filter("type", "=", "foo"), - Filter("type", "=", "bar") + stix2.Filter("type", "=", "foo"), + stix2.Filter("type", "=", "bar"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -717,8 +753,8 @@ def test_optimize_types2(): def test_optimize_types3(): filters = [ - Filter("type", "in", ["A", "B", "C"]), - Filter("type", "in", ["B", "C", "D"]) + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter("type", "in", ["B", "C", "D"]), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -731,8 +767,8 @@ def test_optimize_types3(): def test_optimize_types4(): filters = [ - Filter("type", "in", ["A", "B", "C"]), - Filter("type", "in", ["D", "E", "F"]) + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter("type", "in", ["D", "E", "F"]), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -745,8 +781,8 @@ def test_optimize_types4(): def test_optimize_types5(): filters = [ - Filter("type", "in", ["foo", "bar"]), - Filter("type", "!=", "bar") + stix2.Filter("type", "in", ["foo", "bar"]), + stix2.Filter("type", "!=", "bar"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -759,8 +795,8 @@ def test_optimize_types5(): def test_optimize_types6(): filters = [ - Filter("type", "!=", "foo"), - Filter("type", "!=", "bar") + stix2.Filter("type", "!=", "foo"), + stix2.Filter("type", "!=", "bar"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -773,8 +809,8 @@ def test_optimize_types6(): def test_optimize_types7(): filters = [ - Filter("type", "=", "foo"), - Filter("type", "!=", "foo") + stix2.Filter("type", "=", "foo"), + stix2.Filter("type", "!=", "foo"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -798,8 +834,8 @@ def test_optimize_types8(): def test_optimize_types_ids1(): filters = [ - Filter("type", "in", ["foo", "bar"]), - Filter("id", "=", "foo--00000000-0000-0000-0000-000000000000") + stix2.Filter("type", "in", ["foo", "bar"]), + stix2.Filter("id", "=", "foo--00000000-0000-0000-0000-000000000000"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -812,8 +848,8 @@ def test_optimize_types_ids1(): def test_optimize_types_ids2(): filters = [ - Filter("type", "=", "foo"), - Filter("id", "=", "bar--00000000-0000-0000-0000-000000000000") + stix2.Filter("type", "=", "foo"), + stix2.Filter("id", "=", "bar--00000000-0000-0000-0000-000000000000"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -826,8 +862,8 @@ def test_optimize_types_ids2(): def test_optimize_types_ids3(): filters = [ - Filter("type", "in", ["foo", "bar"]), - Filter("id", "!=", "bar--00000000-0000-0000-0000-000000000000") + stix2.Filter("type", "in", ["foo", "bar"]), + stix2.Filter("id", "!=", "bar--00000000-0000-0000-0000-000000000000"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -840,12 +876,14 @@ def test_optimize_types_ids3(): def test_optimize_types_ids4(): filters = [ - Filter("type", "in", ["A", "B", "C"]), - Filter("id", "in", [ - "B--00000000-0000-0000-0000-000000000000", - "C--00000000-0000-0000-0000-000000000000", - "D--00000000-0000-0000-0000-000000000000", - ]) + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter( + "id", "in", [ + "B--00000000-0000-0000-0000-000000000000", + "C--00000000-0000-0000-0000-000000000000", + "D--00000000-0000-0000-0000-000000000000", + ], + ), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -855,20 +893,22 @@ def test_optimize_types_ids4(): assert auth_ids.auth_type == AuthSet.WHITE assert auth_ids.values == { "B--00000000-0000-0000-0000-000000000000", - "C--00000000-0000-0000-0000-000000000000" + "C--00000000-0000-0000-0000-000000000000", } def test_optimize_types_ids5(): filters = [ - Filter("type", "in", ["A", "B", "C"]), - Filter("type", "!=", "C"), - Filter("id", "in", [ - "B--00000000-0000-0000-0000-000000000000", - "C--00000000-0000-0000-0000-000000000000", - "D--00000000-0000-0000-0000-000000000000" - ]), - Filter("id", "!=", "D--00000000-0000-0000-0000-000000000000") + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter("type", "!=", "C"), + stix2.Filter( + "id", "in", [ + "B--00000000-0000-0000-0000-000000000000", + "C--00000000-0000-0000-0000-000000000000", + "D--00000000-0000-0000-0000-000000000000", + ], + ), + stix2.Filter("id", "!=", "D--00000000-0000-0000-0000-000000000000"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -881,7 +921,7 @@ def test_optimize_types_ids5(): def test_optimize_types_ids6(): filters = [ - Filter("id", "=", "A--00000000-0000-0000-0000-000000000000") + stix2.Filter("id", "=", "A--00000000-0000-0000-0000-000000000000"), ] auth_types, auth_ids = _find_search_optimizations(filters) @@ -895,7 +935,7 @@ def test_optimize_types_ids6(): def test_search_auth_set_white1(): auth_set = AuthSet( {"attack-pattern", "doesntexist"}, - set() + set(), ) results = _get_matching_dir_entries(FS_PATH, auth_set, stat.S_ISDIR) @@ -909,19 +949,19 @@ def test_search_auth_set_white2(): auth_set = AuthSet( { "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", - "malware--92ec0cbd-2c30-44a2-b270-73f4ec949841" + "malware--92ec0cbd-2c30-44a2-b270-73f4ec949841", }, { "malware--92ec0cbd-2c30-44a2-b270-73f4ec949841", "malware--96b08451-b27a-4ff6-893f-790e26393a8e", - "doesntexist" - } + "doesntexist", + }, ) results = _get_matching_dir_entries( os.path.join(FS_PATH, "malware"), - auth_set, stat.S_ISDIR + auth_set, stat.S_ISDIR, ) assert results == ["malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"] @@ -931,9 +971,11 @@ def test_search_auth_set_white3(): auth_set = AuthSet({"20170531213258226477", "doesntexist"}, set()) results = _get_matching_dir_entries( - os.path.join(FS_PATH, "malware", - "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"), - auth_set, stat.S_ISREG, ".json" + os.path.join( + FS_PATH, "malware", + "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + ), + auth_set, stat.S_ISREG, ".json", ) assert results == ["20170531213258226477.json"] @@ -942,23 +984,23 @@ def test_search_auth_set_white3(): def test_search_auth_set_black1(): auth_set = AuthSet( None, - {"tool--242f3da3-4425-4d11-8f5c-b842886da966", "doesntexist"} + {"tool--242f3da3-4425-4d11-8f5c-b842886da966", "doesntexist"}, ) results = _get_matching_dir_entries( os.path.join(FS_PATH, "tool"), - auth_set, stat.S_ISDIR + auth_set, stat.S_ISDIR, ) assert set(results) == { - "tool--03342581-f790-4f03-ba41-e82e67392e23" + "tool--03342581-f790-4f03-ba41-e82e67392e23", } def test_search_auth_set_white_empty(): auth_set = AuthSet( set(), - set() + set(), ) results = _get_matching_dir_entries(FS_PATH, auth_set, stat.S_ISDIR) @@ -971,7 +1013,7 @@ def test_search_auth_set_black_empty(rel_fs_store): # predictable (it adds "campaign"). auth_set = AuthSet( None, - set() + set(), ) results = _get_matching_dir_entries(FS_PATH, auth_set, stat.S_ISDIR) @@ -987,14 +1029,14 @@ def test_search_auth_set_black_empty(rel_fs_store): "malware", "marking-definition", "relationship", - "tool" + "tool", } def test_timestamp2filename_naive(): dt = datetime.datetime( 2010, 6, 15, - 8, 30, 10, 1234 + 8, 30, 10, 1234, ) filename = _timestamp2filename(dt) @@ -1007,7 +1049,7 @@ def test_timestamp2filename_tz(): dt = datetime.datetime( 2010, 6, 15, 7, 30, 10, 1234, - tz + tz, ) filename = _timestamp2filename(dt) diff --git a/stix2/test/test_datastore_filters.py b/stix2/test/v20/test_datastore_filters.py similarity index 95% rename from stix2/test/test_datastore_filters.py rename to stix2/test/v20/test_datastore_filters.py index 06a9a59..c5d26c1 100644 --- a/stix2/test/test_datastore_filters.py +++ b/stix2/test/v20/test_datastore_filters.py @@ -10,23 +10,23 @@ stix_objs = [ "description": "\n\nTITLE:\n\tPoison Ivy", "id": "malware--fdd60b30-b67c-41e3-b0b9-f01faf20d111", "labels": [ - "remote-access-trojan" + "remote-access-trojan", ], "modified": "2017-01-27T13:49:53.997Z", "name": "Poison Ivy", - "type": "malware" + "type": "malware", }, { "created": "2014-05-08T09:00:00.000Z", "id": "indicator--a932fcc6-e032-476c-826f-cb970a5a1ade", "labels": [ - "file-hash-watchlist" + "file-hash-watchlist", ], "modified": "2014-05-08T09:00:00.000Z", "name": "File hash for Poison Ivy variant", "pattern": "[file:hashes.'SHA-256' = 'ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c']", "type": "indicator", - "valid_from": "2014-05-08T09:00:00.000000Z" + "valid_from": "2014-05-08T09:00:00.000000Z", }, { "created": "2014-05-08T09:00:00.000Z", @@ -34,20 +34,20 @@ stix_objs = [ { "marking_ref": "marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed", "selectors": [ - "relationship_type" - ] - } + "relationship_type", + ], + }, ], "id": "relationship--2f9a9aa9-108a-4333-83e2-4fb25add0463", "modified": "2014-05-08T09:00:00.000Z", "object_marking_refs": [ - "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" + "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", ], "relationship_type": "indicates", "revoked": True, "source_ref": "indicator--a932fcc6-e032-476c-826f-cb970a5a1ade", "target_ref": "malware--fdd60b30-b67c-41e3-b0b9-f01faf20d111", - "type": "relationship" + "type": "relationship", }, { "id": "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef", @@ -60,10 +60,10 @@ stix_objs = [ "external_references": [ { "source_name": "cve", - "external_id": "CVE-2014-0160" - } + "external_id": "CVE-2014-0160", + }, ], - "labels": ["heartbleed", "has-logo"] + "labels": ["heartbleed", "has-logo"], }, { "type": "observed-data", @@ -77,11 +77,11 @@ stix_objs = [ "objects": { "0": { "type": "file", - "name": "HAL 9000.exe" - } - } + "name": "HAL 9000.exe", + }, + }, - } + }, ] @@ -406,8 +406,10 @@ def test_filters4(): # Assert invalid Filter cannot be created with pytest.raises(ValueError) as excinfo: Filter("modified", "?", "2017-01-27T13:49:53.935Z") - assert str(excinfo.value) == ("Filter operator '?' not supported " - "for specified property: 'modified'") + assert str(excinfo.value) == ( + "Filter operator '?' not supported " + "for specified property: 'modified'" + ) def test_filters5(stix_objs2, real_stix_objs2): @@ -447,7 +449,7 @@ def test_filters7(stix_objs2, real_stix_objs2): "0": { "type": "file", "hashes": { - "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f", }, "extensions": { "pdf-ext": { @@ -457,14 +459,14 @@ def test_filters7(stix_objs2, real_stix_objs2): "Author": "Adobe Systems Incorporated", "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", "Producer": "Acrobat Distiller 3.01 for Power Macintosh", - "CreationDate": "20070412090123-02" + "CreationDate": "20070412090123-02", }, "pdfid0": "DFCE52BD827ECF765649852119D", - "pdfid1": "57A1E0F9ED2AE523E313C" - } - } - } - } + "pdfid1": "57A1E0F9ED2AE523E313C", + }, + }, + }, + }, } stix_objects = list(stix_objs2) + [obsvd_data_obj] diff --git a/stix2/test/test_memory.py b/stix2/test/v20/test_datastore_memory.py similarity index 68% rename from stix2/test/test_memory.py rename to stix2/test/v20/test_datastore_memory.py index 7499326..495652b 100644 --- a/stix2/test/test_memory.py +++ b/stix2/test/v20/test_datastore_memory.py @@ -3,111 +3,113 @@ import shutil import pytest -from stix2 import (Bundle, Campaign, CustomObject, Filter, Identity, Indicator, - Malware, MemorySource, MemoryStore, Relationship, - properties) +from stix2 import Filter, MemorySource, MemoryStore, properties from stix2.datastore import make_id from stix2.utils import parse_into_datetime +from stix2.v20 import ( + Bundle, Campaign, CustomObject, Identity, Indicator, Malware, Relationship, +) -from .constants import (CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, - IDENTITY_KWARGS, INDICATOR_ID, INDICATOR_KWARGS, - MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS) +from .constants import ( + CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, INDICATOR_ID, + INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS, +) IND1 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } IND2 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } IND3 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.936Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } IND4 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } IND5 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } IND6 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000001", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-31T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } IND7 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } IND8 = { "created": "2017-01-27T13:49:53.935Z", "id": "indicator--00000000-0000-4000-8000-000000000002", "labels": [ - "url-watchlist" + "url-watchlist", ], "modified": "2017-01-27T13:49:53.935Z", "name": "Malicious site hosting downloader", "pattern": "[url:value = 'http://x4z9arb.cn/4712']", "type": "indicator", - "valid_from": "2017-01-27T13:49:53.935382Z" + "valid_from": "2017-01-27T13:49:53.935382Z", } STIX_OBJS2 = [IND6, IND7, IND8] @@ -139,11 +141,22 @@ def rel_mem_store(): @pytest.fixture def fs_mem_store(request, mem_store): - filename = 'memory_test/mem_store.json' - mem_store.save_to_file(filename) + filename = mem_store.save_to_file('memory_test/mem_store.json') def fin(): - # teardown, excecuted regardless of exception + # teardown, executed regardless of exception + shutil.rmtree(os.path.dirname(filename)) + request.addfinalizer(fin) + + return filename + + +@pytest.fixture +def fs_mem_store_no_name(request, mem_store): + filename = mem_store.save_to_file('memory_test/') + + def fin(): + # teardown, executed regardless of exception shutil.rmtree(os.path.dirname(filename)) request.addfinalizer(fin) @@ -162,10 +175,12 @@ def test_memory_source_get_nonexistant_object(mem_source): def test_memory_store_all_versions(mem_store): # Add bundle of items to sink - mem_store.add(dict(id="bundle--%s" % make_id(), - objects=STIX_OBJS2, - spec_version="2.0", - type="bundle")) + mem_store.add(dict( + id="bundle--%s" % make_id(), + objects=STIX_OBJS2, + spec_version="2.0", + type="bundle", + )) resp = mem_store.all_versions("indicator--00000000-0000-4000-8000-000000000001") assert len(resp) == 3 @@ -203,7 +218,7 @@ def test_memory_store_query_multiple_filters(mem_store): assert len(resp) == 2 -def test_memory_store_save_load_file(mem_store, fs_mem_store): +def test_memory_store_save_load_file(fs_mem_store): filename = fs_mem_store # the fixture fs_mem_store yields filename where the memory store was written to # STIX2 contents of mem_store have already been written to file @@ -219,6 +234,22 @@ def test_memory_store_save_load_file(mem_store, fs_mem_store): assert mem_store2.get("indicator--00000000-0000-4000-8000-000000000001") +def test_memory_store_save_load_file_no_name_provided(fs_mem_store_no_name): + filename = fs_mem_store_no_name # the fixture fs_mem_store yields filename where the memory store was written to + + # STIX2 contents of mem_store have already been written to file + # (this is done in fixture 'fs_mem_store'), so can already read-in here + contents = open(os.path.abspath(filename)).read() + + assert '"id": "indicator--00000000-0000-4000-8000-000000000001",' in contents + assert '"id": "indicator--00000000-0000-4000-8000-000000000001",' in contents + + mem_store2 = MemoryStore() + mem_store2.load_from_file(filename) + assert mem_store2.get("indicator--00000000-0000-4000-8000-000000000001") + assert mem_store2.get("indicator--00000000-0000-4000-8000-000000000001") + + def test_memory_store_add_invalid_object(mem_store): ind = ('indicator', IND1) # tuple isn't valid with pytest.raises(TypeError): @@ -226,23 +257,67 @@ def test_memory_store_add_invalid_object(mem_store): def test_memory_store_object_with_custom_property(mem_store): - camp = Campaign(name="Scipio Africanus", - objective="Defeat the Carthaginians", - x_empire="Roman", - allow_custom=True) + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) - mem_store.add(camp, True) + mem_store.add(camp) camp_r = mem_store.get(camp.id) assert camp_r.id == camp.id assert camp_r.x_empire == camp.x_empire +def test_memory_store_object_creator_of_present(mem_store): + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + created_by_ref="identity--e4196283-7420-4277-a7a3-d57f61ef1389", + x_empire="Roman", + allow_custom=True, + ) + + iden = Identity( + id="identity--e4196283-7420-4277-a7a3-d57f61ef1389", + name="Foo Corp.", + identity_class="corporation", + ) + + mem_store.add(camp) + mem_store.add(iden) + + camp_r = mem_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + assert mem_store.creator_of(camp_r) == iden + + +def test_memory_store_object_creator_of_missing(mem_store): + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) + + mem_store.add(camp) + + camp_r = mem_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + assert mem_store.creator_of(camp) is None + + def test_memory_store_object_with_custom_property_in_bundle(mem_store): - camp = Campaign(name="Scipio Africanus", - objective="Defeat the Carthaginians", - x_empire="Roman", - allow_custom=True) + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) bundle = Bundle(camp, allow_custom=True) mem_store.add(bundle) @@ -253,14 +328,16 @@ def test_memory_store_object_with_custom_property_in_bundle(mem_store): def test_memory_store_custom_object(mem_store): - @CustomObject('x-new-obj', [ - ('property1', properties.StringProperty(required=True)), - ]) + @CustomObject( + 'x-new-obj', [ + ('property1', properties.StringProperty(required=True)), + ], + ) class NewObj(): pass newobj = NewObj(property1='something') - mem_store.add(newobj, True) + mem_store.add(newobj) newobj_r = mem_store.get(newobj.id) assert newobj_r.id == newobj.id @@ -337,3 +414,12 @@ def test_related_to_by_target(rel_mem_store): assert len(resp) == 2 assert any(x['id'] == CAMPAIGN_ID for x in resp) assert any(x['id'] == INDICATOR_ID for x in resp) + + +def test_object_family_internal_components(mem_source): + # Testing internal components. + str_representation = str(mem_source._data['indicator--00000000-0000-4000-8000-000000000001']) + repr_representation = repr(mem_source._data['indicator--00000000-0000-4000-8000-000000000001']) + + assert "latest=2017-01-27 13:49:53.936000+00:00>>" in str_representation + assert "latest=2017-01-27 13:49:53.936000+00:00>>" in repr_representation diff --git a/stix2/test/test_datastore_taxii.py b/stix2/test/v20/test_datastore_taxii.py similarity index 62% rename from stix2/test/test_datastore_taxii.py rename to stix2/test/v20/test_datastore_taxii.py index 89bf949..5a3c0cb 100644 --- a/stix2/test/test_datastore_taxii.py +++ b/stix2/test/v20/test_datastore_taxii.py @@ -3,10 +3,10 @@ import json from medallion.filters.basic_filter import BasicFilter import pytest from requests.models import Response +import six from taxii2client import Collection, _filter_kwargs_to_query_params -from stix2 import (Bundle, TAXIICollectionSink, TAXIICollectionSource, - TAXIICollectionStore, ThreatActor) +import stix2 from stix2.datastore import DataSourceError from stix2.datastore.filters import Filter @@ -18,50 +18,52 @@ class MockTAXIICollectionEndpoint(Collection): def __init__(self, url, collection_info): super(MockTAXIICollectionEndpoint, self).__init__( - url, collection_info=collection_info + url, collection_info=collection_info, ) self.objects = [] def add_objects(self, bundle): self._verify_can_write() - if isinstance(bundle, str): - bundle = json.loads(bundle) + if isinstance(bundle, six.string_types): + bundle = json.loads(bundle, encoding='utf-8') for object in bundle.get("objects", []): self.objects.append(object) def get_objects(self, **filter_kwargs): self._verify_can_read() query_params = _filter_kwargs_to_query_params(filter_kwargs) - if not isinstance(query_params, dict): - query_params = json.loads(query_params) - full_filter = BasicFilter(query_params or {}) + assert isinstance(query_params, dict) + full_filter = BasicFilter(query_params) objs = full_filter.process_filter( self.objects, ("id", "type", "version"), - [] + [], ) if objs: - return Bundle(objects=objs) + return stix2.v20.Bundle(objects=objs) else: resp = Response() resp.status_code = 404 resp.raise_for_status() - def get_object(self, id, version=None): + def get_object(self, id, **filter_kwargs): self._verify_can_read() - query_params = None - if version: - query_params = _filter_kwargs_to_query_params({"version": version}) - if query_params: - query_params = json.loads(query_params) - full_filter = BasicFilter(query_params or {}) - objs = full_filter.process_filter( - self.objects, - ("version",), - [] - ) - if objs: - return Bundle(objects=objs) + query_params = _filter_kwargs_to_query_params(filter_kwargs) + assert isinstance(query_params, dict) + full_filter = BasicFilter(query_params) + + # In this endpoint we must first filter objects by id beforehand. + objects = [x for x in self.objects if x["id"] == id] + if objects: + filtered_objects = full_filter.process_filter( + objects, + ("version",), + [], + ) + else: + filtered_objects = [] + if filtered_objects: + return stix2.v20.Bundle(objects=filtered_objects) else: resp = Response() resp.status_code = 404 @@ -70,16 +72,18 @@ class MockTAXIICollectionEndpoint(Collection): @pytest.fixture def collection(stix_objs1): - mock = MockTAXIICollectionEndpoint(COLLECTION_URL, { - "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", - "title": "Writable Collection", - "description": "This collection is a dropbox for submitting indicators", - "can_read": True, - "can_write": True, - "media_types": [ - "application/vnd.oasis.stix+json; version=2.0" - ] - }) + mock = MockTAXIICollectionEndpoint( + COLLECTION_URL, { + "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", + "title": "Writable Collection", + "description": "This collection is a dropbox for submitting indicators", + "can_read": True, + "can_write": True, + "media_types": [ + "application/vnd.oasis.stix+json; version=2.0", + ], + }, + ) mock.objects.extend(stix_objs1) return mock @@ -87,94 +91,118 @@ def collection(stix_objs1): @pytest.fixture def collection_no_rw_access(stix_objs1): - mock = MockTAXIICollectionEndpoint(COLLECTION_URL, { - "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", - "title": "Not writeable or readable Collection", - "description": "This collection is a dropbox for submitting indicators", - "can_read": False, - "can_write": False, - "media_types": [ - "application/vnd.oasis.stix+json; version=2.0" - ] - }) + mock = MockTAXIICollectionEndpoint( + COLLECTION_URL, { + "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", + "title": "Not writeable or readable Collection", + "description": "This collection is a dropbox for submitting indicators", + "can_read": False, + "can_write": False, + "media_types": [ + "application/vnd.oasis.stix+json; version=2.0", + ], + }, + ) mock.objects.extend(stix_objs1) return mock def test_ds_taxii(collection): - ds = TAXIICollectionSource(collection) + ds = stix2.TAXIICollectionSource(collection) assert ds.collection is not None def test_add_stix2_object(collection): - tc_sink = TAXIICollectionSink(collection) + tc_sink = stix2.TAXIICollectionSink(collection) # create new STIX threat-actor - ta = ThreatActor(name="Teddy Bear", - labels=["nation-state"], - sophistication="innovator", - resource_level="government", - goals=[ - "compromising environment NGOs", - "water-hole attacks geared towards energy sector", - ]) + ta = stix2.v20.ThreatActor( + name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + ) tc_sink.add(ta) def test_add_stix2_with_custom_object(collection): - tc_sink = TAXIICollectionStore(collection, allow_custom=True) + tc_sink = stix2.TAXIICollectionStore(collection, allow_custom=True) # create new STIX threat-actor - ta = ThreatActor(name="Teddy Bear", - labels=["nation-state"], - sophistication="innovator", - resource_level="government", - goals=[ - "compromising environment NGOs", - "water-hole attacks geared towards energy sector", - ], - foo="bar", - allow_custom=True) + ta = stix2.v20.ThreatActor( + name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + foo="bar", + allow_custom=True, + ) tc_sink.add(ta) def test_add_list_object(collection, indicator): - tc_sink = TAXIICollectionSink(collection) + tc_sink = stix2.TAXIICollectionSink(collection) # create new STIX threat-actor - ta = ThreatActor(name="Teddy Bear", - labels=["nation-state"], - sophistication="innovator", - resource_level="government", - goals=[ - "compromising environment NGOs", - "water-hole attacks geared towards energy sector", - ]) + ta = stix2.v20.ThreatActor( + name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + ) tc_sink.add([ta, indicator]) +def test_get_object_found(collection): + tc_source = stix2.TAXIICollectionSource(collection) + result = tc_source.query([ + stix2.Filter("id", "=", "indicator--00000000-0000-4000-8000-000000000001"), + ]) + assert result + + +def test_get_object_not_found(collection): + tc_source = stix2.TAXIICollectionSource(collection) + result = tc_source.get("indicator--00000000-0000-4000-8000-000000000005") + assert result is None + + def test_add_stix2_bundle_object(collection): - tc_sink = TAXIICollectionSink(collection) + tc_sink = stix2.TAXIICollectionSink(collection) # create new STIX threat-actor - ta = ThreatActor(name="Teddy Bear", - labels=["nation-state"], - sophistication="innovator", - resource_level="government", - goals=[ - "compromising environment NGOs", - "water-hole attacks geared towards energy sector", - ]) + ta = stix2.v20.ThreatActor( + name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + ) - tc_sink.add(Bundle(objects=[ta])) + tc_sink.add(stix2.v20.Bundle(objects=[ta])) def test_add_str_object(collection): - tc_sink = TAXIICollectionSink(collection) + tc_sink = stix2.TAXIICollectionSink(collection) # create new STIX threat-actor ta = """{ @@ -198,7 +226,7 @@ def test_add_str_object(collection): def test_add_dict_object(collection): - tc_sink = TAXIICollectionSink(collection) + tc_sink = stix2.TAXIICollectionSink(collection) ta = { "type": "threat-actor", @@ -208,25 +236,24 @@ def test_add_dict_object(collection): "name": "Teddy Bear", "goals": [ "compromising environment NGOs", - "water-hole attacks geared towards energy sector" + "water-hole attacks geared towards energy sector", ], "sophistication": "innovator", "resource_level": "government", "labels": [ - "nation-state" - ] + "nation-state", + ], } tc_sink.add(ta) def test_add_dict_bundle_object(collection): - tc_sink = TAXIICollectionSink(collection) + tc_sink = stix2.TAXIICollectionSink(collection) ta = { "type": "bundle", "id": "bundle--860ccc8d-56c9-4fda-9384-84276fb52fb1", - "spec_version": "2.0", "objects": [ { "type": "threat-actor", @@ -236,22 +263,22 @@ def test_add_dict_bundle_object(collection): "name": "Teddy Bear", "goals": [ "compromising environment NGOs", - "water-hole attacks geared towards energy sector" + "water-hole attacks geared towards energy sector", ], "sophistication": "innovator", "resource_level": "government", "labels": [ - "nation-state" - ] - } - ] + "nation-state", + ], + }, + ], } tc_sink.add(ta) def test_get_stix2_object(collection): - tc_sink = TAXIICollectionSource(collection) + tc_sink = stix2.TAXIICollectionSource(collection) objects = tc_sink.get("indicator--00000000-0000-4000-8000-000000000001") @@ -271,10 +298,10 @@ def test_parse_taxii_filters(collection): Filter("added_after", "=", "2016-02-01T00:00:01.000Z"), Filter("id", "=", "taxii stix object ID"), Filter("type", "=", "taxii stix object ID"), - Filter("version", "=", "first") + Filter("version", "=", "first"), ] - ds = TAXIICollectionSource(collection) + ds = stix2.TAXIICollectionSource(collection) taxii_filters = ds._parse_taxii_filters(query) @@ -282,7 +309,7 @@ def test_parse_taxii_filters(collection): def test_add_get_remove_filter(collection): - ds = TAXIICollectionSource(collection) + ds = stix2.TAXIICollectionSource(collection) # First 3 filters are valid, remaining properties are erroneous in some way valid_filters = [ @@ -318,7 +345,7 @@ def test_add_get_remove_filter(collection): def test_get_all_versions(collection): - ds = TAXIICollectionStore(collection) + ds = stix2.TAXIICollectionStore(collection) indicators = ds.all_versions('indicator--00000000-0000-4000-8000-000000000001') # There are 3 indicators but 2 share the same 'modified' timestamp @@ -330,7 +357,7 @@ def test_can_read_error(collection_no_rw_access): instance that does not have read access, check ValueError exception is raised""" with pytest.raises(DataSourceError) as excinfo: - TAXIICollectionSource(collection_no_rw_access) + stix2.TAXIICollectionSource(collection_no_rw_access) assert "Collection object provided does not have read access" in str(excinfo.value) @@ -339,7 +366,7 @@ def test_can_write_error(collection_no_rw_access): instance that does not have write access, check ValueError exception is raised""" with pytest.raises(DataSourceError) as excinfo: - TAXIICollectionSink(collection_no_rw_access) + stix2.TAXIICollectionSink(collection_no_rw_access) assert "Collection object provided does not have write access" in str(excinfo.value) @@ -360,7 +387,7 @@ def test_get_404(): resp.status_code = 404 resp.raise_for_status() - ds = TAXIICollectionSource(TAXIICollection404()) + ds = stix2.TAXIICollectionSource(TAXIICollection404()) # this will raise 404 from mock TAXII Client but TAXIICollectionStore # should handle gracefully and return None @@ -372,7 +399,7 @@ def test_all_versions_404(collection): """ a TAXIICollectionSource.all_version() call that recieves an HTTP 404 response code from the taxii2client should be returned as an exception""" - ds = TAXIICollectionStore(collection) + ds = stix2.TAXIICollectionStore(collection) with pytest.raises(DataSourceError) as excinfo: ds.all_versions("indicator--1") @@ -384,7 +411,7 @@ def test_query_404(collection): """ a TAXIICollectionSource.query() call that recieves an HTTP 404 response code from the taxii2client should be returned as an exception""" - ds = TAXIICollectionStore(collection) + ds = stix2.TAXIICollectionStore(collection) query = [Filter("type", "=", "malware")] with pytest.raises(DataSourceError) as excinfo: diff --git a/stix2/test/test_environment.py b/stix2/test/v20/test_environment.py similarity index 68% rename from stix2/test/test_environment.py rename to stix2/test/v20/test_environment.py index a3ec469..5afb430 100644 --- a/stix2/test/test_environment.py +++ b/stix2/test/v20/test_environment.py @@ -2,109 +2,127 @@ import pytest import stix2 -from .constants import (CAMPAIGN_ID, CAMPAIGN_KWARGS, FAKE_TIME, IDENTITY_ID, - IDENTITY_KWARGS, INDICATOR_ID, INDICATOR_KWARGS, - MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS) +from .constants import ( + CAMPAIGN_ID, CAMPAIGN_KWARGS, FAKE_TIME, IDENTITY_ID, IDENTITY_KWARGS, + INDICATOR_ID, INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, + RELATIONSHIP_IDS, +) @pytest.fixture def ds(): - cam = stix2.Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) - idy = stix2.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) - ind = stix2.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) - mal = stix2.Malware(id=MALWARE_ID, **MALWARE_KWARGS) - rel1 = stix2.Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) - rel2 = stix2.Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) - rel3 = stix2.Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + cam = stix2.v20.Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = stix2.v20.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = stix2.v20.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = stix2.v20.Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = stix2.v20.Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = stix2.v20.Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = stix2.v20.Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] yield stix2.MemoryStore(stix_objs) def test_object_factory_created_by_ref_str(): factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID) - ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, **INDICATOR_KWARGS) assert ind.created_by_ref == IDENTITY_ID def test_object_factory_created_by_ref_obj(): - id_obj = stix2.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + id_obj = stix2.v20.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) factory = stix2.ObjectFactory(created_by_ref=id_obj) - ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, **INDICATOR_KWARGS) assert ind.created_by_ref == IDENTITY_ID def test_object_factory_override_default(): factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID) new_id = "identity--983b3172-44fe-4a80-8091-eb8098841fe8" - ind = factory.create(stix2.Indicator, created_by_ref=new_id, **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, created_by_ref=new_id, **INDICATOR_KWARGS) assert ind.created_by_ref == new_id def test_object_factory_created(): factory = stix2.ObjectFactory(created=FAKE_TIME) - ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, **INDICATOR_KWARGS) assert ind.created == FAKE_TIME assert ind.modified == FAKE_TIME def test_object_factory_external_reference(): - ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel", - description="Threat report") + ext_ref = stix2.v20.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + ) factory = stix2.ObjectFactory(external_references=ext_ref) - ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, **INDICATOR_KWARGS) assert ind.external_references[0].source_name == "ACME Threat Intel" assert ind.external_references[0].description == "Threat report" - ind2 = factory.create(stix2.Indicator, external_references=None, **INDICATOR_KWARGS) + ind2 = factory.create(stix2.v20.Indicator, external_references=None, **INDICATOR_KWARGS) assert 'external_references' not in ind2 def test_object_factory_obj_markings(): - stmt_marking = stix2.StatementMarking("Copyright 2016, Example Corp") - mark_def = stix2.MarkingDefinition(definition_type="statement", - definition=stmt_marking) - factory = stix2.ObjectFactory(object_marking_refs=[mark_def, stix2.TLP_AMBER]) - ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS) + stmt_marking = stix2.v20.StatementMarking("Copyright 2016, Example Corp") + mark_def = stix2.v20.MarkingDefinition( + definition_type="statement", + definition=stmt_marking, + ) + factory = stix2.ObjectFactory(object_marking_refs=[mark_def, stix2.v20.TLP_AMBER]) + ind = factory.create(stix2.v20.Indicator, **INDICATOR_KWARGS) assert mark_def.id in ind.object_marking_refs - assert stix2.TLP_AMBER.id in ind.object_marking_refs + assert stix2.v20.TLP_AMBER.id in ind.object_marking_refs - factory = stix2.ObjectFactory(object_marking_refs=stix2.TLP_RED) - ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS) - assert stix2.TLP_RED.id in ind.object_marking_refs + factory = stix2.ObjectFactory(object_marking_refs=stix2.v20.TLP_RED) + ind = factory.create(stix2.v20.Indicator, **INDICATOR_KWARGS) + assert stix2.v20.TLP_RED.id in ind.object_marking_refs def test_object_factory_list_append(): - ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel", - description="Threat report from ACME") - ext_ref2 = stix2.ExternalReference(source_name="Yet Another Threat Report", - description="Threat report from YATR") - ext_ref3 = stix2.ExternalReference(source_name="Threat Report #3", - description="One more threat report") + ext_ref = stix2.v20.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report from ACME", + ) + ext_ref2 = stix2.v20.ExternalReference( + source_name="Yet Another Threat Report", + description="Threat report from YATR", + ) + ext_ref3 = stix2.v20.ExternalReference( + source_name="Threat Report #3", + description="One more threat report", + ) factory = stix2.ObjectFactory(external_references=ext_ref) - ind = factory.create(stix2.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS) assert ind.external_references[1].source_name == "Yet Another Threat Report" - ind = factory.create(stix2.Indicator, external_references=[ext_ref2, ext_ref3], **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, external_references=[ext_ref2, ext_ref3], **INDICATOR_KWARGS) assert ind.external_references[2].source_name == "Threat Report #3" def test_object_factory_list_replace(): - ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel", - description="Threat report from ACME") - ext_ref2 = stix2.ExternalReference(source_name="Yet Another Threat Report", - description="Threat report from YATR") + ext_ref = stix2.v20.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report from ACME", + ) + ext_ref2 = stix2.v20.ExternalReference( + source_name="Yet Another Threat Report", + description="Threat report from YATR", + ) factory = stix2.ObjectFactory(external_references=ext_ref, list_append=False) - ind = factory.create(stix2.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS) + ind = factory.create(stix2.v20.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS) assert len(ind.external_references) == 1 assert ind.external_references[0].source_name == "Yet Another Threat Report" def test_environment_functions(): - env = stix2.Environment(stix2.ObjectFactory(created_by_ref=IDENTITY_ID), - stix2.MemoryStore()) + env = stix2.Environment( + stix2.ObjectFactory(created_by_ref=IDENTITY_ID), + stix2.MemoryStore(), + ) # Create a STIX object - ind = env.create(stix2.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + ind = env.create(stix2.v20.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) assert ind.created_by_ref == IDENTITY_ID # Add objects to datastore @@ -125,23 +143,27 @@ def test_environment_functions(): assert len(resp) == 0 # See different results after adding filters to the environment - env.add_filters([stix2.Filter('type', '=', 'indicator'), - stix2.Filter('created_by_ref', '=', IDENTITY_ID)]) + env.add_filters([ + stix2.Filter('type', '=', 'indicator'), + stix2.Filter('created_by_ref', '=', IDENTITY_ID), + ]) env.add_filter(stix2.Filter('labels', '=', 'benign')) # should be 'malicious-activity' resp = env.get(INDICATOR_ID) assert resp['labels'][0] == 'benign' # should be 'malicious-activity' def test_environment_source_and_sink(): - ind = stix2.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + ind = stix2.v20.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) env = stix2.Environment(source=stix2.MemorySource([ind]), sink=stix2.MemorySink([ind])) assert env.get(INDICATOR_ID).labels[0] == 'malicious-activity' def test_environment_datastore_and_sink(): with pytest.raises(ValueError) as excinfo: - stix2.Environment(factory=stix2.ObjectFactory(), - store=stix2.MemoryStore(), sink=stix2.MemorySink) + stix2.Environment( + factory=stix2.ObjectFactory(), + store=stix2.MemoryStore(), sink=stix2.MemorySink, + ) assert 'Data store already provided' in str(excinfo.value) @@ -149,7 +171,7 @@ def test_environment_no_datastore(): env = stix2.Environment(factory=stix2.ObjectFactory()) with pytest.raises(AttributeError) as excinfo: - env.add(stix2.Indicator(**INDICATOR_KWARGS)) + env.add(stix2.v20.Indicator(**INDICATOR_KWARGS)) assert 'Environment has no data sink to put objects in' in str(excinfo.value) with pytest.raises(AttributeError) as excinfo: @@ -182,7 +204,7 @@ def test_environment_add_filters(): def test_environment_datastore_and_no_object_factory(): # Uses a default object factory env = stix2.Environment(store=stix2.MemoryStore()) - ind = env.create(stix2.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + ind = env.create(stix2.v20.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) assert ind.id == INDICATOR_ID @@ -198,7 +220,7 @@ def test_parse_malware(): "ransomware" ] }""" - mal = env.parse(data) + mal = env.parse(data, version="2.0") assert mal.type == 'malware' assert mal.id == MALWARE_ID @@ -209,40 +231,40 @@ def test_parse_malware(): def test_creator_of(): - identity = stix2.Identity(**IDENTITY_KWARGS) + identity = stix2.v20.Identity(**IDENTITY_KWARGS) factory = stix2.ObjectFactory(created_by_ref=identity.id) env = stix2.Environment(store=stix2.MemoryStore(), factory=factory) env.add(identity) - ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = env.create(stix2.v20.Indicator, **INDICATOR_KWARGS) creator = env.creator_of(ind) assert creator is identity def test_creator_of_no_datasource(): - identity = stix2.Identity(**IDENTITY_KWARGS) + identity = stix2.v20.Identity(**IDENTITY_KWARGS) factory = stix2.ObjectFactory(created_by_ref=identity.id) env = stix2.Environment(factory=factory) - ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = env.create(stix2.v20.Indicator, **INDICATOR_KWARGS) with pytest.raises(AttributeError) as excinfo: env.creator_of(ind) assert 'Environment has no data source' in str(excinfo.value) def test_creator_of_not_found(): - identity = stix2.Identity(**IDENTITY_KWARGS) + identity = stix2.v20.Identity(**IDENTITY_KWARGS) factory = stix2.ObjectFactory(created_by_ref=identity.id) env = stix2.Environment(store=stix2.MemoryStore(), factory=factory) - ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = env.create(stix2.v20.Indicator, **INDICATOR_KWARGS) creator = env.creator_of(ind) assert creator is None def test_creator_of_no_created_by_ref(): env = stix2.Environment(store=stix2.MemoryStore()) - ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + ind = env.create(stix2.v20.Indicator, **INDICATOR_KWARGS) creator = env.creator_of(ind) assert creator is None @@ -262,7 +284,7 @@ def test_relationships_no_id(ds): env = stix2.Environment(store=ds) mal = { "type": "malware", - "name": "some variant" + "name": "some variant", } with pytest.raises(ValueError) as excinfo: env.relationships(mal) @@ -326,7 +348,7 @@ def test_related_to_no_id(ds): env = stix2.Environment(store=ds) mal = { "type": "malware", - "name": "some variant" + "name": "some variant", } with pytest.raises(ValueError) as excinfo: env.related_to(mal) diff --git a/stix2/test/test_external_reference.py b/stix2/test/v20/test_external_reference.py similarity index 88% rename from stix2/test/test_external_reference.py rename to stix2/test/v20/test_external_reference.py index 9b90998..07cf42d 100644 --- a/stix2/test/test_external_reference.py +++ b/stix2/test/v20/test_external_reference.py @@ -17,11 +17,11 @@ VERIS = """{ def test_external_reference_veris(): - ref = stix2.ExternalReference( + ref = stix2.v20.ExternalReference( source_name="veris", external_id="0001AA7F-C601-424A-B2B8-BE6C9F5164E7", hashes={ - "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b" + "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b", }, url="https://github.com/vz-risk/VCDB/blob/master/data/json/0001AA7F-C601-424A-B2B8-BE6C9F5164E7.json", ) @@ -36,7 +36,7 @@ CAPEC = """{ def test_external_reference_capec(): - ref = stix2.ExternalReference( + ref = stix2.v20.ExternalReference( source_name="capec", external_id="CAPEC-550", ) @@ -53,7 +53,7 @@ CAPEC_URL = """{ def test_external_reference_capec_url(): - ref = stix2.ExternalReference( + ref = stix2.v20.ExternalReference( source_name="capec", external_id="CAPEC-550", url="http://capec.mitre.org/data/definitions/550.html", @@ -70,7 +70,7 @@ THREAT_REPORT = """{ def test_external_reference_threat_report(): - ref = stix2.ExternalReference( + ref = stix2.v20.ExternalReference( source_name="ACME Threat Intel", description="Threat report", url="http://www.example.com/threat-report.pdf", @@ -87,7 +87,7 @@ BUGZILLA = """{ def test_external_reference_bugzilla(): - ref = stix2.ExternalReference( + ref = stix2.v20.ExternalReference( source_name="ACME Bugzilla", external_id="1370", url="https://www.example.com/bugs/1370", @@ -103,7 +103,7 @@ OFFLINE = """{ def test_external_reference_offline(): - ref = stix2.ExternalReference( + ref = stix2.v20.ExternalReference( source_name="ACME Threat Intel", description="Threat report", ) @@ -116,7 +116,7 @@ def test_external_reference_offline(): def test_external_reference_source_required(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.ExternalReference() + stix2.v20.ExternalReference() - assert excinfo.value.cls == stix2.ExternalReference + assert excinfo.value.cls == stix2.v20.ExternalReference assert excinfo.value.properties == ["source_name"] diff --git a/stix2/test/test_fixtures.py b/stix2/test/v20/test_fixtures.py similarity index 100% rename from stix2/test/test_fixtures.py rename to stix2/test/v20/test_fixtures.py diff --git a/stix2/test/test_granular_markings.py b/stix2/test/v20/test_granular_markings.py similarity index 78% rename from stix2/test/test_granular_markings.py rename to stix2/test/v20/test_granular_markings.py index 9e024a1..b5f2e3d 100644 --- a/stix2/test/test_granular_markings.py +++ b/stix2/test/v20/test_granular_markings.py @@ -1,8 +1,9 @@ import pytest -from stix2 import TLP_RED, Malware, markings +from stix2 import markings from stix2.exceptions import MarkingNotFoundError +from stix2.v20 import TLP_RED, Malware from .constants import MALWARE_MORE_KWARGS as MALWARE_KWARGS_CONST from .constants import MARKING_IDS @@ -20,11 +21,11 @@ def test_add_marking_mark_one_selector_multiple_refs(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["description"], - "marking_ref": MARKING_IDS[1] + "marking_ref": MARKING_IDS[1], }, ], **MALWARE_KWARGS @@ -35,44 +36,49 @@ def test_add_marking_mark_one_selector_multiple_refs(): assert m in after["granular_markings"] -@pytest.mark.parametrize("data", [ - ( - Malware(**MALWARE_KWARGS), - Malware( - granular_markings=[ - { - "selectors": ["description", "name"], - "marking_ref": MARKING_IDS[0] - }, - ], - **MALWARE_KWARGS), - MARKING_IDS[0], - ), - ( - MALWARE_KWARGS, - dict( - granular_markings=[ - { - "selectors": ["description", "name"], - "marking_ref": MARKING_IDS[0] - }, - ], - **MALWARE_KWARGS), - MARKING_IDS[0], - ), - ( - Malware(**MALWARE_KWARGS), - Malware( - granular_markings=[ - { - "selectors": ["description", "name"], - "marking_ref": TLP_RED.id, - }, - ], - **MALWARE_KWARGS), - TLP_RED, - ), -]) +@pytest.mark.parametrize( + "data", [ + ( + Malware(**MALWARE_KWARGS), + Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + MALWARE_KWARGS, + dict( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + Malware(**MALWARE_KWARGS), + Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": TLP_RED.id, + }, + ], + **MALWARE_KWARGS + ), + TLP_RED, + ), + ], +) def test_add_marking_mark_multiple_selector_one_refs(data): before = data[0] after = data[1] @@ -91,12 +97,12 @@ def test_add_marking_mark_multiple_selector_multiple_refs(): granular_markings=[ { "selectors": ["description", "name"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["description", "name"], - "marking_ref": MARKING_IDS[1] - } + "marking_ref": MARKING_IDS[1], + }, ], **MALWARE_KWARGS ) @@ -111,7 +117,7 @@ def test_add_marking_mark_another_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, ], **MALWARE_KWARGS @@ -120,7 +126,7 @@ def test_add_marking_mark_another_property_same_marking(): granular_markings=[ { "selectors": ["description", "name"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, ], **MALWARE_KWARGS @@ -136,7 +142,7 @@ def test_add_marking_mark_same_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, ], **MALWARE_KWARGS @@ -145,7 +151,7 @@ def test_add_marking_mark_same_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, ], **MALWARE_KWARGS @@ -156,17 +162,22 @@ def test_add_marking_mark_same_property_same_marking(): assert m in after["granular_markings"] -@pytest.mark.parametrize("data,marking", [ - ({"description": "test description"}, - [["title"], ["marking-definition--1", "marking-definition--2"], - "", ["marking-definition--1", "marking-definition--2"], - [], ["marking-definition--1", "marking-definition--2"], - [""], ["marking-definition--1", "marking-definition--2"], - ["description"], [""], - ["description"], [], - ["description"], ["marking-definition--1", 456] - ]) -]) +@pytest.mark.parametrize( + "data,marking", [ + ( + {"description": "test description"}, + [ + ["title"], ["marking-definition--1", "marking-definition--2"], + "", ["marking-definition--1", "marking-definition--2"], + [], ["marking-definition--1", "marking-definition--2"], + [""], ["marking-definition--1", "marking-definition--2"], + ["description"], [""], + ["description"], [], + ["description"], ["marking-definition--1", 456], + ], + ), + ], +) def test_add_marking_bad_selector(data, marking): with pytest.raises(AssertionError): markings.add_markings(data, marking[0], marking[1]) @@ -180,61 +191,61 @@ GET_MARKINGS_TEST_DATA = { "list value", { "g": "nested", - "h": 45 - } + "h": 45, + }, ], "x": { "y": [ "hello", - 88 + 88, ], "z": { "foo1": "bar", - "foo2": 65 - } + "foo2": 65, + }, }, "granular_markings": [ { "marking_ref": "1", - "selectors": ["a"] + "selectors": ["a"], }, { "marking_ref": "2", - "selectors": ["c"] + "selectors": ["c"], }, { "marking_ref": "3", - "selectors": ["c.[1]"] + "selectors": ["c.[1]"], }, { "marking_ref": "4", - "selectors": ["c.[2]"] + "selectors": ["c.[2]"], }, { "marking_ref": "5", - "selectors": ["c.[2].g"] + "selectors": ["c.[2].g"], }, { "marking_ref": "6", - "selectors": ["x"] + "selectors": ["x"], }, { "marking_ref": "7", - "selectors": ["x.y"] + "selectors": ["x.y"], }, { "marking_ref": "8", - "selectors": ["x.y.[1]"] + "selectors": ["x.y.[1]"], }, { "marking_ref": "9", - "selectors": ["x.z"] + "selectors": ["x.z"], }, { "marking_ref": "10", - "selectors": ["x.z.foo2"] + "selectors": ["x.z.foo2"], }, - ] + ], } @@ -245,10 +256,12 @@ def test_get_markings_smoke(data): assert markings.get_markings(data, "a") == ["1"] -@pytest.mark.parametrize("data", [ - GET_MARKINGS_TEST_DATA, - {"b": 1234}, -]) +@pytest.mark.parametrize( + "data", [ + GET_MARKINGS_TEST_DATA, + {"b": 1234}, + ], +) def test_get_markings_not_marked(data): """Test selector that is not marked returns empty list.""" results = markings.get_markings(data, "b") @@ -267,21 +280,23 @@ def test_get_markings_multiple_selectors(data): assert set(xy_markings).union(xz_markings).issuperset(total) -@pytest.mark.parametrize("data,selector", [ - (GET_MARKINGS_TEST_DATA, "foo"), - (GET_MARKINGS_TEST_DATA, ""), - (GET_MARKINGS_TEST_DATA, []), - (GET_MARKINGS_TEST_DATA, [""]), - (GET_MARKINGS_TEST_DATA, "x.z.[-2]"), - (GET_MARKINGS_TEST_DATA, "c.f"), - (GET_MARKINGS_TEST_DATA, "c.[2].i"), - (GET_MARKINGS_TEST_DATA, "c.[3]"), - (GET_MARKINGS_TEST_DATA, "d"), - (GET_MARKINGS_TEST_DATA, "x.[0]"), - (GET_MARKINGS_TEST_DATA, "z.y.w"), - (GET_MARKINGS_TEST_DATA, "x.z.[1]"), - (GET_MARKINGS_TEST_DATA, "x.z.foo3") -]) +@pytest.mark.parametrize( + "data,selector", [ + (GET_MARKINGS_TEST_DATA, "foo"), + (GET_MARKINGS_TEST_DATA, ""), + (GET_MARKINGS_TEST_DATA, []), + (GET_MARKINGS_TEST_DATA, [""]), + (GET_MARKINGS_TEST_DATA, "x.z.[-2]"), + (GET_MARKINGS_TEST_DATA, "c.f"), + (GET_MARKINGS_TEST_DATA, "c.[2].i"), + (GET_MARKINGS_TEST_DATA, "c.[3]"), + (GET_MARKINGS_TEST_DATA, "d"), + (GET_MARKINGS_TEST_DATA, "x.[0]"), + (GET_MARKINGS_TEST_DATA, "z.y.w"), + (GET_MARKINGS_TEST_DATA, "x.z.[1]"), + (GET_MARKINGS_TEST_DATA, "x.z.foo3"), + ], +) def test_get_markings_bad_selector(data, selector): """Test bad selectors raise exception""" with pytest.raises(AssertionError): @@ -362,40 +377,42 @@ def test_get_markings_positional_arguments_combinations(data): assert set(markings.get_markings(data, "x.z.foo2", False, True)) == set(["10"]) -@pytest.mark.parametrize("data", [ - ( - Malware( - granular_markings=[ - { - "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - }, - { - "selectors": ["description"], - "marking_ref": MARKING_IDS[1] - }, - ], - **MALWARE_KWARGS +@pytest.mark.parametrize( + "data", [ + ( + Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[1]], ), - [MARKING_IDS[0], MARKING_IDS[1]], - ), - ( - dict( - granular_markings=[ - { - "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - }, - { - "selectors": ["description"], - "marking_ref": MARKING_IDS[1] - }, - ], - **MALWARE_KWARGS + ( + dict( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[1]], ), - [MARKING_IDS[0], MARKING_IDS[1]], - ), -]) + ], +) def test_remove_marking_remove_one_selector_with_multiple_refs(data): before = markings.remove_markings(data[0], data[1], ["description"]) assert "granular_markings" not in before @@ -406,8 +423,8 @@ def test_remove_marking_remove_multiple_selector_one_ref(): granular_markings=[ { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -420,8 +437,8 @@ def test_remove_marking_mark_one_selector_from_multiple_ones(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -429,8 +446,8 @@ def test_remove_marking_mark_one_selector_from_multiple_ones(): granular_markings=[ { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -444,12 +461,12 @@ def test_remove_marking_mark_one_selector_markings_from_multiple_ones(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[1] - } + "marking_ref": MARKING_IDS[1], + }, ], **MALWARE_KWARGS ) @@ -457,12 +474,12 @@ def test_remove_marking_mark_one_selector_markings_from_multiple_ones(): granular_markings=[ { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[1] - } + "marking_ref": MARKING_IDS[1], + }, ], **MALWARE_KWARGS ) @@ -476,12 +493,12 @@ def test_remove_marking_mark_mutilple_selector_multiple_refs(): granular_markings=[ { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[1] - } + "marking_ref": MARKING_IDS[1], + }, ], **MALWARE_KWARGS ) @@ -494,8 +511,8 @@ def test_remove_marking_mark_another_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -503,12 +520,12 @@ def test_remove_marking_mark_another_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["modified"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -522,8 +539,8 @@ def test_remove_marking_mark_same_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -552,8 +569,8 @@ def test_remove_marking_not_present(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -566,15 +583,15 @@ IS_MARKED_TEST_DATA = [ granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[1] + "marking_ref": MARKING_IDS[1], }, { "selectors": ["labels", "description"], - "marking_ref": MARKING_IDS[2] + "marking_ref": MARKING_IDS[2], }, { "selectors": ["labels", "description"], - "marking_ref": MARKING_IDS[3] + "marking_ref": MARKING_IDS[3], }, ], **MALWARE_KWARGS @@ -583,15 +600,15 @@ IS_MARKED_TEST_DATA = [ granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[1] + "marking_ref": MARKING_IDS[1], }, { "selectors": ["labels", "description"], - "marking_ref": MARKING_IDS[2] + "marking_ref": MARKING_IDS[2], }, { "selectors": ["labels", "description"], - "marking_ref": MARKING_IDS[3] + "marking_ref": MARKING_IDS[3], }, ], **MALWARE_KWARGS @@ -606,21 +623,23 @@ def test_is_marked_smoke(data): assert markings.is_marked(data, selectors=["modified"]) is False -@pytest.mark.parametrize("data,selector", [ - (IS_MARKED_TEST_DATA[0], "foo"), - (IS_MARKED_TEST_DATA[0], ""), - (IS_MARKED_TEST_DATA[0], []), - (IS_MARKED_TEST_DATA[0], [""]), - (IS_MARKED_TEST_DATA[0], "x.z.[-2]"), - (IS_MARKED_TEST_DATA[0], "c.f"), - (IS_MARKED_TEST_DATA[0], "c.[2].i"), - (IS_MARKED_TEST_DATA[1], "c.[3]"), - (IS_MARKED_TEST_DATA[1], "d"), - (IS_MARKED_TEST_DATA[1], "x.[0]"), - (IS_MARKED_TEST_DATA[1], "z.y.w"), - (IS_MARKED_TEST_DATA[1], "x.z.[1]"), - (IS_MARKED_TEST_DATA[1], "x.z.foo3") -]) +@pytest.mark.parametrize( + "data,selector", [ + (IS_MARKED_TEST_DATA[0], "foo"), + (IS_MARKED_TEST_DATA[0], ""), + (IS_MARKED_TEST_DATA[0], []), + (IS_MARKED_TEST_DATA[0], [""]), + (IS_MARKED_TEST_DATA[0], "x.z.[-2]"), + (IS_MARKED_TEST_DATA[0], "c.f"), + (IS_MARKED_TEST_DATA[0], "c.[2].i"), + (IS_MARKED_TEST_DATA[1], "c.[3]"), + (IS_MARKED_TEST_DATA[1], "d"), + (IS_MARKED_TEST_DATA[1], "x.[0]"), + (IS_MARKED_TEST_DATA[1], "z.y.w"), + (IS_MARKED_TEST_DATA[1], "x.z.[1]"), + (IS_MARKED_TEST_DATA[1], "x.z.foo3"), + ], +) def test_is_marked_invalid_selector(data, selector): """Test invalid selector raises an error.""" with pytest.raises(AssertionError): @@ -688,61 +707,61 @@ def test_is_marked_positional_arguments_combinations(): "list value", { "g": "nested", - "h": 45 - } + "h": 45, + }, ], "x": { "y": [ "hello", - 88 + 88, ], "z": { "foo1": "bar", - "foo2": 65 - } + "foo2": 65, + }, }, "granular_markings": [ { "marking_ref": "1", - "selectors": ["a"] + "selectors": ["a"], }, { "marking_ref": "2", - "selectors": ["c"] + "selectors": ["c"], }, { "marking_ref": "3", - "selectors": ["c.[1]"] + "selectors": ["c.[1]"], }, { "marking_ref": "4", - "selectors": ["c.[2]"] + "selectors": ["c.[2]"], }, { "marking_ref": "5", - "selectors": ["c.[2].g"] + "selectors": ["c.[2].g"], }, { "marking_ref": "6", - "selectors": ["x"] + "selectors": ["x"], }, { "marking_ref": "7", - "selectors": ["x.y"] + "selectors": ["x.y"], }, { "marking_ref": "8", - "selectors": ["x.y.[1]"] + "selectors": ["x.y.[1]"], }, { "marking_ref": "9", - "selectors": ["x.z"] + "selectors": ["x.z"], }, { "marking_ref": "10", - "selectors": ["x.z.foo2"] + "selectors": ["x.z.foo2"], }, - ] + ], } assert markings.is_marked(test_sdo, ["1"], "a", False, False) @@ -822,8 +841,8 @@ def test_create_sdo_with_invalid_marking(): granular_markings=[ { "selectors": ["foo"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -838,12 +857,12 @@ def test_set_marking_mark_one_selector_multiple_refs(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["description"], - "marking_ref": MARKING_IDS[1] - } + "marking_ref": MARKING_IDS[1], + }, ], **MALWARE_KWARGS ) @@ -857,8 +876,8 @@ def test_set_marking_mark_multiple_selector_one_refs(): granular_markings=[ { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[1] - } + "marking_ref": MARKING_IDS[1], + }, ], **MALWARE_KWARGS ) @@ -866,8 +885,8 @@ def test_set_marking_mark_multiple_selector_one_refs(): granular_markings=[ { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -884,12 +903,12 @@ def test_set_marking_mark_multiple_selector_multiple_refs_from_none(): granular_markings=[ { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["description", "modified"], - "marking_ref": MARKING_IDS[1] - } + "marking_ref": MARKING_IDS[1], + }, ], **MALWARE_KWARGS ) @@ -903,8 +922,8 @@ def test_set_marking_mark_another_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -912,12 +931,12 @@ def test_set_marking_mark_another_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[1] + "marking_ref": MARKING_IDS[1], }, { "selectors": ["description"], - "marking_ref": MARKING_IDS[2] - } + "marking_ref": MARKING_IDS[2], + }, ], **MALWARE_KWARGS ) @@ -927,19 +946,21 @@ def test_set_marking_mark_another_property_same_marking(): assert m in after["granular_markings"] -@pytest.mark.parametrize("marking", [ - ([MARKING_IDS[4], MARKING_IDS[5]], ["foo"]), - ([MARKING_IDS[4], MARKING_IDS[5]], ""), - ([MARKING_IDS[4], MARKING_IDS[5]], []), - ([MARKING_IDS[4], MARKING_IDS[5]], [""]), -]) +@pytest.mark.parametrize( + "marking", [ + ([MARKING_IDS[4], MARKING_IDS[5]], ["foo"]), + ([MARKING_IDS[4], MARKING_IDS[5]], ""), + ([MARKING_IDS[4], MARKING_IDS[5]], []), + ([MARKING_IDS[4], MARKING_IDS[5]], [""]), + ], +) def test_set_marking_bad_selector(marking): before = Malware( granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -947,8 +968,8 @@ def test_set_marking_bad_selector(marking): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -964,8 +985,8 @@ def test_set_marking_mark_same_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -973,8 +994,8 @@ def test_set_marking_mark_same_property_same_marking(): granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] - } + "marking_ref": MARKING_IDS[0], + }, ], **MALWARE_KWARGS ) @@ -988,15 +1009,15 @@ CLEAR_MARKINGS_TEST_DATA = [ granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["modified", "description"], - "marking_ref": MARKING_IDS[1] + "marking_ref": MARKING_IDS[1], }, { "selectors": ["modified", "description", "type"], - "marking_ref": MARKING_IDS[2] + "marking_ref": MARKING_IDS[2], }, ], **MALWARE_KWARGS @@ -1005,19 +1026,19 @@ CLEAR_MARKINGS_TEST_DATA = [ granular_markings=[ { "selectors": ["description"], - "marking_ref": MARKING_IDS[0] + "marking_ref": MARKING_IDS[0], }, { "selectors": ["modified", "description"], - "marking_ref": MARKING_IDS[1] + "marking_ref": MARKING_IDS[1], }, { "selectors": ["modified", "description", "type"], - "marking_ref": MARKING_IDS[2] + "marking_ref": MARKING_IDS[2], }, ], **MALWARE_KWARGS - ) + ), ] @@ -1049,12 +1070,14 @@ def test_clear_marking_all_selectors(data): assert "granular_markings" not in data -@pytest.mark.parametrize("data,selector", [ - (CLEAR_MARKINGS_TEST_DATA[0], "foo"), - (CLEAR_MARKINGS_TEST_DATA[0], ""), - (CLEAR_MARKINGS_TEST_DATA[1], []), - (CLEAR_MARKINGS_TEST_DATA[1], [""]), -]) +@pytest.mark.parametrize( + "data,selector", [ + (CLEAR_MARKINGS_TEST_DATA[0], "foo"), + (CLEAR_MARKINGS_TEST_DATA[0], ""), + (CLEAR_MARKINGS_TEST_DATA[1], []), + (CLEAR_MARKINGS_TEST_DATA[1], [""]), + ], +) def test_clear_marking_bad_selector(data, selector): """Test bad selector raises exception.""" with pytest.raises(AssertionError): diff --git a/stix2/test/test_identity.py b/stix2/test/v20/test_identity.py similarity index 71% rename from stix2/test/test_identity.py rename to stix2/test/v20/test_identity.py index ba1cfbe..4a88a8a 100644 --- a/stix2/test/test_identity.py +++ b/stix2/test/v20/test_identity.py @@ -18,7 +18,7 @@ EXPECTED = """{ def test_identity_example(): - identity = stix2.Identity( + identity = stix2.v20.Identity( id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", created="2015-12-21T19:59:11.000Z", modified="2015-12-21T19:59:11.000Z", @@ -29,19 +29,21 @@ def test_identity_example(): assert str(identity) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "created": "2015-12-21T19:59:11.000Z", - "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", - "identity_class": "individual", - "modified": "2015-12-21T19:59:11.000Z", - "name": "John Smith", - "type": "identity" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2015-12-21T19:59:11.000Z", + "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + "identity_class": "individual", + "modified": "2015-12-21T19:59:11.000Z", + "name": "John Smith", + "type": "identity", + }, + ], +) def test_parse_identity(data): - identity = stix2.parse(data) + identity = stix2.parse(data, version="2.0") assert identity.type == 'identity' assert identity.id == IDENTITY_ID @@ -52,21 +54,23 @@ def test_parse_identity(data): def test_parse_no_type(): with pytest.raises(stix2.exceptions.ParseError): - stix2.parse(""" + stix2.parse( + """ { "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", "created": "2015-12-21T19:59:11.000Z", "modified": "2015-12-21T19:59:11.000Z", "name": "John Smith", "identity_class": "individual" - }""") + }""", version="2.0", + ) def test_identity_with_custom(): - identity = stix2.Identity( + identity = stix2.v20.Identity( name="John Smith", identity_class="individual", - custom_properties={'x_foo': 'bar'} + custom_properties={'x_foo': 'bar'}, ) assert identity.x_foo == "bar" diff --git a/stix2/test/test_indicator.py b/stix2/test/v20/test_indicator.py similarity index 78% rename from stix2/test/test_indicator.py rename to stix2/test/v20/test_indicator.py index 520ed6b..f8c3a91 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/v20/test_indicator.py @@ -35,7 +35,7 @@ def test_indicator_with_all_required_properties(): now = dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc) epoch = dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc) - ind = stix2.Indicator( + ind = stix2.v20.Indicator( type="indicator", id=INDICATOR_ID, created=now, @@ -71,9 +71,9 @@ def test_indicator_autogenerated_properties(indicator): def test_indicator_type_must_be_indicator(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Indicator(type='xxx', **INDICATOR_KWARGS) + stix2.v20.Indicator(type='xxx', **INDICATOR_KWARGS) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.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'." @@ -81,9 +81,9 @@ def test_indicator_type_must_be_indicator(): def test_indicator_id_must_start_with_indicator(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Indicator(id='my-prefix--', **INDICATOR_KWARGS) + stix2.v20.Indicator(id='my-prefix--', **INDICATOR_KWARGS) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.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--'." @@ -91,26 +91,26 @@ def test_indicator_id_must_start_with_indicator(): def test_indicator_required_properties(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.Indicator() + stix2.v20.Indicator() - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.Indicator assert excinfo.value.properties == ["labels", "pattern"] assert str(excinfo.value) == "No values for required properties for Indicator: (labels, pattern)." def test_indicator_required_property_pattern(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.Indicator(labels=['malicious-activity']) + stix2.v20.Indicator(labels=['malicious-activity']) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.Indicator assert excinfo.value.properties == ["pattern"] def test_indicator_created_ref_invalid_format(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Indicator(created_by_ref='myprefix--12345678', **INDICATOR_KWARGS) + stix2.v20.Indicator(created_by_ref='myprefix--12345678', **INDICATOR_KWARGS) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.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'." @@ -118,9 +118,9 @@ def test_indicator_created_ref_invalid_format(): def test_indicator_revoked_invalid(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Indicator(revoked='no', **INDICATOR_KWARGS) + stix2.v20.Indicator(revoked='no', **INDICATOR_KWARGS) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.Indicator assert excinfo.value.prop_name == "revoked" assert excinfo.value.reason == "must be a boolean value." @@ -134,36 +134,38 @@ def test_cannot_assign_to_indicator_attributes(indicator): def test_invalid_kwarg_to_indicator(): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: - stix2.Indicator(my_custom_property="foo", **INDICATOR_KWARGS) + stix2.v20.Indicator(my_custom_property="foo", **INDICATOR_KWARGS) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.Indicator assert excinfo.value.properties == ['my_custom_property'] assert str(excinfo.value) == "Unexpected properties for Indicator: (my_custom_property)." def test_created_modified_time_are_identical_by_default(): """By default, the created and modified times should be the same.""" - ind = stix2.Indicator(**INDICATOR_KWARGS) + ind = stix2.v20.Indicator(**INDICATOR_KWARGS) assert ind.created == ind.modified -@pytest.mark.parametrize("data", [ - EXPECTED_INDICATOR, - { - "type": "indicator", - "id": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", - "created": "2017-01-01T00:00:01Z", - "modified": "2017-01-01T00:00:01Z", - "labels": [ - "malicious-activity" - ], - "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", - "valid_from": "1970-01-01T00:00:01Z" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED_INDICATOR, + { + "type": "indicator", + "id": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "created": "2017-01-01T00:00:01Z", + "modified": "2017-01-01T00:00:01Z", + "labels": [ + "malicious-activity", + ], + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "1970-01-01T00:00:01Z", + }, + ], +) def test_parse_indicator(data): - idctr = stix2.parse(data) + idctr = stix2.parse(data, version="2.0") assert idctr.type == 'indicator' assert idctr.id == INDICATOR_ID @@ -176,19 +178,19 @@ def test_parse_indicator(data): def test_invalid_indicator_pattern(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Indicator( + stix2.v20.Indicator( labels=['malicious-activity'], pattern="file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e'", ) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.Indicator assert excinfo.value.prop_name == 'pattern' assert 'input is missing square brackets' in excinfo.value.reason with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Indicator( + stix2.v20.Indicator( labels=['malicious-activity'], pattern='[file:hashes.MD5 = "d41d8cd98f00b204e9800998ecf8427e"]', ) - assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.cls == stix2.v20.Indicator assert excinfo.value.prop_name == 'pattern' assert 'mismatched input' in excinfo.value.reason diff --git a/stix2/test/test_intrusion_set.py b/stix2/test/v20/test_intrusion_set.py similarity index 65% rename from stix2/test/test_intrusion_set.py rename to stix2/test/v20/test_intrusion_set.py index 53e18f5..bf4a7d5 100644 --- a/stix2/test/test_intrusion_set.py +++ b/stix2/test/v20/test_intrusion_set.py @@ -27,7 +27,7 @@ EXPECTED = """{ def test_intrusion_set_example(): - intrusion_set = stix2.IntrusionSet( + intrusion_set = stix2.v20.IntrusionSet( id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:48.000Z", @@ -35,34 +35,36 @@ def test_intrusion_set_example(): name="Bobcat Breakin", description="Incidents usually feature a shared TTP of a bobcat being released...", aliases=["Zookeeper"], - goals=["acquisition-theft", "harassment", "damage"] + goals=["acquisition-theft", "harassment", "damage"], ) assert str(intrusion_set) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "aliases": [ - "Zookeeper" - ], - "created": "2016-04-06T20:03:48.000Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "description": "Incidents usually feature a shared TTP of a bobcat being released...", - "goals": [ - "acquisition-theft", - "harassment", - "damage" - ], - "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", - "modified": "2016-04-06T20:03:48.000Z", - "name": "Bobcat Breakin", - "type": "intrusion-set" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "aliases": [ + "Zookeeper", + ], + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Incidents usually feature a shared TTP of a bobcat being released...", + "goals": [ + "acquisition-theft", + "harassment", + "damage", + ], + "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Bobcat Breakin", + "type": "intrusion-set", + }, + ], +) def test_parse_intrusion_set(data): - intset = stix2.parse(data) + intset = stix2.parse(data, version="2.0") assert intset.type == "intrusion-set" assert intset.id == INTRUSION_SET_ID diff --git a/stix2/test/test_kill_chain_phases.py b/stix2/test/v20/test_kill_chain_phases.py similarity index 73% rename from stix2/test/test_kill_chain_phases.py rename to stix2/test/v20/test_kill_chain_phases.py index 220c714..d150757 100644 --- a/stix2/test/test_kill_chain_phases.py +++ b/stix2/test/v20/test_kill_chain_phases.py @@ -11,7 +11,7 @@ LMCO_RECON = """{ def test_lockheed_martin_cyber_kill_chain(): - recon = stix2.KillChainPhase( + recon = stix2.v20.KillChainPhase( kill_chain_name="lockheed-martin-cyber-kill-chain", phase_name="reconnaissance", ) @@ -26,7 +26,7 @@ FOO_PRE_ATTACK = """{ def test_kill_chain_example(): - preattack = stix2.KillChainPhase( + preattack = stix2.v20.KillChainPhase( kill_chain_name="foo", phase_name="pre-attack", ) @@ -37,25 +37,25 @@ def test_kill_chain_example(): def test_kill_chain_required_properties(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.KillChainPhase() + stix2.v20.KillChainPhase() - assert excinfo.value.cls == stix2.KillChainPhase + assert excinfo.value.cls == stix2.v20.KillChainPhase assert excinfo.value.properties == ["kill_chain_name", "phase_name"] def test_kill_chain_required_property_chain_name(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.KillChainPhase(phase_name="weaponization") + stix2.v20.KillChainPhase(phase_name="weaponization") - assert excinfo.value.cls == stix2.KillChainPhase + assert excinfo.value.cls == stix2.v20.KillChainPhase assert excinfo.value.properties == ["kill_chain_name"] def test_kill_chain_required_property_phase_name(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.KillChainPhase(kill_chain_name="lockheed-martin-cyber-kill-chain") + stix2.v20.KillChainPhase(kill_chain_name="lockheed-martin-cyber-kill-chain") - assert excinfo.value.cls == stix2.KillChainPhase + assert excinfo.value.cls == stix2.v20.KillChainPhase assert excinfo.value.properties == ["phase_name"] diff --git a/stix2/test/test_malware.py b/stix2/test/v20/test_malware.py similarity index 80% rename from stix2/test/test_malware.py rename to stix2/test/v20/test_malware.py index 11bbae4..844c7d9 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/v20/test_malware.py @@ -23,7 +23,7 @@ EXPECTED_MALWARE = """{ def test_malware_with_all_required_properties(): now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) - mal = stix2.Malware( + mal = stix2.v20.Malware( type="malware", id=MALWARE_ID, created=now, @@ -53,9 +53,9 @@ def test_malware_autogenerated_properties(malware): def test_malware_type_must_be_malware(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Malware(type='xxx', **MALWARE_KWARGS) + stix2.v20.Malware(type='xxx', **MALWARE_KWARGS) - assert excinfo.value.cls == stix2.Malware + assert excinfo.value.cls == stix2.v20.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'." @@ -63,9 +63,9 @@ def test_malware_type_must_be_malware(): def test_malware_id_must_start_with_malware(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Malware(id='my-prefix--', **MALWARE_KWARGS) + stix2.v20.Malware(id='my-prefix--', **MALWARE_KWARGS) - assert excinfo.value.cls == stix2.Malware + assert excinfo.value.cls == stix2.v20.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--'." @@ -73,17 +73,17 @@ def test_malware_id_must_start_with_malware(): def test_malware_required_properties(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.Malware() + stix2.v20.Malware() - assert excinfo.value.cls == stix2.Malware + assert excinfo.value.cls == stix2.v20.Malware assert excinfo.value.properties == ["labels", "name"] def test_malware_required_property_name(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.Malware(labels=['ransomware']) + stix2.v20.Malware(labels=['ransomware']) - assert excinfo.value.cls == stix2.Malware + assert excinfo.value.cls == stix2.v20.Malware assert excinfo.value.properties == ["name"] @@ -96,26 +96,28 @@ def test_cannot_assign_to_malware_attributes(malware): def test_invalid_kwarg_to_malware(): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: - stix2.Malware(my_custom_property="foo", **MALWARE_KWARGS) + stix2.v20.Malware(my_custom_property="foo", **MALWARE_KWARGS) - assert excinfo.value.cls == stix2.Malware + assert excinfo.value.cls == stix2.v20.Malware assert excinfo.value.properties == ['my_custom_property'] assert str(excinfo.value) == "Unexpected properties for Malware: (my_custom_property)." -@pytest.mark.parametrize("data", [ - EXPECTED_MALWARE, - { - "type": "malware", - "id": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", - "created": "2016-05-12T08:17:27.000Z", - "modified": "2016-05-12T08:17:27.000Z", - "labels": ["ransomware"], - "name": "Cryptolocker", - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED_MALWARE, + { + "type": "malware", + "id": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "labels": ["ransomware"], + "name": "Cryptolocker", + }, + ], +) def test_parse_malware(data): - mal = stix2.parse(data) + mal = stix2.parse(data, version="2.0") assert mal.type == 'malware' assert mal.id == MALWARE_ID @@ -128,7 +130,7 @@ def test_parse_malware(data): def test_parse_malware_invalid_labels(): data = re.compile('\\[.+\\]', re.DOTALL).sub('1', EXPECTED_MALWARE) with pytest.raises(ValueError) as excinfo: - stix2.parse(data) + stix2.parse(data, version="2.0") assert "Invalid value for Malware 'labels'" in str(excinfo.value) @@ -141,7 +143,7 @@ def test_parse_malware_kill_chain_phases(): } ]""" data = EXPECTED_MALWARE.replace('malware"', 'malware",%s' % kill_chain) - mal = stix2.parse(data) + mal = stix2.parse(data, version="2.0") assert mal.kill_chain_phases[0].kill_chain_name == "lockheed-martin-cyber-kill-chain" assert mal.kill_chain_phases[0].phase_name == "reconnaissance" assert mal['kill_chain_phases'][0]['kill_chain_name'] == "lockheed-martin-cyber-kill-chain" @@ -157,5 +159,5 @@ def test_parse_malware_clean_kill_chain_phases(): } ]""" data = EXPECTED_MALWARE.replace('malware"', 'malware",%s' % kill_chain) - mal = stix2.parse(data) + mal = stix2.parse(data, version="2.0") assert mal['kill_chain_phases'][0]['phase_name'] == "1" diff --git a/stix2/test/test_markings.py b/stix2/test/v20/test_markings.py similarity index 79% rename from stix2/test/test_markings.py rename to stix2/test/v20/test_markings.py index 49803f3..cbf1f5b 100644 --- a/stix2/test/test_markings.py +++ b/stix2/test/v20/test_markings.py @@ -4,7 +4,7 @@ import pytest import pytz import stix2 -from stix2 import TLP_WHITE +from stix2.v20 import TLP_WHITE from .constants import MARKING_DEFINITION_ID @@ -75,11 +75,11 @@ def test_marking_def_example_with_tlp(): def test_marking_def_example_with_statement_positional_argument(): - marking_definition = stix2.MarkingDefinition( + marking_definition = stix2.v20.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") + definition=stix2.v20.StatementMarking(statement="Copyright 2016, Example Corp"), ) assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION @@ -87,11 +87,11 @@ def test_marking_def_example_with_statement_positional_argument(): def test_marking_def_example_with_kwargs_statement(): kwargs = dict(statement="Copyright 2016, Example Corp") - marking_definition = stix2.MarkingDefinition( + marking_definition = stix2.v20.MarkingDefinition( id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", created="2017-01-20T00:00:00.000Z", definition_type="statement", - definition=stix2.StatementMarking(**kwargs) + definition=stix2.v20.StatementMarking(**kwargs), ) assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION @@ -99,31 +99,31 @@ def test_marking_def_example_with_kwargs_statement(): def test_marking_def_invalid_type(): with pytest.raises(ValueError): - stix2.MarkingDefinition( + stix2.v20.MarkingDefinition( id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", created="2017-01-20T00:00:00.000Z", definition_type="my-definition-type", - definition=stix2.StatementMarking("Copyright 2016, Example Corp") + definition=stix2.v20.StatementMarking("Copyright 2016, Example Corp"), ) def test_campaign_with_markings_example(): - campaign = stix2.Campaign( + campaign = stix2.v20.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:00Z", modified="2016-04-06T20:03:00Z", name="Green Group Attacks Against Finance", description="Campaign by Green Group against a series of targets in the financial services sector.", - object_marking_refs=TLP_WHITE + object_marking_refs=TLP_WHITE, ) assert str(campaign) == EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING def test_granular_example(): - granular_marking = stix2.GranularMarking( + granular_marking = stix2.v20.GranularMarking( marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", - selectors=["abc", "abc.[23]", "abc.def", "abc.[2].efg"] + selectors=["abc", "abc.[23]", "abc.def", "abc.[2].efg"], ) assert str(granular_marking) == EXPECTED_GRANULAR_MARKING @@ -131,19 +131,19 @@ def test_granular_example(): def test_granular_example_with_bad_selector(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.GranularMarking( + stix2.v20.GranularMarking( marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", - selectors=["abc[0]"] # missing "." + selectors=["abc[0]"], # missing "." ) - assert excinfo.value.cls == stix2.GranularMarking + assert excinfo.value.cls == stix2.v20.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(): - campaign = stix2.Campaign( + campaign = stix2.v20.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:00Z", @@ -151,27 +151,31 @@ def test_campaign_with_granular_markings_example(): 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( + stix2.v20.GranularMarking( marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", - selectors=["description"]) - ]) + selectors=["description"], + ), + ], + ) assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS -@pytest.mark.parametrize("data", [ - EXPECTED_TLP_MARKING_DEFINITION, - { - "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", - "type": "marking-definition", - "created": "2017-01-20T00:00:00Z", - "definition": { - "tlp": "white" +@pytest.mark.parametrize( + "data", [ + EXPECTED_TLP_MARKING_DEFINITION, + { + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "type": "marking-definition", + "created": "2017-01-20T00:00:00Z", + "definition": { + "tlp": "white", + }, + "definition_type": "tlp", }, - "definition_type": "tlp", - }, -]) + ], +) def test_parse_marking_definition(data): - gm = stix2.parse(data) + gm = stix2.parse(data, version="2.0") assert gm.type == 'marking-definition' assert gm.id == MARKING_DEFINITION_ID @@ -180,10 +184,12 @@ def test_parse_marking_definition(data): assert gm.definition_type == "tlp" -@stix2.common.CustomMarking('x-new-marking-type', [ - ('property1', stix2.properties.StringProperty(required=True)), - ('property2', stix2.properties.IntegerProperty()), -]) +@stix2.v20.CustomMarking( + 'x-new-marking-type', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], +) class NewMarking(object): def __init__(self, property2=None, **kwargs): if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): @@ -193,11 +199,11 @@ class NewMarking(object): def test_registered_custom_marking(): nm = NewMarking(property1='something', property2=55) - marking_def = stix2.MarkingDefinition( + marking_def = stix2.v20.MarkingDefinition( id="marking-definition--00000000-0000-4000-8000-000000000012", created="2017-01-22T00:00:00.000Z", definition_type="x-new-marking-type", - definition=nm + definition=nm, ) assert marking_def.type == "marking-definition" @@ -218,21 +224,23 @@ def test_registered_custom_marking_raises_exception(): def test_not_registered_marking_raises_exception(): with pytest.raises(ValueError) as excinfo: # Used custom object on purpose to demonstrate a not-registered marking - @stix2.sdo.CustomObject('x-new-marking-type2', [ - ('property1', stix2.properties.StringProperty(required=True)), - ('property2', stix2.properties.IntegerProperty()), - ]) + @stix2.v20.CustomObject( + 'x-new-marking-type2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], + ) class NewObject2(object): def __init__(self, property2=None, **kwargs): return no = NewObject2(property1='something', property2=55) - stix2.MarkingDefinition( + stix2.v20.MarkingDefinition( id="marking-definition--00000000-0000-4000-8000-000000000012", created="2017-01-22T00:00:00.000Z", definition_type="x-new-marking-type2", - definition=no + definition=no, ) assert str(excinfo.value) == "definition_type must be a valid marking type" @@ -241,7 +249,7 @@ def test_not_registered_marking_raises_exception(): def test_marking_wrong_type_construction(): with pytest.raises(ValueError) as excinfo: # Test passing wrong type for properties. - @stix2.CustomMarking('x-new-marking-type2', ("a", "b")) + @stix2.v20.CustomMarking('x-new-marking-type2', ("a", "b")) class NewObject3(object): pass @@ -249,7 +257,7 @@ def test_marking_wrong_type_construction(): def test_campaign_add_markings(): - campaign = stix2.Campaign( + campaign = stix2.v20.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:00Z", diff --git a/stix2/test/test_object_markings.py b/stix2/test/v20/test_object_markings.py similarity index 78% rename from stix2/test/test_object_markings.py rename to stix2/test/v20/test_object_markings.py index f216355..495c45a 100644 --- a/stix2/test/test_object_markings.py +++ b/stix2/test/v20/test_object_markings.py @@ -1,7 +1,8 @@ import pytest -from stix2 import TLP_AMBER, Malware, exceptions, markings +from stix2 import exceptions, markings +from stix2.v20 import TLP_AMBER, Malware from .constants import FAKE_TIME, MALWARE_ID from .constants import MALWARE_KWARGS as MALWARE_KWARGS_CONST @@ -17,26 +18,34 @@ MALWARE_KWARGS.update({ }) -@pytest.mark.parametrize("data", [ - ( - Malware(**MALWARE_KWARGS), - Malware(object_marking_refs=[MARKING_IDS[0]], - **MALWARE_KWARGS), - MARKING_IDS[0], - ), - ( - MALWARE_KWARGS, - dict(object_marking_refs=[MARKING_IDS[0]], - **MALWARE_KWARGS), - MARKING_IDS[0], - ), - ( - Malware(**MALWARE_KWARGS), - Malware(object_marking_refs=[TLP_AMBER.id], - **MALWARE_KWARGS), - TLP_AMBER, - ), -]) +@pytest.mark.parametrize( + "data", [ + ( + Malware(**MALWARE_KWARGS), + Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + MALWARE_KWARGS, + dict( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + Malware(**MALWARE_KWARGS), + Malware( + object_marking_refs=[TLP_AMBER.id], + **MALWARE_KWARGS + ), + TLP_AMBER, + ), + ], +) def test_add_markings_one_marking(data): before = data[0] after = data[1] @@ -72,12 +81,12 @@ def test_add_markings_combination(): granular_markings=[ { "selectors": ["labels"], - "marking_ref": MARKING_IDS[2] + "marking_ref": MARKING_IDS[2], }, { "selectors": ["name"], - "marking_ref": MARKING_IDS[3] - } + "marking_ref": MARKING_IDS[3], + }, ], **MALWARE_KWARGS ) @@ -94,12 +103,14 @@ def test_add_markings_combination(): assert m in after["object_marking_refs"] -@pytest.mark.parametrize("data", [ - ([""]), - (""), - ([]), - ([MARKING_IDS[0], 456]) -]) +@pytest.mark.parametrize( + "data", [ + ([""]), + (""), + ([]), + ([MARKING_IDS[0], 456]), + ], +) def test_add_markings_bad_markings(data): before = Malware( **MALWARE_KWARGS @@ -119,62 +130,62 @@ GET_MARKINGS_TEST_DATA = \ "list value", { "g": "nested", - "h": 45 - } + "h": 45, + }, ], "x": { "y": [ "hello", - 88 + 88, ], "z": { "foo1": "bar", - "foo2": 65 - } + "foo2": 65, + }, }, "object_marking_refs": ["11"], "granular_markings": [ { "marking_ref": "1", - "selectors": ["a"] + "selectors": ["a"], }, { "marking_ref": "2", - "selectors": ["c"] + "selectors": ["c"], }, { "marking_ref": "3", - "selectors": ["c.[1]"] + "selectors": ["c.[1]"], }, { "marking_ref": "4", - "selectors": ["c.[2]"] + "selectors": ["c.[2]"], }, { "marking_ref": "5", - "selectors": ["c.[2].g"] + "selectors": ["c.[2].g"], }, { "marking_ref": "6", - "selectors": ["x"] + "selectors": ["x"], }, { "marking_ref": "7", - "selectors": ["x.y"] + "selectors": ["x.y"], }, { "marking_ref": "8", - "selectors": ["x.y.[1]"] + "selectors": ["x.y.[1]"], }, { "marking_ref": "9", - "selectors": ["x.z"] + "selectors": ["x.z"], }, { "marking_ref": "10", - "selectors": ["x.z.foo2"] + "selectors": ["x.z.foo2"], }, - ] + ], } @@ -257,18 +268,24 @@ def test_get_markings_object_and_granular_combinations(data): assert set(markings.get_markings(data, "x.z.foo2", False, True)) == set(["10"]) -@pytest.mark.parametrize("data", [ - ( - Malware(object_marking_refs=[MARKING_IDS[0]], - **MALWARE_KWARGS), - Malware(**MALWARE_KWARGS), - ), - ( - dict(object_marking_refs=[MARKING_IDS[0]], - **MALWARE_KWARGS), - MALWARE_KWARGS, - ), -]) +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + Malware(**MALWARE_KWARGS), + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + MALWARE_KWARGS, + ), + ], +) def test_remove_markings_object_level(data): before = data[0] after = data[1] @@ -283,29 +300,43 @@ def test_remove_markings_object_level(data): modified == after['modified'] -@pytest.mark.parametrize("data", [ - ( - Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], - **MALWARE_KWARGS), - Malware(object_marking_refs=[MARKING_IDS[1]], - **MALWARE_KWARGS), - [MARKING_IDS[0], MARKING_IDS[2]], - ), - ( - dict(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], - **MALWARE_KWARGS), - dict(object_marking_refs=[MARKING_IDS[1]], - **MALWARE_KWARGS), - [MARKING_IDS[0], MARKING_IDS[2]], - ), - ( - Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], TLP_AMBER.id], - **MALWARE_KWARGS), - Malware(object_marking_refs=[MARKING_IDS[1]], - **MALWARE_KWARGS), - [MARKING_IDS[0], TLP_AMBER], - ), -]) +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + Malware( + object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[2]], + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + dict( + object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[2]], + ), + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], TLP_AMBER.id], + **MALWARE_KWARGS + ), + Malware( + object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], TLP_AMBER], + ), + ], +) def test_remove_markings_multiple(data): before = data[0] after = data[1] @@ -325,18 +356,24 @@ def test_remove_markings_bad_markings(): assert str(excinfo.value) == "Marking ['%s'] was not found in Malware!" % MARKING_IDS[4] -@pytest.mark.parametrize("data", [ - ( - Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], - **MALWARE_KWARGS), - Malware(**MALWARE_KWARGS), - ), - ( - dict(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], - **MALWARE_KWARGS), - MALWARE_KWARGS, - ), -]) +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + Malware(**MALWARE_KWARGS), + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + MALWARE_KWARGS, + ), + ], +) def test_clear_markings(data): before = data[0] after = data[1] @@ -358,62 +395,62 @@ def test_is_marked_object_and_granular_combinations(): "list value", { "g": "nested", - "h": 45 - } + "h": 45, + }, ], "x": { "y": [ "hello", - 88 + 88, ], "z": { "foo1": "bar", - "foo2": 65 - } + "foo2": 65, + }, }, "object_marking_refs": "11", "granular_markings": [ { "marking_ref": "1", - "selectors": ["a"] + "selectors": ["a"], }, { "marking_ref": "2", - "selectors": ["c"] + "selectors": ["c"], }, { "marking_ref": "3", - "selectors": ["c.[1]"] + "selectors": ["c.[1]"], }, { "marking_ref": "4", - "selectors": ["c.[2]"] + "selectors": ["c.[2]"], }, { "marking_ref": "5", - "selectors": ["c.[2].g"] + "selectors": ["c.[2].g"], }, { "marking_ref": "6", - "selectors": ["x"] + "selectors": ["x"], }, { "marking_ref": "7", - "selectors": ["x.y"] + "selectors": ["x.y"], }, { "marking_ref": "8", - "selectors": ["x.y.[1]"] + "selectors": ["x.y.[1]"], }, { "marking_ref": "9", - "selectors": ["x.z"] + "selectors": ["x.z"], }, { "marking_ref": "10", - "selectors": ["x.z.foo2"] + "selectors": ["x.z.foo2"], }, - ] + ], } assert markings.is_marked(test_sdo, ["1"], "a", False, False) @@ -490,18 +527,24 @@ def test_is_marked_object_and_granular_combinations(): assert markings.is_marked(test_sdo, ["2"], None, True, True) is False -@pytest.mark.parametrize("data", [ - ( - Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], - **MALWARE_KWARGS), - Malware(**MALWARE_KWARGS), - ), - ( - dict(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], - **MALWARE_KWARGS), - MALWARE_KWARGS, - ), -]) +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + Malware(**MALWARE_KWARGS), + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + MALWARE_KWARGS, + ), + ], +) def test_is_marked_no_markings(data): marked = data[0] nonmarked = data[1] @@ -531,12 +574,14 @@ def test_set_marking(): assert x in after["object_marking_refs"] -@pytest.mark.parametrize("data", [ - ([]), - ([""]), - (""), - ([MARKING_IDS[4], 687]) -]) +@pytest.mark.parametrize( + "data", [ + ([]), + ([""]), + (""), + ([MARKING_IDS[4], 687]), + ], +) def test_set_marking_bad_input(data): before = Malware( object_marking_refs=[MARKING_IDS[0]], diff --git a/stix2/test/test_observed_data.py b/stix2/test/v20/test_observed_data.py similarity index 59% rename from stix2/test/test_observed_data.py rename to stix2/test/v20/test_observed_data.py index 11c74ca..41a80d6 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/v20/test_observed_data.py @@ -30,7 +30,7 @@ EXPECTED = """{ def test_observed_data_example(): - observed_data = stix2.ObservedData( + observed_data = stix2.v20.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T19:58:16.000Z", @@ -41,7 +41,7 @@ def test_observed_data_example(): objects={ "0": { "name": "foo.exe", - "type": "file" + "type": "file", }, }, ) @@ -75,7 +75,7 @@ EXPECTED_WITH_REF = """{ def test_observed_data_example_with_refs(): - observed_data = stix2.ObservedData( + observed_data = stix2.v20.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T19:58:16.000Z", @@ -86,13 +86,13 @@ def test_observed_data_example_with_refs(): objects={ "0": { "name": "foo.exe", - "type": "file" + "type": "file", }, "1": { "type": "directory", "path": "/usr/home", - "contains_refs": ["0"] - } + "contains_refs": ["0"], + }, }, ) @@ -101,7 +101,7 @@ def test_observed_data_example_with_refs(): def test_observed_data_example_with_bad_refs(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.ObservedData( + stix2.v20.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T19:58:16.000Z", @@ -112,24 +112,24 @@ def test_observed_data_example_with_bad_refs(): objects={ "0": { "type": "file", - "name": "foo.exe" + "name": "foo.exe", }, "1": { "type": "directory", "path": "/usr/home", - "contains_refs": ["2"] - } + "contains_refs": ["2"], + }, }, ) - assert excinfo.value.cls == stix2.ObservedData + assert excinfo.value.cls == stix2.v20.ObservedData assert excinfo.value.prop_name == "objects" assert excinfo.value.reason == "Invalid object reference for 'Directory:contains_refs': '2' is not a valid object in local scope" def test_observed_data_example_with_non_dictionary(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.ObservedData( + stix2.v20.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T19:58:16.000Z", @@ -140,14 +140,14 @@ def test_observed_data_example_with_non_dictionary(): objects="file: foo.exe", ) - assert excinfo.value.cls == stix2.ObservedData + assert excinfo.value.cls == stix2.v20.ObservedData assert excinfo.value.prop_name == "objects" assert 'must contain a dictionary' in excinfo.value.reason def test_observed_data_example_with_empty_dictionary(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.ObservedData( + stix2.v20.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T19:58:16.000Z", @@ -158,32 +158,34 @@ def test_observed_data_example_with_empty_dictionary(): objects={}, ) - assert excinfo.value.cls == stix2.ObservedData + assert excinfo.value.cls == stix2.v20.ObservedData assert excinfo.value.prop_name == "objects" assert 'must contain a non-empty dictionary' in excinfo.value.reason -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "type": "observed-data", - "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", - "created": "2016-04-06T19:58:16.000Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "first_observed": "2015-12-21T19:00:00Z", - "last_observed": "2015-12-21T19:00:00Z", - "modified": "2016-04-06T19:58:16.000Z", - "number_observed": 50, - "objects": { - "0": { - "name": "foo.exe", - "type": "file" - } - } - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "type": "observed-data", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "created": "2016-04-06T19:58:16.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "modified": "2016-04-06T19:58:16.000Z", + "number_observed": 50, + "objects": { + "0": { + "name": "foo.exe", + "type": "file", + }, + }, + }, + ], +) def test_parse_observed_data(data): - odata = stix2.parse(data) + odata = stix2.parse(data, version="2.0") assert odata.type == 'observed-data' assert odata.id == OBSERVED_DATA_ID @@ -195,13 +197,14 @@ def test_parse_observed_data(data): assert odata.objects["0"].type == "file" -@pytest.mark.parametrize("data", [ - """"0": { +@pytest.mark.parametrize( + "data", [ + """"0": { "type": "artifact", "mime_type": "image/jpeg", "payload_bin": "VBORw0KGgoAAAANSUhEUgAAADI==" }""", - """"0": { + """"0": { "type": "artifact", "mime_type": "image/jpeg", "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", @@ -209,20 +212,22 @@ def test_parse_observed_data(data): "MD5": "6826f9a05da08134006557758bb3afbb" } }""", -]) + ], +) def test_parse_artifact_valid(data): odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) - odata = stix2.parse(odata_str) + odata = stix2.parse(odata_str, version="2.0") assert odata.objects["0"].type == "artifact" -@pytest.mark.parametrize("data", [ - """"0": { +@pytest.mark.parametrize( + "data", [ + """"0": { "type": "artifact", "mime_type": "image/jpeg", "payload_bin": "abcVBORw0KGgoAAAANSUhEUgAAADI==" }""", - """"0": { + """"0": { "type": "artifact", "mime_type": "image/jpeg", "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", @@ -230,57 +235,63 @@ def test_parse_artifact_valid(data): "MD5": "a" } }""", -]) + ], +) def test_parse_artifact_invalid(data): odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) with pytest.raises(ValueError): - stix2.parse(odata_str) + stix2.parse(odata_str, version="2.0") def test_artifact_example_dependency_error(): with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: - stix2.Artifact(url="http://example.com/sirvizio.exe") + stix2.v20.Artifact(url="http://example.com/sirvizio.exe") assert excinfo.value.dependencies == [("hashes", "url")] assert str(excinfo.value) == "The property dependencies for Artifact: (hashes, url) are not met." -@pytest.mark.parametrize("data", [ - """"0": { +@pytest.mark.parametrize( + "data", [ + """"0": { "type": "autonomous-system", "number": 15139, "name": "Slime Industries", "rir": "ARIN" }""", -]) + ], +) def test_parse_autonomous_system_valid(data): odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) - odata = stix2.parse(odata_str) + odata = stix2.parse(odata_str, version="2.0") assert odata.objects["0"].type == "autonomous-system" assert odata.objects["0"].number == 15139 assert odata.objects["0"].name == "Slime Industries" assert odata.objects["0"].rir == "ARIN" -@pytest.mark.parametrize("data", [ - """{ +@pytest.mark.parametrize( + "data", [ + """{ "type": "email-addr", "value": "john@example.com", "display_name": "John Doe", "belongs_to_ref": "0" }""", -]) + ], +) def test_parse_email_address(data): - odata = stix2.parse_observable(data, {"0": "user-account"}) + odata = stix2.parse_observable(data, {"0": "user-account"}, version='2.0') assert odata.type == "email-addr" odata_str = re.compile('"belongs_to_ref": "0"', re.DOTALL).sub('"belongs_to_ref": "3"', data) with pytest.raises(stix2.exceptions.InvalidObjRefError): - stix2.parse_observable(odata_str, {"0": "user-account"}) + stix2.parse_observable(odata_str, {"0": "user-account"}, version='2.0') -@pytest.mark.parametrize("data", [ - """ +@pytest.mark.parametrize( + "data", [ + """ { "type": "email-message", "is_multipart": true, @@ -317,8 +328,9 @@ def test_parse_email_address(data): } ] } - """ -]) + """, + ], +) def test_parse_email_message(data): valid_refs = { "0": "email-message", @@ -328,13 +340,14 @@ def test_parse_email_message(data): "4": "artifact", "5": "file", } - odata = stix2.parse_observable(data, valid_refs) + odata = stix2.parse_observable(data, valid_refs, version='2.0') assert odata.type == "email-message" assert odata.body_multipart[0].content_disposition == "inline" -@pytest.mark.parametrize("data", [ - """ +@pytest.mark.parametrize( + "data", [ + """ { "type": "email-message", "from_ref": "0", @@ -344,22 +357,24 @@ def test_parse_email_message(data): "subject": "Saying Hello", "body": "Cats are funny!" } - """ -]) + """, + ], +) def test_parse_email_message_not_multipart(data): valid_refs = { "0": "email-addr", "1": "email-addr", } with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: - stix2.parse_observable(data, valid_refs) + stix2.parse_observable(data, valid_refs, version='2.0') - assert excinfo.value.cls == stix2.EmailMessage + assert excinfo.value.cls == stix2.v20.EmailMessage assert excinfo.value.dependencies == [("is_multipart", "body")] -@pytest.mark.parametrize("data", [ - """"0": { +@pytest.mark.parametrize( + "data", [ + """"0": { "type": "file", "hashes": { "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a" @@ -395,15 +410,17 @@ def test_parse_email_message_not_multipart(data): } } }""", -]) + ], +) def test_parse_file_archive(data): odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) - odata = stix2.parse(odata_str) + odata = stix2.parse(odata_str, version="2.0") assert odata.objects["3"].extensions['archive-ext'].version == "5.0" -@pytest.mark.parametrize("data", [ - """ +@pytest.mark.parametrize( + "data", [ + """ { "type": "email-message", "is_multipart": true, @@ -439,8 +456,9 @@ def test_parse_file_archive(data): } ] } - """ -]) + """, + ], +) def test_parse_email_message_with_at_least_one_error(data): valid_refs = { "0": "email-message", @@ -451,16 +469,17 @@ def test_parse_email_message_with_at_least_one_error(data): "5": "file", } with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: - stix2.parse_observable(data, valid_refs) + stix2.parse_observable(data, valid_refs, version='2.0') - assert excinfo.value.cls == stix2.EmailMIMEComponent + assert excinfo.value.cls == stix2.v20.EmailMIMEComponent assert excinfo.value.properties == ["body", "body_raw_ref"] assert "At least one of the" in str(excinfo.value) assert "must be populated" in str(excinfo.value) -@pytest.mark.parametrize("data", [ - """ +@pytest.mark.parametrize( + "data", [ + """ { "type": "network-traffic", "src_ref": "0", @@ -469,10 +488,14 @@ def test_parse_email_message_with_at_least_one_error(data): "tcp" ] } - """ -]) + """, + ], +) def test_parse_basic_tcp_traffic(data): - odata = stix2.parse_observable(data, {"0": "ipv4-addr", "1": "ipv4-addr"}) + odata = stix2.parse_observable( + data, {"0": "ipv4-addr", "1": "ipv4-addr"}, + version='2.0', + ) assert odata.type == "network-traffic" assert odata.src_ref == "0" @@ -480,8 +503,9 @@ def test_parse_basic_tcp_traffic(data): assert odata.protocols == ["tcp"] -@pytest.mark.parametrize("data", [ - """ +@pytest.mark.parametrize( + "data", [ + """ { "type": "network-traffic", "src_port": 2487, @@ -496,13 +520,14 @@ def test_parse_basic_tcp_traffic(data): "4" ] } - """ -]) + """, + ], +) def test_parse_basic_tcp_traffic_with_error(data): with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: - stix2.parse_observable(data, {"4": "network-traffic"}) + stix2.parse_observable(data, {"4": "network-traffic"}, version='2.0') - assert excinfo.value.cls == stix2.NetworkTraffic + assert excinfo.value.cls == stix2.v20.NetworkTraffic assert excinfo.value.properties == ["dst_ref", "src_ref"] @@ -537,7 +562,7 @@ EXPECTED_PROCESS_OD = """{ def test_observed_data_with_process_example(): - observed_data = stix2.ObservedData( + observed_data = stix2.v20.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T19:58:16.000Z", @@ -549,7 +574,7 @@ def test_observed_data_with_process_example(): "0": { "type": "file", "hashes": { - "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f", }, }, "1": { @@ -558,11 +583,12 @@ def test_observed_data_with_process_example(): "name": "gedit-bin", "created": "2016-01-20T14:11:25.55Z", "arguments": [ - "--new-window" + "--new-window", ], - "binary_ref": "0" - } - }) + "binary_ref": "0", + }, + }, + ) assert observed_data.objects["0"].type == "file" assert observed_data.objects["0"].hashes["SHA-256"] == "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" @@ -575,11 +601,13 @@ def test_observed_data_with_process_example(): # creating cyber observables directly def test_artifact_example(): - art = stix2.Artifact(mime_type="image/jpeg", - url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", - hashes={ - "MD5": "6826f9a05da08134006557758bb3afbb" - }) + art = stix2.v20.Artifact( + mime_type="image/jpeg", + url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + hashes={ + "MD5": "6826f9a05da08134006557758bb3afbb", + }, + ) assert art.mime_type == "image/jpeg" assert art.url == "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg" assert art.hashes["MD5"] == "6826f9a05da08134006557758bb3afbb" @@ -587,25 +615,29 @@ def test_artifact_example(): def test_artifact_mutual_exclusion_error(): with pytest.raises(stix2.exceptions.MutuallyExclusivePropertiesError) as excinfo: - stix2.Artifact(mime_type="image/jpeg", - url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", - hashes={ - "MD5": "6826f9a05da08134006557758bb3afbb" - }, - payload_bin="VBORw0KGgoAAAANSUhEUgAAADI==") + stix2.v20.Artifact( + mime_type="image/jpeg", + url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + hashes={ + "MD5": "6826f9a05da08134006557758bb3afbb", + }, + payload_bin="VBORw0KGgoAAAANSUhEUgAAADI==", + ) - assert excinfo.value.cls == stix2.Artifact + assert excinfo.value.cls == stix2.v20.Artifact assert excinfo.value.properties == ["payload_bin", "url"] assert 'are mutually exclusive' in str(excinfo.value) def test_directory_example(): - dir = stix2.Directory(_valid_refs={"1": "file"}, - path='/usr/lib', - created="2015-12-21T19:00:00Z", - modified="2015-12-24T19:00:00Z", - accessed="2015-12-21T20:00:00Z", - contains_refs=["1"]) + dir = stix2.v20.Directory( + _valid_refs={"1": "file"}, + path='/usr/lib', + created="2015-12-21T19:00:00Z", + modified="2015-12-24T19:00:00Z", + accessed="2015-12-21T20:00:00Z", + contains_refs=["1"], + ) assert dir.path == '/usr/lib' assert dir.created == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) @@ -616,21 +648,25 @@ def test_directory_example(): def test_directory_example_ref_error(): with pytest.raises(stix2.exceptions.InvalidObjRefError) as excinfo: - stix2.Directory(_valid_refs=[], - path='/usr/lib', - created="2015-12-21T19:00:00Z", - modified="2015-12-24T19:00:00Z", - accessed="2015-12-21T20:00:00Z", - contains_refs=["1"]) + stix2.v20.Directory( + _valid_refs=[], + path='/usr/lib', + created="2015-12-21T19:00:00Z", + modified="2015-12-24T19:00:00Z", + accessed="2015-12-21T20:00:00Z", + contains_refs=["1"], + ) - assert excinfo.value.cls == stix2.Directory + assert excinfo.value.cls == stix2.v20.Directory assert excinfo.value.prop_name == "contains_refs" def test_domain_name_example(): - dn = stix2.DomainName(_valid_refs={"1": 'domain-name'}, - value="example.com", - resolves_to_refs=["1"]) + dn = stix2.v20.DomainName( + _valid_refs={"1": 'domain-name'}, + value="example.com", + resolves_to_refs=["1"], + ) assert dn.value == "example.com" assert dn.resolves_to_refs == ["1"] @@ -638,28 +674,32 @@ def test_domain_name_example(): def test_domain_name_example_invalid_ref_type(): with pytest.raises(stix2.exceptions.InvalidObjRefError) as excinfo: - stix2.DomainName(_valid_refs={"1": "file"}, - value="example.com", - resolves_to_refs=["1"]) + stix2.v20.DomainName( + _valid_refs={"1": "file"}, + value="example.com", + resolves_to_refs=["1"], + ) - assert excinfo.value.cls == stix2.DomainName + assert excinfo.value.cls == stix2.v20.DomainName assert excinfo.value.prop_name == "resolves_to_refs" def test_file_example(): - f = stix2.File(name="qwerty.dll", - hashes={ - "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a"}, - size=100, - magic_number_hex="1C", - mime_type="application/msword", - created="2016-12-21T19:00:00Z", - modified="2016-12-24T19:00:00Z", - accessed="2016-12-21T20:00:00Z", - is_encrypted=True, - encryption_algorithm="AES128-CBC", - decryption_key="fred" - ) + f = stix2.v20.File( + name="qwerty.dll", + hashes={ + "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a", + }, + size=100, + magic_number_hex="1C", + mime_type="application/msword", + created="2016-12-21T19:00:00Z", + modified="2016-12-24T19:00:00Z", + accessed="2016-12-21T20:00:00Z", + is_encrypted=True, + encryption_algorithm="AES128-CBC", + decryption_key="fred", + ) assert f.name == "qwerty.dll" assert f.size == 100 @@ -675,17 +715,19 @@ def test_file_example(): def test_file_example_with_NTFSExt(): - f = stix2.File(name="abc.txt", - extensions={ - "ntfs-ext": { - "alternate_data_streams": [ - { - "name": "second.stream", - "size": 25536 - } - ] - } - }) + f = stix2.v20.File( + name="abc.txt", + extensions={ + "ntfs-ext": { + "alternate_data_streams": [ + { + "name": "second.stream", + "size": 25536, + }, + ], + }, + }, + ) assert f.name == "abc.txt" assert f.extensions["ntfs-ext"].alternate_data_streams[0].size == 25536 @@ -693,32 +735,35 @@ def test_file_example_with_NTFSExt(): def test_file_example_with_empty_NTFSExt(): with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: - stix2.File(name="abc.txt", - extensions={ - "ntfs-ext": { - } - }) + stix2.v20.File( + name="abc.txt", + extensions={ + "ntfs-ext": {}, + }, + ) - assert excinfo.value.cls == stix2.NTFSExt + assert excinfo.value.cls == stix2.v20.NTFSExt assert excinfo.value.properties == sorted(list(stix2.NTFSExt._properties.keys())) def test_file_example_with_PDFExt(): - f = stix2.File(name="qwerty.dll", - extensions={ - "pdf-ext": { - "version": "1.7", - "document_info_dict": { - "Title": "Sample document", - "Author": "Adobe Systems Incorporated", - "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", - "Producer": "Acrobat Distiller 3.01 for Power Macintosh", - "CreationDate": "20070412090123-02" - }, - "pdfid0": "DFCE52BD827ECF765649852119D", - "pdfid1": "57A1E0F9ED2AE523E313C" - } - }) + f = stix2.v20.File( + name="qwerty.dll", + extensions={ + "pdf-ext": { + "version": "1.7", + "document_info_dict": { + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02", + }, + "pdfid0": "DFCE52BD827ECF765649852119D", + "pdfid1": "57A1E0F9ED2AE523E313C", + }, + }, + ) assert f.name == "qwerty.dll" assert f.extensions["pdf-ext"].version == "1.7" @@ -726,21 +771,23 @@ def test_file_example_with_PDFExt(): def test_file_example_with_PDFExt_Object(): - f = stix2.File(name="qwerty.dll", - extensions={ - "pdf-ext": - stix2.PDFExt(version="1.7", - document_info_dict={ - "Title": "Sample document", - "Author": "Adobe Systems Incorporated", - "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", - "Producer": "Acrobat Distiller 3.01 for Power Macintosh", - "CreationDate": "20070412090123-02" - }, - pdfid0="DFCE52BD827ECF765649852119D", - pdfid1="57A1E0F9ED2AE523E313C") - - }) + f = stix2.v20.File( + name="qwerty.dll", + extensions={ + "pdf-ext": stix2.v20.PDFExt( + version="1.7", + document_info_dict={ + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02", + }, + pdfid0="DFCE52BD827ECF765649852119D", + pdfid1="57A1E0F9ED2AE523E313C", + ), + }, + ) assert f.name == "qwerty.dll" assert f.extensions["pdf-ext"].version == "1.7" @@ -748,18 +795,20 @@ def test_file_example_with_PDFExt_Object(): def test_file_example_with_RasterImageExt_Object(): - f = stix2.File(name="qwerty.jpeg", - extensions={ - "raster-image-ext": { - "bits_per_pixel": 123, - "exif_tags": { - "Make": "Nikon", - "Model": "D7000", - "XResolution": 4928, - "YResolution": 3264 - } - } - }) + f = stix2.v20.File( + name="qwerty.jpeg", + extensions={ + "raster-image-ext": { + "bits_per_pixel": 123, + "exif_tags": { + "Make": "Nikon", + "Model": "D7000", + "XResolution": 4928, + "YResolution": 3264, + }, + }, + }, + ) assert f.name == "qwerty.jpeg" assert f.extensions["raster-image-ext"].bits_per_pixel == 123 assert f.extensions["raster-image-ext"].exif_tags["XResolution"] == 4928 @@ -801,150 +850,163 @@ RASTER_IMAGE_EXT = """{ def test_raster_image_ext_parse(): - obj = stix2.parse(RASTER_IMAGE_EXT) + obj = stix2.parse(RASTER_IMAGE_EXT, version="2.0") assert obj.objects["0"].extensions['raster-image-ext'].image_width == 1024 def test_raster_images_ext_create(): - ext = stix2.RasterImageExt(image_width=1024) + ext = stix2.v20.RasterImageExt(image_width=1024) assert "image_width" in str(ext) def test_file_example_with_WindowsPEBinaryExt(): - f = stix2.File(name="qwerty.dll", - extensions={ - "windows-pebinary-ext": { - "pe_type": "exe", - "machine_hex": "014c", - "number_of_sections": 4, - "time_date_stamp": "2016-01-22T12:31:12Z", - "pointer_to_symbol_table_hex": "74726144", - "number_of_symbols": 4542568, - "size_of_optional_header": 224, - "characteristics_hex": "818f", - "optional_header": { - "magic_hex": "010b", - "major_linker_version": 2, - "minor_linker_version": 25, - "size_of_code": 512, - "size_of_initialized_data": 283648, - "size_of_uninitialized_data": 0, - "address_of_entry_point": 4096, - "base_of_code": 4096, - "base_of_data": 8192, - "image_base": 14548992, - "section_alignment": 4096, - "file_alignment": 4096, - "major_os_version": 1, - "minor_os_version": 0, - "major_image_version": 0, - "minor_image_version": 0, - "major_subsystem_version": 4, - "minor_subsystem_version": 0, - "win32_version_value_hex": "00", - "size_of_image": 299008, - "size_of_headers": 4096, - "checksum_hex": "00", - "subsystem_hex": "03", - "dll_characteristics_hex": "00", - "size_of_stack_reserve": 100000, - "size_of_stack_commit": 8192, - "size_of_heap_reserve": 100000, - "size_of_heap_commit": 4096, - "loader_flags_hex": "abdbffde", - "number_of_rva_and_sizes": 3758087646 - }, - "sections": [ - { - "name": "CODE", - "entropy": 0.061089 - }, - { - "name": "DATA", - "entropy": 7.980693 - }, - { - "name": "NicolasB", - "entropy": 0.607433 - }, - { - "name": ".idata", - "entropy": 0.607433 - } - ] - } - - }) + f = stix2.v20.File( + name="qwerty.dll", + extensions={ + "windows-pebinary-ext": { + "pe_type": "exe", + "machine_hex": "014c", + "number_of_sections": 4, + "time_date_stamp": "2016-01-22T12:31:12Z", + "pointer_to_symbol_table_hex": "74726144", + "number_of_symbols": 4542568, + "size_of_optional_header": 224, + "characteristics_hex": "818f", + "optional_header": { + "magic_hex": "010b", + "major_linker_version": 2, + "minor_linker_version": 25, + "size_of_code": 512, + "size_of_initialized_data": 283648, + "size_of_uninitialized_data": 0, + "address_of_entry_point": 4096, + "base_of_code": 4096, + "base_of_data": 8192, + "image_base": 14548992, + "section_alignment": 4096, + "file_alignment": 4096, + "major_os_version": 1, + "minor_os_version": 0, + "major_image_version": 0, + "minor_image_version": 0, + "major_subsystem_version": 4, + "minor_subsystem_version": 0, + "win32_version_value_hex": "00", + "size_of_image": 299008, + "size_of_headers": 4096, + "checksum_hex": "00", + "subsystem_hex": "03", + "dll_characteristics_hex": "00", + "size_of_stack_reserve": 100000, + "size_of_stack_commit": 8192, + "size_of_heap_reserve": 100000, + "size_of_heap_commit": 4096, + "loader_flags_hex": "abdbffde", + "number_of_rva_and_sizes": 3758087646, + }, + "sections": [ + { + "name": "CODE", + "entropy": 0.061089, + }, + { + "name": "DATA", + "entropy": 7.980693, + }, + { + "name": "NicolasB", + "entropy": 0.607433, + }, + { + "name": ".idata", + "entropy": 0.607433, + }, + ], + }, + }, + ) assert f.name == "qwerty.dll" assert f.extensions["windows-pebinary-ext"].sections[2].entropy == 0.607433 def test_file_example_encryption_error(): with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: - stix2.File(name="qwerty.dll", - is_encrypted=False, - encryption_algorithm="AES128-CBC") + stix2.v20.File( + name="qwerty.dll", + is_encrypted=False, + encryption_algorithm="AES128-CBC", + ) - assert excinfo.value.cls == stix2.File + assert excinfo.value.cls == stix2.v20.File assert excinfo.value.dependencies == [("is_encrypted", "encryption_algorithm")] assert "property dependencies" in str(excinfo.value) assert "are not met" in str(excinfo.value) with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: - stix2.File(name="qwerty.dll", - encryption_algorithm="AES128-CBC") + stix2.v20.File( + name="qwerty.dll", + encryption_algorithm="AES128-CBC", + ) def test_ip4_address_example(): - ip4 = stix2.IPv4Address(_valid_refs={"4": "mac-addr", "5": "mac-addr"}, - value="198.51.100.3", - resolves_to_refs=["4", "5"]) + ip4 = stix2.v20.IPv4Address( + _valid_refs={"4": "mac-addr", "5": "mac-addr"}, + value="198.51.100.3", + resolves_to_refs=["4", "5"], + ) assert ip4.value == "198.51.100.3" assert ip4.resolves_to_refs == ["4", "5"] def test_ip4_address_example_cidr(): - ip4 = stix2.IPv4Address(value="198.51.100.0/24") + ip4 = stix2.v20.IPv4Address(value="198.51.100.0/24") assert ip4.value == "198.51.100.0/24" def test_ip6_address_example(): - ip6 = stix2.IPv6Address(value="2001:0db8:85a3:0000:0000:8a2e:0370:7334") + ip6 = stix2.v20.IPv6Address(value="2001:0db8:85a3:0000:0000:8a2e:0370:7334") assert ip6.value == "2001:0db8:85a3:0000:0000:8a2e:0370:7334" def test_mac_address_example(): - ip6 = stix2.MACAddress(value="d2:fb:49:24:37:18") + ip6 = stix2.v20.MACAddress(value="d2:fb:49:24:37:18") assert ip6.value == "d2:fb:49:24:37:18" def test_network_traffic_example(): - nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr", "1": "ipv4-addr"}, - protocols="tcp", - src_ref="0", - dst_ref="1") + nt = stix2.v20.NetworkTraffic( + _valid_refs={"0": "ipv4-addr", "1": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + dst_ref="1", + ) assert nt.protocols == ["tcp"] assert nt.src_ref == "0" assert nt.dst_ref == "1" def test_network_traffic_http_request_example(): - h = stix2.HTTPRequestExt(request_method="get", - request_value="/download.html", - request_version="http/1.1", - request_header={ - "Accept-Encoding": "gzip,deflate", - "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", - "Host": "www.example.com" - }) - nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, - protocols="tcp", - src_ref="0", - extensions={'http-request-ext': h}) + h = stix2.v20.HTTPRequestExt( + request_method="get", + request_value="/download.html", + request_version="http/1.1", + request_header={ + "Accept-Encoding": "gzip,deflate", + "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", + "Host": "www.example.com", + }, + ) + nt = stix2.v20.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'http-request-ext': h}, + ) assert nt.extensions['http-request-ext'].request_method == "get" assert nt.extensions['http-request-ext'].request_value == "/download.html" assert nt.extensions['http-request-ext'].request_version == "http/1.1" @@ -954,25 +1016,30 @@ def test_network_traffic_http_request_example(): def test_network_traffic_icmp_example(): - h = stix2.ICMPExt(icmp_type_hex="08", - icmp_code_hex="00") - nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, - protocols="tcp", - src_ref="0", - extensions={'icmp-ext': h}) + h = stix2.v20.ICMPExt(icmp_type_hex="08", icmp_code_hex="00") + nt = stix2.v20.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'icmp-ext': h}, + ) assert nt.extensions['icmp-ext'].icmp_type_hex == "08" assert nt.extensions['icmp-ext'].icmp_code_hex == "00" def test_network_traffic_socket_example(): - h = stix2.SocketExt(is_listening=True, - address_family="AF_INET", - protocol_family="PF_INET", - socket_type="SOCK_STREAM") - nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, - protocols="tcp", - src_ref="0", - extensions={'socket-ext': h}) + h = stix2.v20.SocketExt( + is_listening=True, + address_family="AF_INET", + protocol_family="PF_INET", + socket_type="SOCK_STREAM", + ) + nt = stix2.v20.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'socket-ext': h}, + ) assert nt.extensions['socket-ext'].is_listening assert nt.extensions['socket-ext'].address_family == "AF_INET" assert nt.extensions['socket-ext'].protocol_family == "PF_INET" @@ -980,27 +1047,31 @@ def test_network_traffic_socket_example(): def test_network_traffic_tcp_example(): - h = stix2.TCPExt(src_flags_hex="00000002") - nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, - protocols="tcp", - src_ref="0", - extensions={'tcp-ext': h}) + h = stix2.v20.TCPExt(src_flags_hex="00000002") + nt = stix2.v20.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'tcp-ext': h}, + ) assert nt.extensions['tcp-ext'].src_flags_hex == "00000002" def test_mutex_example(): - m = stix2.Mutex(name="barney") + m = stix2.v20.Mutex(name="barney") assert m.name == "barney" def test_process_example(): - p = stix2.Process(_valid_refs={"0": "file"}, - pid=1221, - name="gedit-bin", - created="2016-01-20T14:11:25.55Z", - arguments=["--new-window"], - binary_ref="0") + p = stix2.v20.Process( + _valid_refs={"0": "file"}, + pid=1221, + name="gedit-bin", + created="2016-01-20T14:11:25.55Z", + arguments=["--new-window"], + binary_ref="0", + ) assert p.name == "gedit-bin" assert p.arguments == ["--new-window"] @@ -1008,40 +1079,46 @@ def test_process_example(): def test_process_example_empty_error(): with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: - stix2.Process() + stix2.v20.Process() - assert excinfo.value.cls == stix2.Process - properties_of_process = list(stix2.Process._properties.keys()) + assert excinfo.value.cls == stix2.v20.Process + properties_of_process = list(stix2.v20.Process._properties.keys()) properties_of_process.remove("type") assert excinfo.value.properties == sorted(properties_of_process) msg = "At least one of the ({1}) properties for {0} must be populated." - msg = msg.format(stix2.Process.__name__, - ", ".join(sorted(properties_of_process))) + msg = msg.format( + stix2.v20.Process.__name__, + ", ".join(sorted(properties_of_process)), + ) assert str(excinfo.value) == msg def test_process_example_empty_with_extensions(): with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: - stix2.Process(extensions={ - "windows-process-ext": {} - }) + stix2.v20.Process( + extensions={ + "windows-process-ext": {}, + }, + ) - assert excinfo.value.cls == stix2.WindowsProcessExt - properties_of_extension = list(stix2.WindowsProcessExt._properties.keys()) + assert excinfo.value.cls == stix2.v20.WindowsProcessExt + properties_of_extension = list(stix2.v20.WindowsProcessExt._properties.keys()) assert excinfo.value.properties == sorted(properties_of_extension) def test_process_example_windows_process_ext(): - proc = stix2.Process(pid=314, - name="foobar.exe", - extensions={ - "windows-process-ext": { - "aslr_enabled": True, - "dep_enabled": True, - "priority": "HIGH_PRIORITY_CLASS", - "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309" - } - }) + proc = stix2.v20.Process( + pid=314, + name="foobar.exe", + extensions={ + "windows-process-ext": { + "aslr_enabled": True, + "dep_enabled": True, + "priority": "HIGH_PRIORITY_CLASS", + "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309", + }, + }, + ) assert proc.extensions["windows-process-ext"].aslr_enabled assert proc.extensions["windows-process-ext"].dep_enabled assert proc.extensions["windows-process-ext"].priority == "HIGH_PRIORITY_CLASS" @@ -1050,47 +1127,51 @@ def test_process_example_windows_process_ext(): def test_process_example_windows_process_ext_empty(): with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: - stix2.Process(pid=1221, - name="gedit-bin", - extensions={ - "windows-process-ext": {} - }) + stix2.v20.Process( + pid=1221, + name="gedit-bin", + extensions={ + "windows-process-ext": {}, + }, + ) - assert excinfo.value.cls == stix2.WindowsProcessExt - properties_of_extension = list(stix2.WindowsProcessExt._properties.keys()) + assert excinfo.value.cls == stix2.v20.WindowsProcessExt + properties_of_extension = list(stix2.v20.WindowsProcessExt._properties.keys()) assert excinfo.value.properties == sorted(properties_of_extension) def test_process_example_extensions_empty(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Process(extensions={}) + stix2.v20.Process(extensions={}) - assert excinfo.value.cls == stix2.Process + assert excinfo.value.cls == stix2.v20.Process assert excinfo.value.prop_name == 'extensions' assert 'non-empty dictionary' in excinfo.value.reason def test_process_example_with_WindowsProcessExt_Object(): - p = stix2.Process(extensions={ - "windows-process-ext": stix2.WindowsProcessExt(aslr_enabled=True, - dep_enabled=True, - priority="HIGH_PRIORITY_CLASS", - owner_sid="S-1-5-21-186985262-1144665072-74031268-1309") # noqa - }) + p = stix2.v20.Process(extensions={ + "windows-process-ext": stix2.v20.WindowsProcessExt( + aslr_enabled=True, + dep_enabled=True, + priority="HIGH_PRIORITY_CLASS", + owner_sid="S-1-5-21-186985262-1144665072-74031268-1309", + ), # noqa + }) assert p.extensions["windows-process-ext"].dep_enabled assert p.extensions["windows-process-ext"].owner_sid == "S-1-5-21-186985262-1144665072-74031268-1309" def test_process_example_with_WindowsServiceExt(): - p = stix2.Process(extensions={ - "windows-service-ext": { - "service_name": "sirvizio", - "display_name": "Sirvizio", - "start_type": "SERVICE_AUTO_START", - "service_type": "SERVICE_WIN32_OWN_PROCESS", - "service_status": "SERVICE_RUNNING" - } + p = stix2.v20.Process(extensions={ + "windows-service-ext": { + "service_name": "sirvizio", + "display_name": "Sirvizio", + "start_type": "SERVICE_AUTO_START", + "service_type": "SERVICE_WIN32_OWN_PROCESS", + "service_status": "SERVICE_RUNNING", + }, }) assert p.extensions["windows-service-ext"].service_name == "sirvizio" @@ -1098,20 +1179,20 @@ def test_process_example_with_WindowsServiceExt(): def test_process_example_with_WindowsProcessServiceExt(): - p = stix2.Process(extensions={ + p = stix2.v20.Process(extensions={ "windows-service-ext": { "service_name": "sirvizio", "display_name": "Sirvizio", "start_type": "SERVICE_AUTO_START", "service_type": "SERVICE_WIN32_OWN_PROCESS", - "service_status": "SERVICE_RUNNING" + "service_status": "SERVICE_RUNNING", }, "windows-process-ext": { "aslr_enabled": True, "dep_enabled": True, "priority": "HIGH_PRIORITY_CLASS", - "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309" - } + "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309", + }, }) assert p.extensions["windows-service-ext"].service_name == "sirvizio" @@ -1121,10 +1202,12 @@ def test_process_example_with_WindowsProcessServiceExt(): def test_software_example(): - s = stix2.Software(name="Word", - cpe="cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*", - version="2002", - vendor="Microsoft") + s = stix2.v20.Software( + name="Word", + cpe="cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*", + version="2002", + vendor="Microsoft", + ) assert s.name == "Word" assert s.cpe == "cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*" @@ -1133,24 +1216,26 @@ def test_software_example(): def test_url_example(): - s = stix2.URL(value="https://example.com/research/index.html") + s = stix2.v20.URL(value="https://example.com/research/index.html") assert s.type == "url" assert s.value == "https://example.com/research/index.html" def test_user_account_example(): - a = stix2.UserAccount(user_id="1001", - account_login="jdoe", - account_type="unix", - display_name="John Doe", - is_service_account=False, - is_privileged=False, - can_escalate_privs=True, - account_created="2016-01-20T12:31:12Z", - password_last_changed="2016-01-20T14:27:43Z", - account_first_login="2016-01-20T14:26:07Z", - account_last_login="2016-07-22T16:08:28Z") + a = stix2.v20.UserAccount( + user_id="1001", + account_login="jdoe", + account_type="unix", + display_name="John Doe", + is_service_account=False, + is_privileged=False, + can_escalate_privs=True, + account_created="2016-01-20T12:31:12Z", + password_last_changed="2016-01-20T14:27:43Z", + account_first_login="2016-01-20T14:26:07Z", + account_last_login="2016-07-22T16:08:28Z", + ) assert a.user_id == "1001" assert a.account_login == "jdoe" @@ -1166,14 +1251,18 @@ def test_user_account_example(): def test_user_account_unix_account_ext_example(): - u = stix2.UNIXAccountExt(gid=1001, - groups=["wheel"], - home_dir="/home/jdoe", - shell="/bin/bash") - a = stix2.UserAccount(user_id="1001", - account_login="jdoe", - account_type="unix", - extensions={'unix-account-ext': u}) + u = stix2.v20.UNIXAccountExt( + gid=1001, + groups=["wheel"], + home_dir="/home/jdoe", + shell="/bin/bash", + ) + a = stix2.v20.UserAccount( + user_id="1001", + account_login="jdoe", + account_type="unix", + extensions={'unix-account-ext': u}, + ) assert a.extensions['unix-account-ext'].gid == 1001 assert a.extensions['unix-account-ext'].groups == ["wheel"] assert a.extensions['unix-account-ext'].home_dir == "/home/jdoe" @@ -1182,15 +1271,21 @@ def test_user_account_unix_account_ext_example(): def test_windows_registry_key_example(): with pytest.raises(ValueError): - v = stix2.WindowsRegistryValueType(name="Foo", - data="qwerty", - data_type="string") + stix2.v20.WindowsRegistryValueType( + name="Foo", + data="qwerty", + data_type="string", + ) - v = stix2.WindowsRegistryValueType(name="Foo", - data="qwerty", - data_type="REG_SZ") - w = stix2.WindowsRegistryKey(key="hkey_local_machine\\system\\bar\\foo", - values=[v]) + v = stix2.v20.WindowsRegistryValueType( + name="Foo", + data="qwerty", + data_type="REG_SZ", + ) + w = stix2.v20.WindowsRegistryKey( + key="hkey_local_machine\\system\\bar\\foo", + values=[v], + ) assert w.key == "hkey_local_machine\\system\\bar\\foo" assert w.values[0].name == "Foo" assert w.values[0].data == "qwerty" @@ -1198,11 +1293,12 @@ def test_windows_registry_key_example(): def test_x509_certificate_example(): - x509 = stix2.X509Certificate( + x509 = stix2.v20.X509Certificate( issuer="C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com", # noqa validity_not_before="2016-03-12T12:00:00Z", validity_not_after="2016-08-21T12:00:00Z", - subject="C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org") # noqa + subject="C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org", + ) # noqa assert x509.type == "x509-certificate" assert x509.issuer == "C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com" # noqa @@ -1210,21 +1306,21 @@ def test_x509_certificate_example(): def test_new_version_with_related_objects(): - data = stix2.ObservedData( + data = stix2.v20.ObservedData( first_observed="2016-03-12T12:00:00Z", last_observed="2016-03-12T12:00:00Z", number_observed=1, objects={ 'src_ip': { 'type': 'ipv4-addr', - 'value': '127.0.0.1/32' + 'value': '127.0.0.1/32', }, 'domain': { 'type': 'domain-name', 'value': 'example.com', - 'resolves_to_refs': ['src_ip'] - } - } + 'resolves_to_refs': ['src_ip'], + }, + }, ) new_version = data.new_version(last_observed="2017-12-12T12:00:00Z") assert new_version.last_observed.year == 2017 diff --git a/stix2/test/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py similarity index 99% rename from stix2/test/test_pattern_expressions.py rename to stix2/test/v20/test_pattern_expressions.py index ac98489..3dc7cde 100644 --- a/stix2/test/test_pattern_expressions.py +++ b/stix2/test/v20/test_pattern_expressions.py @@ -513,9 +513,11 @@ def test_parsing_comparison_expression(): def test_parsing_qualified_expression(): patt_obj = create_pattern_object( - "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS") + "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS", + ) assert str( - patt_obj) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS" + patt_obj, + ) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS" def test_list_constant(): diff --git a/stix2/test/test_pickle.py b/stix2/test/v20/test_pickle.py similarity index 80% rename from stix2/test/test_pickle.py rename to stix2/test/v20/test_pickle.py index 9e2cc9a..6c65d8f 100644 --- a/stix2/test/test_pickle.py +++ b/stix2/test/v20/test_pickle.py @@ -7,11 +7,11 @@ def test_pickling(): """ Ensure a pickle/unpickle cycle works okay. """ - identity = stix2.Identity( + identity = stix2.v20.Identity( id="identity--d66cb89d-5228-4983-958c-fa84ef75c88c", name="alice", description="this is a pickle test", - identity_class="some_class" + identity_class="some_class", ) pickle.loads(pickle.dumps(identity)) diff --git a/stix2/test/test_properties.py b/stix2/test/v20/test_properties.py similarity index 53% rename from stix2/test/test_properties.py rename to stix2/test/v20/test_properties.py index 19419bb..24c1c99 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/v20/test_properties.py @@ -2,15 +2,16 @@ import uuid import pytest -from stix2 import CustomObject, EmailMIMEComponent, ExtensionsProperty, TCPExt +import stix2 from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError -from stix2.properties import (ERROR_INVALID_ID, BinaryProperty, - BooleanProperty, DictionaryProperty, - EmbeddedObjectProperty, EnumProperty, - FloatProperty, HashesProperty, HexProperty, - IDProperty, IntegerProperty, ListProperty, - Property, ReferenceProperty, StringProperty, - TimestampProperty, TypeProperty) +from stix2.properties import ( + ERROR_INVALID_ID, BinaryProperty, BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, + HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty, + Property, ReferenceProperty, STIXObjectProperty, StringProperty, + TimestampProperty, TypeProperty, +) +from stix2.v20.common import MarkingProperty from . import constants @@ -92,10 +93,12 @@ ID_PROP = IDProperty('my-type') MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7' -@pytest.mark.parametrize("value", [ - MY_ID, - 'my-type--00000000-0000-4000-8000-000000000000', -]) +@pytest.mark.parametrize( + "value", [ + MY_ID, + 'my-type--00000000-0000-4000-8000-000000000000', + ], +) def test_id_property_valid(value): assert ID_PROP.clean(value) == value @@ -133,14 +136,16 @@ def test_id_property_wrong_type(): assert str(excinfo.value) == "must start with 'my-type--'." -@pytest.mark.parametrize("value", [ - 'my-type--foo', - # Not a v4 UUID - 'my-type--00000000-0000-0000-0000-000000000000', - 'my-type--' + str(uuid.uuid1()), - 'my-type--' + str(uuid.uuid3(uuid.NAMESPACE_DNS, "example.org")), - 'my-type--' + str(uuid.uuid5(uuid.NAMESPACE_DNS, "example.org")), -]) +@pytest.mark.parametrize( + "value", [ + 'my-type--foo', + # Not a v4 UUID + 'my-type--00000000-0000-0000-0000-000000000000', + 'my-type--' + str(uuid.uuid1()), + 'my-type--' + str(uuid.uuid3(uuid.NAMESPACE_DNS, "example.org")), + 'my-type--' + str(uuid.uuid5(uuid.NAMESPACE_DNS, "example.org")), + ], +) def test_id_property_not_a_valid_hex_uuid(value): with pytest.raises(ValueError) as excinfo: ID_PROP.clean(value) @@ -152,77 +157,117 @@ def test_id_property_default(): assert ID_PROP.clean(default) == default -@pytest.mark.parametrize("value", [ - 2, - -1, - 3.14, - False, -]) +@pytest.mark.parametrize( + "value", [ + 2, + -1, + 3.14, + False, + ], +) def test_integer_property_valid(value): int_prop = IntegerProperty() assert int_prop.clean(value) is not None -@pytest.mark.parametrize("value", [ - "something", - StringProperty(), -]) +@pytest.mark.parametrize( + "value", [ + -1, + -100, + -5 * 6, + ], +) +def test_integer_property_invalid_min_with_constraints(value): + int_prop = IntegerProperty(min=0, max=180) + with pytest.raises(ValueError) as excinfo: + int_prop.clean(value) + assert "minimum value is" in str(excinfo.value) + + +@pytest.mark.parametrize( + "value", [ + 181, + 200, + 50 * 6, + ], +) +def test_integer_property_invalid_max_with_constraints(value): + int_prop = IntegerProperty(min=0, max=180) + with pytest.raises(ValueError) as excinfo: + int_prop.clean(value) + assert "maximum value is" in str(excinfo.value) + + +@pytest.mark.parametrize( + "value", [ + "something", + StringProperty(), + ], +) def test_integer_property_invalid(value): int_prop = IntegerProperty() with pytest.raises(ValueError): int_prop.clean(value) -@pytest.mark.parametrize("value", [ - 2, - -1, - 3.14, - False, -]) +@pytest.mark.parametrize( + "value", [ + 2, + -1, + 3.14, + False, + ], +) def test_float_property_valid(value): int_prop = FloatProperty() assert int_prop.clean(value) is not None -@pytest.mark.parametrize("value", [ - "something", - StringProperty(), -]) +@pytest.mark.parametrize( + "value", [ + "something", + StringProperty(), + ], +) def test_float_property_invalid(value): int_prop = FloatProperty() with pytest.raises(ValueError): int_prop.clean(value) -@pytest.mark.parametrize("value", [ - True, - False, - 'True', - 'False', - 'true', - 'false', - 'TRUE', - 'FALSE', - 'T', - 'F', - 't', - 'f', - 1, - 0, -]) +@pytest.mark.parametrize( + "value", [ + True, + False, + 'True', + 'False', + 'true', + 'false', + 'TRUE', + 'FALSE', + 'T', + 'F', + 't', + 'f', + 1, + 0, + ], +) def test_boolean_property_valid(value): bool_prop = BooleanProperty() assert bool_prop.clean(value) is not None -@pytest.mark.parametrize("value", [ - 'abc', - ['false'], - {'true': 'true'}, - 2, - -1, -]) +@pytest.mark.parametrize( + "value", [ + 'abc', + ['false'], + {'true': 'true'}, + 2, + -1, + ], +) def test_boolean_property_invalid(value): bool_prop = BooleanProperty() with pytest.raises(ValueError): @@ -241,11 +286,13 @@ def test_reference_property(): ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000") -@pytest.mark.parametrize("value", [ - '2017-01-01T12:34:56Z', - '2017-01-01 12:34:56', - 'Jan 1 2017 12:34:56', -]) +@pytest.mark.parametrize( + "value", [ + '2017-01-01T12:34:56Z', + '2017-01-01 12:34:56', + 'Jan 1 2017 12:34:56', + ], +) def test_timestamp_property_valid(value): ts_prop = TimestampProperty() assert ts_prop.clean(value) == constants.FAKE_TIME @@ -275,25 +322,33 @@ def test_hex_property(): hex_prop.clean("foobar") -@pytest.mark.parametrize("d", [ - {'description': 'something'}, - [('abc', 1), ('bcd', 2), ('cde', 3)], -]) +@pytest.mark.parametrize( + "d", [ + {'description': 'something'}, + [('abc', 1), ('bcd', 2), ('cde', 3)], + ], +) def test_dictionary_property_valid(d): dict_prop = DictionaryProperty() assert dict_prop.clean(d) -@pytest.mark.parametrize("d", [ - [{'a': 'something'}, "Invalid dictionary key a: (shorter than 3 characters)."], - [{'a'*300: 'something'}, "Invalid dictionary key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "aaaaaaaaaaaaaaaaaaaaaaa: (longer than 256 characters)."], - [{'Hey!': 'something'}, "Invalid dictionary key Hey!: (contains characters other thanlowercase a-z, " - "uppercase A-Z, numerals 0-9, hyphen (-), or underscore (_))."], -]) +@pytest.mark.parametrize( + "d", [ + [{'a': 'something'}, "Invalid dictionary key a: (shorter than 3 characters)."], + [ + {'a'*300: 'something'}, "Invalid dictionary key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaa: (longer than 256 characters).", + ], + [ + {'Hey!': 'something'}, "Invalid dictionary key Hey!: (contains characters other than lowercase a-z, " + "uppercase A-Z, numerals 0-9, hyphen (-), or underscore (_)).", + ], + ], +) def test_dictionary_property_invalid_key(d): dict_prop = DictionaryProperty() @@ -303,18 +358,20 @@ def test_dictionary_property_invalid_key(d): assert str(excinfo.value) == d[1] -@pytest.mark.parametrize("d", [ - ({}, "The dictionary property must contain a non-empty dictionary"), - # TODO: This error message could be made more helpful. The error is caused - # because `json.loads()` doesn't like the *single* quotes around the key - # name, even though they are valid in a Python dictionary. While technically - # accurate (a string is not a dictionary), if we want to be able to load - # string-encoded "dictionaries" that are, we need a better error message - # or an alternative to `json.loads()` ... and preferably *not* `eval()`. :-) - # Changing the following to `'{"description": "something"}'` does not cause - # any ValueError to be raised. - ("{'description': 'something'}", "The dictionary property must contain a dictionary"), -]) +@pytest.mark.parametrize( + "d", [ + ({}, "The dictionary property must contain a non-empty dictionary"), + # TODO: This error message could be made more helpful. The error is caused + # because `json.loads()` doesn't like the *single* quotes around the key + # name, even though they are valid in a Python dictionary. While technically + # accurate (a string is not a dictionary), if we want to be able to load + # string-encoded "dictionaries" that are, we need a better error message + # or an alternative to `json.loads()` ... and preferably *not* `eval()`. :-) + # Changing the following to `'{"description": "something"}'` does not cause + # any ValueError to be raised. + ("{'description': 'something'}", "The dictionary property must contain a dictionary"), + ], +) def test_dictionary_property_invalid(d): dict_prop = DictionaryProperty() @@ -324,9 +381,11 @@ def test_dictionary_property_invalid(d): def test_property_list_of_dictionary(): - @CustomObject('x-new-obj', [ - ('property1', ListProperty(DictionaryProperty(), required=True)), - ]) + @stix2.v20.CustomObject( + 'x-new-obj', [ + ('property1', ListProperty(DictionaryProperty(), required=True)), + ], + ) class NewObj(): pass @@ -334,19 +393,23 @@ def test_property_list_of_dictionary(): assert test_obj.property1[0]['foo'] == 'bar' -@pytest.mark.parametrize("value", [ - {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, - [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], -]) +@pytest.mark.parametrize( + "value", [ + {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, + [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], + ], +) def test_hashes_property_valid(value): hash_prop = HashesProperty() assert hash_prop.clean(value) -@pytest.mark.parametrize("value", [ - {"MD5": "a"}, - {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, -]) +@pytest.mark.parametrize( + "value", [ + {"MD5": "a"}, + {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, + ], +) def test_hashes_property_invalid(value): hash_prop = HashesProperty() @@ -355,11 +418,11 @@ def test_hashes_property_invalid(value): def test_embedded_property(): - emb_prop = EmbeddedObjectProperty(type=EmailMIMEComponent) - mime = EmailMIMEComponent( + emb_prop = EmbeddedObjectProperty(type=stix2.v20.EmailMIMEComponent) + mime = stix2.v20.EmailMIMEComponent( content_type="text/plain; charset=utf-8", content_disposition="inline", - body="Cats are funny!" + body="Cats are funny!", ) assert emb_prop.clean(mime) @@ -367,11 +430,13 @@ def test_embedded_property(): emb_prop.clean("string") -@pytest.mark.parametrize("value", [ - ['a', 'b', 'c'], - ('a', 'b', 'c'), - 'b', -]) +@pytest.mark.parametrize( + "value", [ + ['a', 'b', 'c'], + ('a', 'b', 'c'), + 'b', + ], +) def test_enum_property_valid(value): enum_prop = EnumProperty(value) assert enum_prop.clean('b') @@ -387,17 +452,19 @@ def test_extension_property_valid(): ext_prop = ExtensionsProperty(enclosing_type='file') assert ext_prop({ 'windows-pebinary-ext': { - 'pe_type': 'exe' + 'pe_type': 'exe', }, }) -@pytest.mark.parametrize("data", [ - 1, - {'foobar-ext': { - 'pe_type': 'exe' - }}, -]) +@pytest.mark.parametrize( + "data", [ + 1, + {'foobar-ext': { + 'pe_type': 'exe', + }}, + ], +) def test_extension_property_invalid(data): ext_prop = ExtensionsProperty(enclosing_type='file') with pytest.raises(ValueError): @@ -407,14 +474,36 @@ def test_extension_property_invalid(data): def test_extension_property_invalid_type(): ext_prop = ExtensionsProperty(enclosing_type='indicator') with pytest.raises(ValueError) as excinfo: - ext_prop.clean({ - 'windows-pebinary-ext': { - 'pe_type': 'exe' - }} + ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, ) assert "Can't parse unknown extension" in str(excinfo.value) def test_extension_at_least_one_property_constraint(): with pytest.raises(AtLeastOnePropertyError): - TCPExt() + stix2.v20.TCPExt() + + +def test_marking_property_error(): + mark_prop = MarkingProperty() + + with pytest.raises(ValueError) as excinfo: + mark_prop.clean('my-marking') + + assert str(excinfo.value) == "must be a Statement, TLP Marking or a registered marking." + + +def test_stix_property_not_compliant_spec(): + # This is a 2.0 test only... + indicator = stix2.v20.Indicator(spec_version="2.0", allow_custom=True, **constants.INDICATOR_KWARGS) + stix_prop = STIXObjectProperty(spec_version="2.0") + + with pytest.raises(ValueError) as excinfo: + stix_prop.clean(indicator) + + assert "Spec version 2.0 bundles don't yet support containing objects of a different spec version." in str(excinfo.value) diff --git a/stix2/test/test_relationship.py b/stix2/test/v20/test_relationship.py similarity index 76% rename from stix2/test/test_relationship.py rename to stix2/test/v20/test_relationship.py index 162c7d4..4dc1de8 100644 --- a/stix2/test/test_relationship.py +++ b/stix2/test/v20/test_relationship.py @@ -5,8 +5,9 @@ import pytz import stix2 -from .constants import (FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID, - RELATIONSHIP_KWARGS) +from .constants import ( + FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID, RELATIONSHIP_KWARGS, +) EXPECTED_RELATIONSHIP = """{ "type": "relationship", @@ -22,7 +23,7 @@ EXPECTED_RELATIONSHIP = """{ def test_relationship_all_required_properties(): now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) - rel = stix2.Relationship( + rel = stix2.v20.Relationship( type='relationship', id=RELATIONSHIP_ID, created=now, @@ -54,9 +55,9 @@ def test_relationship_autogenerated_properties(relationship): def test_relationship_type_must_be_relationship(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Relationship(type='xxx', **RELATIONSHIP_KWARGS) + stix2.v20.Relationship(type='xxx', **RELATIONSHIP_KWARGS) - assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.cls == stix2.v20.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'." @@ -64,9 +65,9 @@ def test_relationship_type_must_be_relationship(): def test_relationship_id_must_start_with_relationship(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Relationship(id='my-prefix--', **RELATIONSHIP_KWARGS) + stix2.v20.Relationship(id='my-prefix--', **RELATIONSHIP_KWARGS) - assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.cls == stix2.v20.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--'." @@ -74,27 +75,27 @@ def test_relationship_id_must_start_with_relationship(): def test_relationship_required_property_relationship_type(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.Relationship() - assert excinfo.value.cls == stix2.Relationship + stix2.v20.Relationship() + assert excinfo.value.cls == stix2.v20.Relationship assert excinfo.value.properties == ["relationship_type", "source_ref", "target_ref"] def test_relationship_missing_some_required_properties(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.Relationship(relationship_type='indicates') + stix2.v20.Relationship(relationship_type='indicates') - assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.cls == stix2.v20.Relationship assert excinfo.value.properties == ["source_ref", "target_ref"] def test_relationship_required_properties_target_ref(): with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: - stix2.Relationship( + stix2.v20.Relationship( relationship_type='indicates', - source_ref=INDICATOR_ID + source_ref=INDICATOR_ID, ) - assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.cls == stix2.v20.Relationship assert excinfo.value.properties == ["target_ref"] @@ -107,15 +108,15 @@ def test_cannot_assign_to_relationship_attributes(relationship): def test_invalid_kwarg_to_relationship(): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: - stix2.Relationship(my_custom_property="foo", **RELATIONSHIP_KWARGS) + stix2.v20.Relationship(my_custom_property="foo", **RELATIONSHIP_KWARGS) - assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.cls == stix2.v20.Relationship assert excinfo.value.properties == ['my_custom_property'] assert str(excinfo.value) == "Unexpected properties for Relationship: (my_custom_property)." def test_create_relationship_from_objects_rather_than_ids(indicator, malware): - rel = stix2.Relationship( + rel = stix2.v20.Relationship( relationship_type="indicates", source_ref=indicator, target_ref=malware, @@ -128,7 +129,7 @@ def test_create_relationship_from_objects_rather_than_ids(indicator, malware): def test_create_relationship_with_positional_args(indicator, malware): - rel = stix2.Relationship(indicator, 'indicates', malware) + rel = stix2.v20.Relationship(indicator, 'indicates', malware) assert rel.relationship_type == 'indicates' assert rel.source_ref == 'indicator--00000000-0000-4000-8000-000000000001' @@ -136,20 +137,22 @@ def test_create_relationship_with_positional_args(indicator, malware): assert rel.id == 'relationship--00000000-0000-4000-8000-000000000005' -@pytest.mark.parametrize("data", [ - EXPECTED_RELATIONSHIP, - { - "created": "2016-04-06T20:06:37Z", - "id": "relationship--df7c87eb-75d2-4948-af81-9d49d246f301", - "modified": "2016-04-06T20:06:37Z", - "relationship_type": "indicates", - "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", - "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", - "type": "relationship" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED_RELATIONSHIP, + { + "created": "2016-04-06T20:06:37Z", + "id": "relationship--df7c87eb-75d2-4948-af81-9d49d246f301", + "modified": "2016-04-06T20:06:37Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + "type": "relationship", + }, + ], +) def test_parse_relationship(data): - rel = stix2.parse(data) + rel = stix2.parse(data, version="2.0") assert rel.type == 'relationship' assert rel.id == RELATIONSHIP_ID diff --git a/stix2/test/test_report.py b/stix2/test/v20/test_report.py similarity index 72% rename from stix2/test/test_report.py rename to stix2/test/v20/test_report.py index 9999877..072fc95 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/v20/test_report.py @@ -28,7 +28,7 @@ EXPECTED = """{ def test_report_example(): - report = stix2.Report( + report = stix2.v20.Report( id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", created="2015-12-21T19:59:11.000Z", @@ -40,7 +40,7 @@ def test_report_example(): object_refs=[ "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", - "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", ], ) @@ -48,7 +48,7 @@ def test_report_example(): def test_report_example_objects_in_object_refs(): - report = stix2.Report( + report = stix2.v20.Report( id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", created="2015-12-21T19:59:11.000Z", @@ -58,9 +58,9 @@ def test_report_example_objects_in_object_refs(): published="2016-01-20T17:00:00Z", labels=["campaign"], object_refs=[ - stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), + stix2.v20.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", - "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", ], ) @@ -69,7 +69,7 @@ def test_report_example_objects_in_object_refs(): def test_report_example_objects_in_object_refs_with_bad_id(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Report( + stix2.v20.Report( id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", created="2015-12-21T19:59:11.000Z", @@ -79,50 +79,54 @@ def test_report_example_objects_in_object_refs_with_bad_id(): published="2016-01-20T17:00:00Z", labels=["campaign"], object_refs=[ - stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), + stix2.v20.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" + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", ], ) - assert excinfo.value.cls == stix2.Report + assert excinfo.value.cls == stix2.v20.Report assert excinfo.value.prop_name == "object_refs" assert excinfo.value.reason == stix2.properties.ERROR_INVALID_ID assert str(excinfo.value) == "Invalid value for Report 'object_refs': " + stix2.properties.ERROR_INVALID_ID -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "created": "2015-12-21T19:59:11.000Z", - "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", - "description": "A simple report with an indicator and campaign", - "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", - "labels": [ - "campaign" - ], - "modified": "2015-12-21T19:59:11.000Z", - "name": "The Black Vine Cyberespionage Group", - "object_refs": [ - "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", - "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", - "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" - ], - "published": "2016-01-20T17:00:00Z", - "type": "report" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2015-12-21T19:59:11.000Z", + "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", + "description": "A simple report with an indicator and campaign", + "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", + "labels": [ + "campaign", + ], + "modified": "2015-12-21T19:59:11.000Z", + "name": "The Black Vine Cyberespionage Group", + "object_refs": [ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", + ], + "published": "2016-01-20T17:00:00Z", + "type": "report", + }, + ], +) def test_parse_report(data): - rept = stix2.parse(data) + rept = stix2.parse(data, version="2.0") assert rept.type == 'report' assert rept.id == REPORT_ID assert rept.created == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) assert rept.modified == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) assert rept.created_by_ref == "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283" - assert rept.object_refs == ["indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", - "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", - "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a"] + assert rept.object_refs == [ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", + ] assert rept.description == "A simple report with an indicator and campaign" assert rept.labels == ["campaign"] assert rept.name == "The Black Vine Cyberespionage Group" diff --git a/stix2/test/test_sighting.py b/stix2/test/v20/test_sighting.py similarity index 76% rename from stix2/test/test_sighting.py rename to stix2/test/v20/test_sighting.py index 22e01a3..e93ca7e 100644 --- a/stix2/test/test_sighting.py +++ b/stix2/test/v20/test_sighting.py @@ -33,13 +33,13 @@ BAD_SIGHTING = """{ def test_sighting_all_required_properties(): now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) - s = stix2.Sighting( + s = stix2.v20.Sighting( type='sighting', id=SIGHTING_ID, created=now, modified=now, sighting_of_ref=INDICATOR_ID, - where_sighted_refs=["identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"] + where_sighted_refs=["identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"], ) assert str(s) == EXPECTED_SIGHTING @@ -48,16 +48,16 @@ def test_sighting_bad_where_sighted_refs(): now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Sighting( + stix2.v20.Sighting( type='sighting', id=SIGHTING_ID, created=now, modified=now, sighting_of_ref=INDICATOR_ID, - where_sighted_refs=["malware--8cc7afd6-5455-4d2b-a736-e614ee631d99"] + where_sighted_refs=["malware--8cc7afd6-5455-4d2b-a736-e614ee631d99"], ) - assert excinfo.value.cls == stix2.Sighting + assert excinfo.value.cls == stix2.v20.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'." @@ -65,9 +65,9 @@ def test_sighting_bad_where_sighted_refs(): def test_sighting_type_must_be_sightings(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Sighting(type='xxx', **SIGHTING_KWARGS) + stix2.v20.Sighting(type='xxx', **SIGHTING_KWARGS) - assert excinfo.value.cls == stix2.Sighting + assert excinfo.value.cls == stix2.v20.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'." @@ -75,35 +75,37 @@ def test_sighting_type_must_be_sightings(): def test_invalid_kwarg_to_sighting(): with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: - stix2.Sighting(my_custom_property="foo", **SIGHTING_KWARGS) + stix2.v20.Sighting(my_custom_property="foo", **SIGHTING_KWARGS) - assert excinfo.value.cls == stix2.Sighting + assert excinfo.value.cls == stix2.v20.Sighting assert excinfo.value.properties == ['my_custom_property'] assert str(excinfo.value) == "Unexpected properties for Sighting: (my_custom_property)." def test_create_sighting_from_objects_rather_than_ids(malware): # noqa: F811 - rel = stix2.Sighting(sighting_of_ref=malware) + rel = stix2.v20.Sighting(sighting_of_ref=malware) assert rel.sighting_of_ref == 'malware--00000000-0000-4000-8000-000000000001' assert rel.id == 'sighting--00000000-0000-4000-8000-000000000003' -@pytest.mark.parametrize("data", [ - 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--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", - "type": "sighting", - "where_sighted_refs": [ - "identity--8cc7afd6-5455-4d2b-a736-e614ee631d99" - ] - }, -]) +@pytest.mark.parametrize( + "data", [ + 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--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "type": "sighting", + "where_sighted_refs": [ + "identity--8cc7afd6-5455-4d2b-a736-e614ee631d99", + ], + }, + ], +) def test_parse_sighting(data): - sighting = stix2.parse(data) + sighting = stix2.parse(data, version="2.0") assert sighting.type == 'sighting' assert sighting.id == SIGHTING_ID diff --git a/stix2/test/test_threat_actor.py b/stix2/test/v20/test_threat_actor.py similarity index 69% rename from stix2/test/test_threat_actor.py rename to stix2/test/v20/test_threat_actor.py index 8079a21..f7ef843 100644 --- a/stix2/test/test_threat_actor.py +++ b/stix2/test/v20/test_threat_actor.py @@ -22,7 +22,7 @@ EXPECTED = """{ def test_threat_actor_example(): - threat_actor = stix2.ThreatActor( + threat_actor = stix2.v20.ThreatActor( id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:48.000Z", @@ -35,23 +35,25 @@ def test_threat_actor_example(): assert str(threat_actor) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "created": "2016-04-06T20:03:48.000Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "description": "The Evil Org threat actor group", - "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "labels": [ - "crime-syndicate" - ], - "modified": "2016-04-06T20:03:48.000Z", - "name": "Evil Org", - "type": "threat-actor" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "The Evil Org threat actor group", + "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "labels": [ + "crime-syndicate", + ], + "modified": "2016-04-06T20:03:48.000Z", + "name": "Evil Org", + "type": "threat-actor", + }, + ], +) def test_parse_threat_actor(data): - actor = stix2.parse(data) + actor = stix2.parse(data, version="2.0") assert actor.type == 'threat-actor' assert actor.id == THREAT_ACTOR_ID diff --git a/stix2/test/test_tool.py b/stix2/test/v20/test_tool.py similarity index 77% rename from stix2/test/test_tool.py rename to stix2/test/v20/test_tool.py index 9fc2c22..e0c7082 100644 --- a/stix2/test/test_tool.py +++ b/stix2/test/v20/test_tool.py @@ -34,7 +34,7 @@ EXPECTED_WITH_REVOKED = """{ def test_tool_example(): - tool = stix2.Tool( + tool = stix2.v20.Tool( id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:48.000Z", @@ -46,22 +46,24 @@ def test_tool_example(): assert str(tool) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "created": "2016-04-06T20:03:48Z", - "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "labels": [ - "remote-access" - ], - "modified": "2016-04-06T20:03:48Z", - "name": "VNC", - "type": "tool" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "labels": [ + "remote-access", + ], + "modified": "2016-04-06T20:03:48Z", + "name": "VNC", + "type": "tool", + }, + ], +) def test_parse_tool(data): - tool = stix2.parse(data) + tool = stix2.parse(data, version="2.0") assert tool.type == 'tool' assert tool.id == TOOL_ID @@ -73,13 +75,13 @@ def test_parse_tool(data): def test_tool_no_workbench_wrappers(): - tool = stix2.Tool(name='VNC', labels=['remote-access']) + tool = stix2.v20.Tool(name='VNC', labels=['remote-access']) with pytest.raises(AttributeError): tool.created_by() def test_tool_serialize_with_defaults(): - tool = stix2.Tool( + tool = stix2.v20.Tool( id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created="2016-04-06T20:03:48.000Z", diff --git a/stix2/test/v20/test_utils.py b/stix2/test/v20/test_utils.py new file mode 100644 index 0000000..1aa85b1 --- /dev/null +++ b/stix2/test/v20/test_utils.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +import datetime as dt +from io import StringIO + +import pytest +import pytz + +import stix2.utils + +amsterdam = pytz.timezone('Europe/Amsterdam') +eastern = pytz.timezone('US/Eastern') + + +@pytest.mark.parametrize( + 'dttm, timestamp', [ + (dt.datetime(2017, 1, 1, tzinfo=pytz.utc), '2017-01-01T00:00:00Z'), + (amsterdam.localize(dt.datetime(2017, 1, 1)), '2016-12-31T23:00:00Z'), + (eastern.localize(dt.datetime(2017, 1, 1, 12, 34, 56)), '2017-01-01T17:34:56Z'), + (eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'), + (dt.datetime(2017, 7, 1), '2017-07-01T00:00:00Z'), + (dt.datetime(2017, 7, 1, 0, 0, 0, 1), '2017-07-01T00:00:00.000001Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='millisecond'), '2017-07-01T00:00:00.000Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='second'), '2017-07-01T00:00:00Z'), + ], +) +def test_timestamp_formatting(dttm, timestamp): + assert stix2.utils.format_datetime(dttm) == timestamp + + +@pytest.mark.parametrize( + 'timestamp, dttm', [ + (dt.datetime(2017, 1, 1, 0, tzinfo=pytz.utc), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + (dt.date(2017, 1, 1), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T00:00:00Z', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T02:00:00+2:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T00:00:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ], +) +def test_parse_datetime(timestamp, dttm): + assert stix2.utils.parse_into_datetime(timestamp) == dttm + + +@pytest.mark.parametrize( + 'timestamp, dttm, precision', [ + ('2017-01-01T01:02:03.000001', dt.datetime(2017, 1, 1, 1, 2, 3, 0, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.001', dt.datetime(2017, 1, 1, 1, 2, 3, 1000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.1', dt.datetime(2017, 1, 1, 1, 2, 3, 100000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, 450000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, tzinfo=pytz.utc), 'second'), + ], +) +def test_parse_datetime_precision(timestamp, dttm, precision): + assert stix2.utils.parse_into_datetime(timestamp, precision) == dttm + + +@pytest.mark.parametrize( + 'ts', [ + 'foobar', + 1, + ], +) +def test_parse_datetime_invalid(ts): + with pytest.raises(ValueError): + stix2.utils.parse_into_datetime('foobar') + + +@pytest.mark.parametrize( + 'data', [ + {"a": 1}, + '{"a": 1}', + StringIO(u'{"a": 1}'), + [("a", 1,)], + ], +) +def test_get_dict(data): + assert stix2.utils._get_dict(data) + + +@pytest.mark.parametrize( + 'data', [ + 1, + [1], + ['a', 1], + "foobar", + ], +) +def test_get_dict_invalid(data): + with pytest.raises(ValueError): + stix2.utils._get_dict(data) + + +@pytest.mark.parametrize( + 'stix_id, type', [ + ('malware--d69c8146-ab35-4d50-8382-6fc80e641d43', 'malware'), + ('intrusion-set--899ce53f-13a0-479b-a0e4-67d46e241542', 'intrusion-set'), + ], +) +def test_get_type_from_id(stix_id, type): + assert stix2.utils.get_type_from_id(stix_id) == type + + +def test_deduplicate(stix_objs1): + unique = stix2.utils.deduplicate(stix_objs1) + + # Only 3 objects are unique + # 2 id's vary + # 2 modified times vary for a particular id + + assert len(unique) == 3 + + ids = [obj['id'] for obj in unique] + mods = [obj['modified'] for obj in unique] + + assert "indicator--00000000-0000-4000-8000-000000000001" in ids + assert "indicator--00000000-0000-4000-8000-000000000001" in ids + assert "2017-01-27T13:49:53.935Z" in mods + assert "2017-01-27T13:49:53.936Z" in mods + + +@pytest.mark.parametrize( + 'object, tuple_to_find, expected_index', [ + ( + stix2.v20.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + }, + "1": { + "type": "ipv4-addr", + "value": "198.51.100.3", + }, + "2": { + "type": "network-traffic", + "src_ref": "1", + "protocols": [ + "tcp", + "http", + ], + "extensions": { + "http-request-ext": { + "request_method": "get", + "request_value": "/download.html", + "request_version": "http/1.1", + "request_header": { + "Accept-Encoding": "gzip,deflate", + "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", + "Host": "www.example.com", + }, + }, + }, + }, + }, + ), ('1', {"type": "ipv4-addr", "value": "198.51.100.3"}), 1, + ), + ( + { + "type": "x-example", + "id": "x-example--d5413db2-c26c-42e0-b0e0-ec800a310bfb", + "created": "2018-06-11T01:25:22.063Z", + "modified": "2018-06-11T01:25:22.063Z", + "dictionary": { + "key": { + "key_one": "value", + "key_two": "value", + }, + }, + }, ('key', {'key_one': 'value', 'key_two': 'value'}), 0, + ), + ( + { + "type": "language-content", + "id": "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d", + "created": "2017-02-08T21:31:22.007Z", + "modified": "2017-02-08T21:31:22.007Z", + "object_ref": "campaign--12a111f0-b824-4baf-a224-83b80237a094", + "object_modified": "2017-02-08T21:31:22.007Z", + "contents": { + "de": { + "name": "Bank Angriff 1", + "description": "Weitere Informationen über Banküberfall", + }, + "fr": { + "name": "Attaque Bank 1", + "description": "Plus d'informations sur la crise bancaire", + }, + }, + }, ('fr', {"name": "Attaque Bank 1", "description": "Plus d'informations sur la crise bancaire"}), 1, + ), + ], +) +def test_find_property_index(object, tuple_to_find, expected_index): + assert stix2.utils.find_property_index( + object, + *tuple_to_find + ) == expected_index + + +@pytest.mark.parametrize( + 'dict_value, tuple_to_find, expected_index', [ + ( + { + "contents": { + "de": { + "name": "Bank Angriff 1", + "description": "Weitere Informationen über Banküberfall", + }, + "fr": { + "name": "Attaque Bank 1", + "description": "Plus d'informations sur la crise bancaire", + }, + "es": { + "name": "Ataque al Banco", + "description": "Mas informacion sobre el ataque al banco", + }, + }, + }, ('es', {"name": "Ataque al Banco", "description": "Mas informacion sobre el ataque al banco"}), 1, + ), # Sorted alphabetically + ( + { + 'my_list': [ + {"key_one": 1}, + {"key_two": 2}, + ], + }, ('key_one', 1), 0, + ), + ], +) +def test_iterate_over_values(dict_value, tuple_to_find, expected_index): + assert stix2.utils._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index diff --git a/stix2/test/test_versioning.py b/stix2/test/v20/test_versioning.py similarity index 81% rename from stix2/test/test_versioning.py rename to stix2/test/v20/test_versioning.py index fa3bddb..9974e42 100644 --- a/stix2/test/test_versioning.py +++ b/stix2/test/v20/test_versioning.py @@ -6,7 +6,7 @@ from .constants import CAMPAIGN_MORE_KWARGS def test_making_new_version(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) campaign_v2 = campaign_v1.new_version(name="fred") @@ -20,7 +20,7 @@ def test_making_new_version(): def test_making_new_version_with_unset(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) campaign_v2 = campaign_v1.new_version(description=None) @@ -34,18 +34,18 @@ def test_making_new_version_with_unset(): def test_making_new_version_with_embedded_object(): - campaign_v1 = stix2.Campaign( + campaign_v1 = stix2.v20.Campaign( external_references=[{ "source_name": "capec", - "external_id": "CAPEC-163" + "external_id": "CAPEC-163", }], **CAMPAIGN_MORE_KWARGS ) campaign_v2 = campaign_v1.new_version(external_references=[{ "source_name": "capec", - "external_id": "CAPEC-164" - }]) + "external_id": "CAPEC-164", + }]) assert campaign_v1.id == campaign_v2.id assert campaign_v1.created_by_ref == campaign_v2.created_by_ref @@ -57,7 +57,7 @@ def test_making_new_version_with_embedded_object(): def test_revoke(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) campaign_v2 = campaign_v1.revoke() @@ -72,7 +72,7 @@ def test_revoke(): def test_versioning_error_invalid_property(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as excinfo: campaign_v1.new_version(type="threat-actor") @@ -81,41 +81,43 @@ def test_versioning_error_invalid_property(): def test_versioning_error_bad_modified_value(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: campaign_v1.new_version(modified="2015-04-06T20:03:00.000Z") - assert excinfo.value.cls == stix2.Campaign + assert excinfo.value.cls == stix2.v20.Campaign assert excinfo.value.prop_name == "modified" assert excinfo.value.reason == "The new modified datetime cannot be before than or equal to the current modified datetime." \ "It cannot be equal, as according to STIX 2 specification, objects that are different " \ "but have the same id and modified timestamp do not have defined consumer behavior." msg = "Invalid value for {0} '{1}': {2}" - msg = msg.format(stix2.Campaign.__name__, "modified", - "The new modified datetime cannot be before than or equal to the current modified datetime." - "It cannot be equal, as according to STIX 2 specification, objects that are different " - "but have the same id and modified timestamp do not have defined consumer behavior.") + msg = msg.format( + stix2.v20.Campaign.__name__, "modified", + "The new modified datetime cannot be before than or equal to the current modified datetime." + "It cannot be equal, as according to STIX 2 specification, objects that are different " + "but have the same id and modified timestamp do not have defined consumer behavior.", + ) assert str(excinfo.value) == msg def test_versioning_error_usetting_required_property(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: campaign_v1.new_version(name=None) - assert excinfo.value.cls == stix2.Campaign + assert excinfo.value.cls == stix2.v20.Campaign assert excinfo.value.properties == ["name"] msg = "No values for required properties for {0}: ({1})." - msg = msg.format(stix2.Campaign.__name__, "name") + msg = msg.format(stix2.v20.Campaign.__name__, "name") assert str(excinfo.value) == msg def test_versioning_error_new_version_of_revoked(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) campaign_v2 = campaign_v1.revoke() with pytest.raises(stix2.exceptions.RevokeError) as excinfo: @@ -127,7 +129,7 @@ def test_versioning_error_new_version_of_revoked(): def test_versioning_error_revoke_of_revoked(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) campaign_v2 = campaign_v1.revoke() with pytest.raises(stix2.exceptions.RevokeError) as excinfo: @@ -215,23 +217,27 @@ def test_revoke_invalid_cls(): def test_remove_custom_stix_property(): - mal = stix2.Malware(name="ColePowers", - labels=["rootkit"], - x_custom="armada", - allow_custom=True) + mal = stix2.v20.Malware( + name="ColePowers", + labels=["rootkit"], + x_custom="armada", + allow_custom=True, + ) mal_nc = stix2.utils.remove_custom_stix(mal) assert "x_custom" not in mal_nc - assert stix2.utils.parse_into_datetime(mal["modified"], precision="millisecond") < stix2.utils.parse_into_datetime(mal_nc["modified"], - precision="millisecond") + assert (stix2.utils.parse_into_datetime(mal["modified"], precision="millisecond") < + stix2.utils.parse_into_datetime(mal_nc["modified"], precision="millisecond")) def test_remove_custom_stix_object(): - @stix2.CustomObject("x-animal", [ - ("species", stix2.properties.StringProperty(required=True)), - ("animal_class", stix2.properties.StringProperty()), - ]) + @stix2.v20.CustomObject( + "x-animal", [ + ("species", stix2.properties.StringProperty(required=True)), + ("animal_class", stix2.properties.StringProperty()), + ], + ) class Animal(object): pass @@ -243,7 +249,7 @@ def test_remove_custom_stix_object(): def test_remove_custom_stix_no_custom(): - campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v1 = stix2.v20.Campaign(**CAMPAIGN_MORE_KWARGS) campaign_v2 = stix2.utils.remove_custom_stix(campaign_v1) assert len(campaign_v1.keys()) == len(campaign_v2.keys()) diff --git a/stix2/test/test_vulnerability.py b/stix2/test/v20/test_vulnerability.py similarity index 62% rename from stix2/test/test_vulnerability.py rename to stix2/test/v20/test_vulnerability.py index e7358df..7ce05ef 100644 --- a/stix2/test/test_vulnerability.py +++ b/stix2/test/v20/test_vulnerability.py @@ -23,38 +23,42 @@ EXPECTED = """{ def test_vulnerability_example(): - vulnerability = stix2.Vulnerability( + vulnerability = stix2.v20.Vulnerability( id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", created="2016-05-12T08:17:27.000Z", modified="2016-05-12T08:17:27.000Z", name="CVE-2016-1234", external_references=[ - stix2.ExternalReference(source_name='cve', - external_id="CVE-2016-1234"), + stix2.v20.ExternalReference( + source_name='cve', + external_id="CVE-2016-1234", + ), ], ) assert str(vulnerability) == EXPECTED -@pytest.mark.parametrize("data", [ - EXPECTED, - { - "created": "2016-05-12T08:17:27Z", - "external_references": [ - { - "external_id": "CVE-2016-1234", - "source_name": "cve" - } - ], - "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "modified": "2016-05-12T08:17:27Z", - "name": "CVE-2016-1234", - "type": "vulnerability" - }, -]) +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-05-12T08:17:27Z", + "external_references": [ + { + "external_id": "CVE-2016-1234", + "source_name": "cve", + }, + ], + "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "modified": "2016-05-12T08:17:27Z", + "name": "CVE-2016-1234", + "type": "vulnerability", + }, + ], +) def test_parse_vulnerability(data): - vuln = stix2.parse(data) + vuln = stix2.parse(data, version="2.0") assert vuln.type == 'vulnerability' assert vuln.id == VULNERABILITY_ID diff --git a/stix2/test/test_workbench.py b/stix2/test/v20/test_workbench.py similarity index 77% rename from stix2/test/test_workbench.py rename to stix2/test/v20/test_workbench.py index d436261..c254966 100644 --- a/stix2/test/test_workbench.py +++ b/stix2/test/v20/test_workbench.py @@ -1,29 +1,25 @@ import os import stix2 -from stix2.workbench import (AttackPattern, Bundle, Campaign, CourseOfAction, - ExternalReference, FileSystemSource, Filter, - Identity, Indicator, IntrusionSet, Malware, - MarkingDefinition, ObservedData, Relationship, - Report, StatementMarking, ThreatActor, Tool, - Vulnerability, add_data_source, all_versions, - attack_patterns, campaigns, courses_of_action, - create, get, identities, indicators, - intrusion_sets, malware, observed_data, query, - reports, save, set_default_created, - set_default_creator, set_default_external_refs, - set_default_object_marking_refs, threat_actors, - tools, vulnerabilities) +from stix2.workbench import ( + AttackPattern, Campaign, CourseOfAction, ExternalReference, + FileSystemSource, Filter, Identity, Indicator, IntrusionSet, Malware, + MarkingDefinition, ObservedData, Relationship, Report, StatementMarking, + ThreatActor, Tool, Vulnerability, add_data_source, all_versions, + attack_patterns, campaigns, courses_of_action, create, get, identities, + indicators, intrusion_sets, malware, observed_data, query, reports, save, + set_default_created, set_default_creator, set_default_external_refs, + set_default_object_marking_refs, threat_actors, tools, vulnerabilities, +) -from .constants import (ATTACK_PATTERN_ID, ATTACK_PATTERN_KWARGS, CAMPAIGN_ID, - CAMPAIGN_KWARGS, COURSE_OF_ACTION_ID, - COURSE_OF_ACTION_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, - INDICATOR_ID, INDICATOR_KWARGS, INTRUSION_SET_ID, - INTRUSION_SET_KWARGS, MALWARE_ID, MALWARE_KWARGS, - OBSERVED_DATA_ID, OBSERVED_DATA_KWARGS, REPORT_ID, - REPORT_KWARGS, THREAT_ACTOR_ID, THREAT_ACTOR_KWARGS, - TOOL_ID, TOOL_KWARGS, VULNERABILITY_ID, - VULNERABILITY_KWARGS) +from .constants import ( + ATTACK_PATTERN_ID, ATTACK_PATTERN_KWARGS, CAMPAIGN_ID, CAMPAIGN_KWARGS, + COURSE_OF_ACTION_ID, COURSE_OF_ACTION_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, + INDICATOR_ID, INDICATOR_KWARGS, INTRUSION_SET_ID, INTRUSION_SET_KWARGS, + MALWARE_ID, MALWARE_KWARGS, OBSERVED_DATA_ID, OBSERVED_DATA_KWARGS, + REPORT_ID, REPORT_KWARGS, THREAT_ACTOR_ID, THREAT_ACTOR_KWARGS, TOOL_ID, + TOOL_KWARGS, VULNERABILITY_ID, VULNERABILITY_KWARGS, +) def test_workbench_environment(): @@ -151,7 +147,7 @@ def test_workbench_get_all_vulnerabilities(): def test_workbench_add_to_bundle(): vuln = Vulnerability(**VULNERABILITY_KWARGS) - bundle = Bundle(vuln) + bundle = stix2.v20.Bundle(vuln) assert bundle.objects[0].name == 'Heartbleed' @@ -225,8 +221,10 @@ def test_additional_filter(): def test_additional_filters_list(): - resp = tools([Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5'), - Filter('name', '=', 'Windows Credential Editor')]) + resp = tools([ + Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5'), + Filter('name', '=', 'Windows Credential Editor'), + ]) assert len(resp) == 1 @@ -249,8 +247,10 @@ def test_default_created_timestamp(): def test_default_external_refs(): - ext_ref = ExternalReference(source_name="ACME Threat Intel", - description="Threat report") + ext_ref = ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + ) set_default_external_refs(ext_ref) campaign = Campaign(**CAMPAIGN_KWARGS) @@ -260,8 +260,10 @@ def test_default_external_refs(): def test_default_object_marking_refs(): stmt_marking = StatementMarking("Copyright 2016, Example Corp") - mark_def = MarkingDefinition(definition_type="statement", - definition=stmt_marking) + mark_def = MarkingDefinition( + definition_type="statement", + definition=stmt_marking, + ) set_default_object_marking_refs(mark_def) campaign = Campaign(**CAMPAIGN_KWARGS) @@ -269,12 +271,12 @@ def test_default_object_marking_refs(): def test_workbench_custom_property_object_in_observable_extension(): - ntfs = stix2.NTFSExt( + ntfs = stix2.v20.NTFSExt( allow_custom=True, sid=1, x_foo='bar', ) - artifact = stix2.File( + artifact = stix2.v20.File( name='test', extensions={'ntfs-ext': ntfs}, ) @@ -282,7 +284,7 @@ def test_workbench_custom_property_object_in_observable_extension(): allow_custom=True, first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", - number_observed=0, + number_observed=1, objects={"0": artifact}, ) @@ -291,7 +293,7 @@ def test_workbench_custom_property_object_in_observable_extension(): def test_workbench_custom_property_dict_in_observable_extension(): - artifact = stix2.File( + artifact = stix2.v20.File( allow_custom=True, name='test', extensions={ @@ -299,14 +301,14 @@ def test_workbench_custom_property_dict_in_observable_extension(): 'allow_custom': True, 'sid': 1, 'x_foo': 'bar', - } + }, }, ) observed_data = ObservedData( allow_custom=True, first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", - number_observed=0, + number_observed=1, objects={"0": artifact}, ) diff --git a/stix2/test/v21/__init__.py b/stix2/test/v21/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stix2/test/v21/conftest.py b/stix2/test/v21/conftest.py new file mode 100644 index 0000000..dea29ca --- /dev/null +++ b/stix2/test/v21/conftest.py @@ -0,0 +1,168 @@ +import uuid + +import pytest + +import stix2 + +from .constants import ( + FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS, RELATIONSHIP_KWARGS, +) + + +# Inspired by: http://stackoverflow.com/a/24006251 +@pytest.fixture +def clock(monkeypatch): + + class mydatetime(stix2.utils.STIXdatetime): + @classmethod + def now(cls, tz=None): + return FAKE_TIME + + monkeypatch.setattr(stix2.utils, 'STIXdatetime', mydatetime) + + +@pytest.fixture +def uuid4(monkeypatch): + def wrapper(): + data = [0] + + def wrapped(): + data[0] += 1 + return "00000000-0000-4000-8000-00000000%04x" % data[0] + + return wrapped + monkeypatch.setattr(uuid, "uuid4", wrapper()) + + +@pytest.fixture +def indicator(uuid4, clock): + return stix2.v21.Indicator(**INDICATOR_KWARGS) + + +@pytest.fixture +def malware(uuid4, clock): + return stix2.v21.Malware(**MALWARE_KWARGS) + + +@pytest.fixture +def relationship(uuid4, clock): + return stix2.v21.Relationship(**RELATIONSHIP_KWARGS) + + +@pytest.fixture +def stix_objs1(): + ind1 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + ind2 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + ind3 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.936Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + ind4 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + ind5 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + return [ind1, ind2, ind3, ind4, ind5] + + +@pytest.fixture +def stix_objs2(): + ind6 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-31T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + ind7 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + ind8 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", + } + return [ind6, ind7, ind8] + + +@pytest.fixture +def real_stix_objs2(stix_objs2): + return [stix2.parse(x, version="2.1") for x in stix_objs2] diff --git a/stix2/test/v21/constants.py b/stix2/test/v21/constants.py new file mode 100644 index 0000000..e03c610 --- /dev/null +++ b/stix2/test/v21/constants.py @@ -0,0 +1,137 @@ +import datetime as dt + +import pytz + +FAKE_TIME = dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc) + +ATTACK_PATTERN_ID = "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061" +CAMPAIGN_ID = "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" +COURSE_OF_ACTION_ID = "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" +IDENTITY_ID = "identity--311b2d2d-f010-4473-83ec-1edf84858f4c" +INDICATOR_ID = "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7" +INTRUSION_SET_ID = "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29" +LOCATION_ID = "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64" +MALWARE_ID = "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e" +MARKING_DEFINITION_ID = "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" +NOTE_ID = "note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061" +OBSERVED_DATA_ID = "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf" +OPINION_ID = "opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7" +REPORT_ID = "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3" +RELATIONSHIP_ID = "relationship--df7c87eb-75d2-4948-af81-9d49d246f301" +THREAT_ACTOR_ID = "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" +TOOL_ID = "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" +SIGHTING_ID = "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb" +VULNERABILITY_ID = "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061" + +MARKING_IDS = [ + "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "marking-definition--443eb5c3-a76c-4a0a-8caa-e93998e7bc09", + "marking-definition--57fcd772-9c1d-41b0-8d1f-3d47713415d9", + "marking-definition--462bf1a6-03d2-419c-b74e-eee2238b2de4", + "marking-definition--68520ae2-fefe-43a9-84ee-2c2a934d2c7d", + "marking-definition--2802dfb1-1019-40a8-8848-68d0ec0e417f", +] +RELATIONSHIP_IDS = [ + 'relationship--06520621-5352-4e6a-b976-e8fa3d437ffd', + 'relationship--181c9c09-43e6-45dd-9374-3bec192f05ef', + 'relationship--a0cbb21c-8daf-4a7f-96aa-7155a4ef8f70', +] + +# *_KWARGS contains all required arguments to create an instance of that STIX object +# *_MORE_KWARGS contains all the required arguments, plus some optional ones + +ATTACK_PATTERN_KWARGS = dict( + name="Phishing", +) + +CAMPAIGN_KWARGS = dict( + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", +) + +CAMPAIGN_MORE_KWARGS = dict( + type='campaign', + spec_version='2.1', + id=CAMPAIGN_ID, + 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.", +) + +COURSE_OF_ACTION_KWARGS = dict( + name="Block", +) + +IDENTITY_KWARGS = dict( + name="John Smith", + identity_class="individual", +) + +INDICATOR_KWARGS = dict( + indicator_types=['malicious-activity'], + pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", +) + +INTRUSION_SET_KWARGS = dict( + name="Bobcat Breakin", +) + +MALWARE_KWARGS = dict( + malware_types=['ransomware'], + name="Cryptolocker", +) + +MALWARE_MORE_KWARGS = dict( + type='malware', + id=MALWARE_ID, + created="2016-04-06T20:03:00.000Z", + modified="2016-04-06T20:03:00.000Z", + malware_types=['ransomware'], + name="Cryptolocker", + description="A ransomware related to ...", +) + +OBSERVED_DATA_KWARGS = dict( + first_observed=FAKE_TIME, + last_observed=FAKE_TIME, + number_observed=1, + objects={ + "0": { + "type": "windows-registry-key", + "key": "HKEY_LOCAL_MACHINE\\System\\Foo\\Bar", + }, + }, +) + +REPORT_KWARGS = dict( + report_types=["campaign"], + name="Bad Cybercrime", + published=FAKE_TIME, + object_refs=[INDICATOR_ID], +) + +RELATIONSHIP_KWARGS = dict( + relationship_type="indicates", + source_ref=INDICATOR_ID, + target_ref=MALWARE_ID, +) + +SIGHTING_KWARGS = dict( + sighting_of_ref=INDICATOR_ID, +) + +THREAT_ACTOR_KWARGS = dict( + threat_actor_types=["crime-syndicate"], + name="Evil Org", +) + +TOOL_KWARGS = dict( + tool_types=["remote-access"], + name="VNC", +) + +VULNERABILITY_KWARGS = dict( + name="Heartbleed", +) diff --git a/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22/20170531213019735010.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22/20170531213019735010.json new file mode 100644 index 0000000..ccbe2cc --- /dev/null +++ b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22/20170531213019735010.json @@ -0,0 +1,42 @@ +{ + "id": "bundle--f68640b4-0cdc-42ae-b176-def1754a1ea0", + "objects": [ + { + "created": "2017-05-31T21:30:19.73501Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Credential dumping is the process of obtaining account login and password information from the operating system and software. Credentials can be used to perform Windows Credential Editor, Mimikatz, and gsecdump. These tools are in use by both professional security testers and adversaries.\n\nPlaintext passwords can be obtained using tools such as Mimikatz to extract passwords stored by the Local Security Authority (LSA). If smart cards are used to authenticate to a domain using a personal identification number (PIN), then that PIN is also cached as a result and may be dumped.Mimikatz access the LSA Subsystem Service (LSASS) process by opening the process, locating the LSA secrets key, and decrypting the sections in memory where credential details are stored. Credential dumpers may also use methods for reflective DLL Injection to reduce potential indicators of malicious activity.\n\nNTLM hash dumpers open the Security Accounts Manager (SAM) on the local file system (%SystemRoot%/system32/config/SAM) or create a dump of the Registry SAM key to access stored account password hashes. Some hash dumpers will open the local file system as a device and parse to the SAM table to avoid file access defenses. Others will make an in-memory copy of the SAM table before reading hashes. Detection of compromised Legitimate Credentials in-use by adversaries may help as well. \n\nOn Windows 8.1 and Windows Server 2012 R2, monitor Windows Logs for LSASS.exe creation to verify that LSASS started as a protected process.\n\nMonitor processes and command-line arguments for program execution that may be indicative of credential dumping. Remote access tools may contain built-in features or incorporate existing tools like Mimikatz. PowerShell scripts also exist that contain credential dumping functionality, such as PowerSploit's Invoke-Mimikatz module,[[Citation: Powersploit]] which may require additional logging features to be configured in the operating system to collect necessary information for analysis.\n\nPlatforms: Windows Server 2003, Windows Server 2008, Windows Server 2012, Windows XP, Windows 7, Windows 8, Windows Server 2003 R2, Windows Server 2008 R2, Windows Server 2012 R2, Windows Vista, Windows 8.1\n\nData Sources: API monitoring, Process command-line parameters, Process monitoring, PowerShell logs", + "external_references": [ + { + "external_id": "T1003", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Technique/T1003" + }, + { + "description": "Delpy, B. (2014, September 14). Mimikatz module ~ sekurlsa. Retrieved January 10, 2016.", + "source_name": "Github Mimikatz Module sekurlsa", + "url": "https://github.com/gentilkiwi/mimikatz/wiki/module-~-sekurlsa" + }, + { + "description": "PowerSploit. (n.d.). Retrieved December 4, 2014.", + "source_name": "Powersploit", + "url": "https://github.com/mattifestation/PowerSploit" + } + ], + "id": "attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22", + "kill_chain_phases": [ + { + "kill_chain_name": "mitre-attack", + "phase_name": "credential-access" + } + ], + "modified": "2017-05-31T21:30:19.73501Z", + "name": "Credential Dumping", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "attack-pattern" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b/20170531213026496201.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b/20170531213026496201.json new file mode 100644 index 0000000..c36831e --- /dev/null +++ b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b/20170531213026496201.json @@ -0,0 +1,37 @@ +{ + "id": "bundle--b07d6fd6-7cc5-492d-a1eb-9ba956b329d5", + "objects": [ + { + "created": "2017-05-31T21:30:26.496201Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Rootkits are programs that hide the existence of malware by intercepting and modifying operating system API calls that supply system information. Rootkits or rootkit enabling functionality may reside at the user or kernel level in the operating system or lower, to include a Hypervisor, Master Boot Record, or the Basic Input/Output System.[[Citation: Wikipedia Rootkit]]\n\nAdversaries may use rootkits to hide the presence of programs, files, network connections, services, drivers, and other system components.\n\nDetection: Some rootkit protections may be built into anti-virus or operating system software. There are dedicated rootkit detection tools that look for specific types of rootkit behavior. Monitor for the existence of unrecognized DLLs, devices, services, and changes to the MBR.[[Citation: Wikipedia Rootkit]]\n\nPlatforms: Windows Server 2003, Windows Server 2008, Windows Server 2012, Windows XP, Windows 7, Windows 8, Windows Server 2003 R2, Windows Server 2008 R2, Windows Server 2012 R2, Windows Vista, Windows 8.1\n\nData Sources: BIOS, MBR, System calls", + "external_references": [ + { + "external_id": "T1014", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Technique/T1014" + }, + { + "description": "Wikipedia. (2016, June 1). Rootkit. Retrieved June 2, 2016.", + "source_name": "Wikipedia Rootkit", + "url": "https://en.wikipedia.org/wiki/Rootkit" + } + ], + "id": "attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b", + "kill_chain_phases": [ + { + "kill_chain_name": "mitre-attack", + "phase_name": "defense-evasion" + } + ], + "modified": "2017-05-31T21:30:26.496201Z", + "name": "Rootkit", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "attack-pattern" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9/20170531213029458940.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9/20170531213029458940.json new file mode 100644 index 0000000..0504875 --- /dev/null +++ b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9/20170531213029458940.json @@ -0,0 +1,32 @@ +{ + "id": "bundle--1a854c96-639e-4771-befb-e7b960a65974", + "objects": [ + { + "created": "2017-05-31T21:30:29.45894Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Data, such as sensitive documents, may be exfiltrated through the use of automated processing or Scripting after being gathered during Exfiltration Over Command and Control Channel and Exfiltration Over Alternative Protocol.\n\nDetection: Monitor process file access patterns and network behavior. Unrecognized processes or scripts that appear to be traversing file systems and sending network traffic may be suspicious.\n\nPlatforms: Windows Server 2003, Windows Server 2008, Windows Server 2012, Windows XP, Windows 7, Windows 8, Windows Server 2003 R2, Windows Server 2008 R2, Windows Server 2012 R2, Windows Vista, Windows 8.1\n\nData Sources: File monitoring, Process monitoring, Process use of network", + "external_references": [ + { + "external_id": "T1020", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Technique/T1020" + } + ], + "id": "attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9", + "kill_chain_phases": [ + { + "kill_chain_name": "mitre-attack", + "phase_name": "exfiltration" + } + ], + "modified": "2017-05-31T21:30:29.45894Z", + "name": "Automated Exfiltration", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "attack-pattern" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475/20170531213045139269.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475/20170531213045139269.json new file mode 100644 index 0000000..2e3b622 --- /dev/null +++ b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475/20170531213045139269.json @@ -0,0 +1,32 @@ +{ + "id": "bundle--33e3e33a-38b8-4a37-9455-5b8c82d3b10a", + "objects": [ + { + "created": "2017-05-31T21:30:45.139269Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Adversaries may attempt to get a listing of network connections to or from the compromised system.\nUtilities and commands that acquire this information include netstat, \"net use,\" and \"net session\" with Net.\n\nDetection: System and network discovery techniques normally occur throughout an operation as an adversary learns the environment. Data and events should not be viewed in isolation, but as part of a chain of behavior that could lead to other activities, such as Windows Management Instrumentation and PowerShell.\n\nPlatforms: Windows Server 2003, Windows Server 2008, Windows Server 2012, Windows XP, Windows 7, Windows 8, Windows Server 2003 R2, Windows Server 2008 R2, Windows Server 2012 R2, Windows Vista, Windows 8.1\n\nData Sources: Process command-line parameters, Process monitoring", + "external_references": [ + { + "external_id": "T1049", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Technique/T1049" + } + ], + "id": "attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475", + "kill_chain_phases": [ + { + "kill_chain_name": "mitre-attack", + "phase_name": "discovery" + } + ], + "modified": "2017-05-31T21:30:45.139269Z", + "name": "Local Network Connections Discovery", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "attack-pattern" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c/20170531213041022897.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c/20170531213041022897.json new file mode 100644 index 0000000..8819fcb --- /dev/null +++ b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c/20170531213041022897.json @@ -0,0 +1,32 @@ +{ + "id": "bundle--a87938c5-cc1e-4e06-a8a3-b10243ae397d", + "objects": [ + { + "created": "2017-05-31T21:30:41.022897Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Sensitive data can be collected from remote systems via shared network drives (host shared directory, network file server, etc.) that are accessible from the current system prior to cmd may be used to gather information.\n\nDetection: Monitor processes and command-line arguments for actions that could be taken to collect files from a network share. Remote access tools with built-in features may interact directly with the Windows API to gather data. Data may also be acquired through Windows system management tools such as Windows Management Instrumentation and PowerShell.\n\nPlatforms: Windows Server 2003, Windows Server 2008, Windows Server 2012, Windows XP, Windows 7, Windows 8, Windows Server 2003 R2, Windows Server 2008 R2, Windows Server 2012 R2, Windows Vista, Windows 8.1\n\nData Sources: File monitoring, Process monitoring, Process command-line parameters", + "external_references": [ + { + "external_id": "T1039", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Technique/T1039" + } + ], + "id": "attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c", + "kill_chain_phases": [ + { + "kill_chain_name": "mitre-attack", + "phase_name": "collection" + } + ], + "modified": "2017-05-31T21:30:41.022897Z", + "name": "Data from Network Shared Drive", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "attack-pattern" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a/20170531213032662702.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a/20170531213032662702.json new file mode 100644 index 0000000..7d2b58e --- /dev/null +++ b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a/20170531213032662702.json @@ -0,0 +1,32 @@ +{ + "id": "bundle--5ddaeff9-eca7-4094-9e65-4f53da21a444", + "objects": [ + { + "created": "2017-05-31T21:30:32.662702Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Adversaries may attempt to make an executable or file difficult to discover or analyze by encrypting, encoding, or otherwise obfuscating its contents on the system.\n\nDetection: Detection of file obfuscation is difficult unless artifacts are left behind by the obfuscation process that are uniquely detectable with a signature. If detection of the obfuscation itself is not possible, it may be possible to detect the malicious activity that caused the obfuscated file (for example, the method that was used to write, read, or modify the file on the file system).\n\nPlatforms: Windows Server 2003, Windows Server 2008, Windows Server 2012, Windows XP, Windows 7, Windows 8, Windows Server 2003 R2, Windows Server 2008 R2, Windows Server 2012 R2, Windows Vista, Windows 8.1\n\nData Sources: Network protocol analysis, Process use of network, Binary file metadata, File monitoring, Malware reverse engineering", + "external_references": [ + { + "external_id": "T1027", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Technique/T1027" + } + ], + "id": "attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a", + "kill_chain_phases": [ + { + "kill_chain_name": "mitre-attack", + "phase_name": "defense-evasion" + } + ], + "modified": "2017-05-31T21:30:32.662702Z", + "name": "Obfuscated Files or Information", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "attack-pattern" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f/20170531213026495974.json b/stix2/test/v21/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f/20170531213026495974.json new file mode 100644 index 0000000..3117103 --- /dev/null +++ b/stix2/test/v21/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f/20170531213026495974.json @@ -0,0 +1,16 @@ +{ + "id": "bundle--a42d26fe-c938-4074-a1b3-50d852e6f0bd", + "objects": [ + { + "created": "2017-05-31T21:30:26.495974Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Identify potentially malicious software that may contain rootkit functionality, and audit and/or block it by using whitelisting[[CiteRef::Beechey 2010]] tools, like AppLocker,[[CiteRef::Windows Commands JPCERT]][[CiteRef::NSA MS AppLocker]] or Software Restriction Policies[[CiteRef::Corio 2008]] where appropriate.[[CiteRef::TechNet Applocker vs SRP]]", + "id": "course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f", + "modified": "2017-05-31T21:30:26.495974Z", + "name": "Rootkit Mitigation", + "spec_version": "2.1", + "type": "course-of-action" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd/20170531213041022744.json b/stix2/test/v21/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd/20170531213041022744.json new file mode 100644 index 0000000..dcc5b0d --- /dev/null +++ b/stix2/test/v21/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd/20170531213041022744.json @@ -0,0 +1,10 @@ +{ + "created": "2017-05-31T21:30:41.022744Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Identify unnecessary system utilities or potentially malicious software that may be used to collect data from a network share, and audit and/or block them by using whitelisting[[CiteRef::Beechey 2010]] tools, like AppLocker,[[CiteRef::Windows Commands JPCERT]][[CiteRef::NSA MS AppLocker]] or Software Restriction Policies[[CiteRef::Corio 2008]] where appropriate.[[CiteRef::TechNet Applocker vs SRP]]", + "id": "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd", + "modified": "2017-05-31T21:30:41.022744Z", + "name": "Data from Network Shared Drive Mitigation", + "spec_version": "2.1", + "type": "course-of-action" +} diff --git a/stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20170601000000000000.json b/stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20170601000000000000.json new file mode 100644 index 0000000..368273d --- /dev/null +++ b/stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20170601000000000000.json @@ -0,0 +1,15 @@ +{ + "id": "bundle--81884287-2548-47fc-a997-39489ddd5462", + "objects": [ + { + "created": "2017-06-01T00:00:00Z", + "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "identity_class": "organization", + "modified": "2017-06-01T00:00:00Z", + "name": "The MITRE Corporation", + "spec_version": "2.1", + "type": "identity" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20181101232448446000.json b/stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20181101232448446000.json new file mode 100644 index 0000000..e20f6f1 --- /dev/null +++ b/stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5/20181101232448446000.json @@ -0,0 +1,12 @@ +{ + "type": "identity", + "spec_version": "2.1", + "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "created": "2017-06-01T00:00:00.000Z", + "modified": "2018-11-01T23:24:48.446Z", + "name": "The MITRE Corporation", + "identity_class": "organization", + "labels": [ + "version two" + ] +} diff --git a/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064/20170531213149412497.json b/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064/20170531213149412497.json new file mode 100644 index 0000000..b8372aa --- /dev/null +++ b/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064/20170531213149412497.json @@ -0,0 +1,54 @@ +{ + "id": "bundle--7790ee4c-2d57-419a-bc9d-8805b5bb4118", + "objects": [ + { + "aliases": [ + "Deep Panda", + "Shell Crew", + "WebMasters", + "KungFu Kittens", + "PinkPanther", + "Black Vine" + ], + "created": "2017-05-31T21:31:49.412497Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Deep Panda is a suspected Chinese threat group known to target many industries, including government, defense, financial, and telecommunications.Deep Panda.Deep Panda also appears to be known as Black Vine based on the attribution of both group names to the Anthem intrusion.[[Citation: Symantec Black Vine]]", + "external_references": [ + { + "external_id": "G0009", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Group/G0009" + }, + { + "description": "Alperovitch, D. (2014, July 7). Deep in Thought: Chinese Targeting of National Security Think Tanks. Retrieved November 12, 2014.", + "source_name": "Alperovitch 2014", + "url": "http://blog.crowdstrike.com/deep-thought-chinese-targeting-national-security-think-tanks/" + }, + { + "description": "DiMaggio, J.. (2015, August 6). The Black Vine cyberespionage group. Retrieved January 26, 2016.", + "source_name": "Symantec Black Vine", + "url": "http://www.symantec.com/content/en/us/enterprise/media/security%20response/whitepapers/the-black-vine-cyberespionage-group.pdf" + }, + { + "description": "RSA Incident Response. (2014, January). RSA Incident Response Emerging Threat Profile: Shell Crew. Retrieved January 14, 2016.", + "source_name": "RSA Shell Crew", + "url": "https://www.emc.com/collateral/white-papers/h12756-wp-shell-crew.pdf" + }, + { + "description": "ThreatConnect Research Team. (2015, February 27). The Anthem Hack: All Roads Lead to China. Retrieved January 26, 2016.", + "source_name": "ThreatConnect Anthem", + "url": "https://www.threatconnect.com/the-anthem-hack-all-roads-lead-to-china/" + } + ], + "id": "intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064", + "modified": "2017-05-31T21:31:49.412497Z", + "name": "Deep Panda", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "intrusion-set" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a/20170531213153197755.json b/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a/20170531213153197755.json new file mode 100644 index 0000000..2fe46f1 --- /dev/null +++ b/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a/20170531213153197755.json @@ -0,0 +1,44 @@ +{ + "id": "bundle--96a6ea7a-fcff-4aab-925b-a494bcdf0480", + "objects": [ + { + "aliases": [ + "DragonOK" + ], + "created": "2017-05-31T21:31:53.197755Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "DragonOK is a threat group that has targeted Japanese organizations with phishing emails. Due to overlapping TTPs, including similar custom tools, DragonOK is thought to have a direct or indirect relationship with the threat group Moafee. [[Citation: Operation Quantum Entanglement]][[Citation: Symbiotic APT Groups]] It is known to use a variety of malware, including Sysget/HelloBridge, PlugX, PoisonIvy, FormerFirstRat, NFlog, and NewCT. [[Citation: New DragonOK]]", + "external_references": [ + { + "external_id": "G0017", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Group/G0017" + }, + { + "description": "Haq, T., Moran, N., Vashisht, S., Scott, M. (2014, September). OPERATION QUANTUM ENTANGLEMENT. Retrieved November 4, 2015.", + "source_name": "Operation Quantum Entanglement", + "url": "https://www.fireeye.com/content/dam/fireeye-www/global/en/current-threats/pdfs/wp-operation-quantum-entanglement.pdf" + }, + { + "description": "Haq, T. (2014, October). An Insight into Symbiotic APT Groups. Retrieved November 4, 2015.", + "source_name": "Symbiotic APT Groups", + "url": "https://dl.mandiant.com/EE/library/MIRcon2014/MIRcon%202014%20R&D%20Track%20Insight%20into%20Symbiotic%20APT.pdf" + }, + { + "description": "Miller-Osborn, J., Grunzweig, J.. (2015, April). Unit 42 Identifies New DragonOK Backdoor Malware Deployed Against Japanese Targets. Retrieved November 4, 2015.", + "source_name": "New DragonOK", + "url": "http://researchcenter.paloaltonetworks.com/2015/04/unit-42-identifies-new-dragonok-backdoor-malware-deployed-against-japanese-targets/" + } + ], + "id": "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a", + "modified": "2017-05-31T21:31:53.197755Z", + "name": "DragonOK", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "intrusion-set" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json new file mode 100644 index 0000000..54343ce --- /dev/null +++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json @@ -0,0 +1,28 @@ +{ + "type": "malware", + "spec_version": "2.1", + "id": "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "created": "2017-05-31T21:32:58.226Z", + "modified": "2018-11-16T22:54:20.390Z", + "name": "Rover", + "malware_types": [ + "version four" + ], + "description": "Rover is malware suspected of being used for espionage purposes. It was used in 2015 in a targeted email sent to an Indian Ambassador to Afghanistan.[[Citation: Palo Alto Rover]]", + "external_references": [ + { + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0090", + "external_id": "S0090" + }, + { + "source_name": "Palo Alto Rover", + "description": "Ray, V., Hayashi, K. (2016, February 29). New Malware \u2018Rover\u2019 Targets Indian Ambassador to Afghanistan. Retrieved February 29, 2016.", + "url": "http://researchcenter.paloaltonetworks.com/2016/02/new-malware-rover-targets-indian-ambassador-to-afghanistan/" + } + ], + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ] +} diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json new file mode 100644 index 0000000..8ea538e --- /dev/null +++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json @@ -0,0 +1,34 @@ +{ + "id": "bundle--f64de948-7067-4534-8018-85f03d470625", + "objects": [ + { + "created": "2017-05-31T21:32:58.226477Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Rover is malware suspected of being used for espionage purposes. It was used in 2015 in a targeted email sent to an Indian Ambassador to Afghanistan.[[Citation: Palo Alto Rover]]", + "external_references": [ + { + "external_id": "S0090", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0090" + }, + { + "description": "Ray, V., Hayashi, K. (2016, February 29). New Malware \u2018Rover\u2019 Targets Indian Ambassador to Afghanistan. Retrieved February 29, 2016.", + "source_name": "Palo Alto Rover", + "url": "http://researchcenter.paloaltonetworks.com/2016/02/new-malware-rover-targets-indian-ambassador-to-afghanistan/" + } + ], + "id": "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + "malware_types": [ + "malware" + ], + "modified": "2017-05-31T21:32:58.226477Z", + "name": "Rover", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "malware" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json new file mode 100644 index 0000000..4236920 --- /dev/null +++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json @@ -0,0 +1,28 @@ +{ + "type": "malware", + "spec_version": "2.1", + "id": "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "created": "2017-05-31T21:32:58.226Z", + "modified": "2018-11-01T23:24:48.456Z", + "name": "Rover", + "malware_types": [ + "version two" + ], + "description": "Rover is malware suspected of being used for espionage purposes. It was used in 2015 in a targeted email sent to an Indian Ambassador to Afghanistan.[[Citation: Palo Alto Rover]]", + "external_references": [ + { + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0090", + "external_id": "S0090" + }, + { + "source_name": "Palo Alto Rover", + "description": "Ray, V., Hayashi, K. (2016, February 29). New Malware \u2018Rover\u2019 Targets Indian Ambassador to Afghanistan. Retrieved February 29, 2016.", + "url": "http://researchcenter.paloaltonetworks.com/2016/02/new-malware-rover-targets-indian-ambassador-to-afghanistan/" + } + ], + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ] +} diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json new file mode 100644 index 0000000..37dd9c5 --- /dev/null +++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json @@ -0,0 +1,28 @@ +{ + "type": "malware", + "spec_version": "2.1", + "id": "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "created": "2017-05-31T21:32:58.226Z", + "modified": "2018-11-01T23:24:48.457Z", + "name": "Rover", + "malware_types": [ + "version three" + ], + "description": "Rover is malware suspected of being used for espionage purposes. It was used in 2015 in a targeted email sent to an Indian Ambassador to Afghanistan.[[Citation: Palo Alto Rover]]", + "external_references": [ + { + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0090", + "external_id": "S0090" + }, + { + "source_name": "Palo Alto Rover", + "description": "Ray, V., Hayashi, K. (2016, February 29). New Malware \u2018Rover\u2019 Targets Indian Ambassador to Afghanistan. Retrieved February 29, 2016.", + "url": "http://researchcenter.paloaltonetworks.com/2016/02/new-malware-rover-targets-indian-ambassador-to-afghanistan/" + } + ], + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ] +} diff --git a/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json b/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json new file mode 100644 index 0000000..9f51a11 --- /dev/null +++ b/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json @@ -0,0 +1,34 @@ +{ + "id": "bundle--c633942b-545c-4c87-91b7-9fe5740365e0", + "objects": [ + { + "created": "2017-05-31T21:33:26.565056Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "RTM is custom malware written in Delphi. It is used by the group of the same name (RTM).[[Citation: ESET RTM Feb 2017]]", + "external_references": [ + { + "external_id": "S0148", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0148" + }, + { + "description": "Faou, M. and Boutin, J.. (2017, February). Read The Manual: A Guide to the RTM Banking Trojan. Retrieved March 9, 2017.", + "source_name": "ESET RTM Feb 2017", + "url": "https://www.welivesecurity.com/wp-content/uploads/2017/02/Read-The-Manual.pdf" + } + ], + "id": "malware--92ec0cbd-2c30-44a2-b270-73f4ec949841", + "malware_types": [ + "malware" + ], + "modified": "2017-05-31T21:33:26.565056Z", + "name": "RTM", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "malware" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json b/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json new file mode 100644 index 0000000..2808866 --- /dev/null +++ b/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json @@ -0,0 +1,34 @@ +{ + "id": "bundle--09ce4338-8741-4fcf-9738-d216c8e40974", + "objects": [ + { + "created": "2017-05-31T21:32:48.482655Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Sakula is a remote access tool (RAT) that first surfaced in 2012 and was used in intrusions throughout 2015.[[Citation: Dell Sakula]]\n\nAliases: Sakula, Sakurel, VIPER", + "external_references": [ + { + "external_id": "S0074", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0074" + }, + { + "description": "Dell SecureWorks Counter Threat Unit Threat Intelligence. (2015, July 30). Sakula Malware Family. Retrieved January 26, 2016.", + "source_name": "Dell Sakula", + "url": "http://www.secureworks.com/cyber-threat-intelligence/threats/sakula-malware-family/" + } + ], + "id": "malware--96b08451-b27a-4ff6-893f-790e26393a8e", + "malware_types": [ + "malware" + ], + "modified": "2017-05-31T21:32:48.482655Z", + "name": "Sakula", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "malware" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json b/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json new file mode 100644 index 0000000..3e1c870 --- /dev/null +++ b/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json @@ -0,0 +1,34 @@ +{ + "id": "bundle--611947ce-ae3b-4fdb-b297-aed8eab22e4f", + "objects": [ + { + "created": "2017-05-31T21:32:15.263882Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "PoisonIvy is a popular remote access tool (RAT) that has been used by many groups.[[Citation: FireEye Poison Ivy]]\n\nAliases: PoisonIvy, Poison Ivy", + "external_references": [ + { + "external_id": "S0012", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0012" + }, + { + "description": "FireEye. (2014). POISON IVY: Assessing Damage and Extracting Intelligence. Retrieved November 12, 2014.", + "source_name": "FireEye Poison Ivy", + "url": "https://www.fireeye.com/content/dam/fireeye-www/global/en/current-threats/pdfs/rpt-poison-ivy.pdf" + } + ], + "id": "malware--b42378e0-f147-496f-992a-26a49705395b", + "labels": [ + "malware" + ], + "modified": "2017-05-31T21:32:15.263882Z", + "name": "PoisonIvy", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "type": "malware" + } + ], + "spec_version": "2.0", + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json b/stix2/test/v21/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json new file mode 100644 index 0000000..3f8d6f0 --- /dev/null +++ b/stix2/test/v21/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json @@ -0,0 +1,16 @@ +{ + "id": "bundle--0f4a3025-7788-4f25-a0c7-26171056dfae", + "objects": [ + { + "created": "2017-06-01T00:00:00Z", + "definition": { + "statement": "Copyright 2017, The MITRE Corporation" + }, + "definition_type": "statement", + "id": "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168", + "spec_version": "2.1", + "type": "marking-definition" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883/20170531213327182784.json b/stix2/test/v21/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883/20170531213327182784.json new file mode 100644 index 0000000..915b126 --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883/20170531213327182784.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--7e715462-dd9d-40b9-968a-10ef0ecf126d", + "objects": [ + { + "created": "2017-05-31T21:33:27.182784Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--0d4a7788-7f3b-4df8-a498-31a38003c883", + "modified": "2017-05-31T21:33:27.182784Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "uses", + "source_ref": "attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a", + "target_ref": "malware--92ec0cbd-2c30-44a2-b270-73f4ec949841", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227/20170531213327082801.json b/stix2/test/v21/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227/20170531213327082801.json new file mode 100644 index 0000000..478ca3a --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227/20170531213327082801.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--a53eef35-abfc-4bcd-b84e-a048f7b4a9bf", + "objects": [ + { + "created": "2017-05-31T21:33:27.082801Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227", + "modified": "2017-05-31T21:33:27.082801Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "uses", + "source_ref": "attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22", + "target_ref": "tool--242f3da3-4425-4d11-8f5c-b842886da966", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432/20170531213327018782.json b/stix2/test/v21/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432/20170531213327018782.json new file mode 100644 index 0000000..2ea9d22 --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432/20170531213327018782.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--0b9f6412-314f-44e3-8779-9738c9578ef5", + "objects": [ + { + "created": "2017-05-31T21:33:27.018782Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--1e91cd45-a725-4965-abe3-700694374432", + "modified": "2017-05-31T21:33:27.018782Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "mitigates", + "source_ref": "course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f", + "target_ref": "attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e/20170531213327100701.json b/stix2/test/v21/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e/20170531213327100701.json new file mode 100644 index 0000000..d0a2a50 --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e/20170531213327100701.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--6d5b04a8-efb2-4179-990e-74f1dcc76e0c", + "objects": [ + { + "created": "2017-05-31T21:33:27.100701Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e", + "modified": "2017-05-31T21:33:27.100701Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "uses", + "source_ref": "attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475", + "target_ref": "tool--03342581-f790-4f03-ba41-e82e67392e23", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1/20170531213327143973.json b/stix2/test/v21/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1/20170531213327143973.json new file mode 100644 index 0000000..0ff1d5a --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1/20170531213327143973.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--a7efc025-040d-49c7-bf97-e5a1120ecacc", + "objects": [ + { + "created": "2017-05-31T21:33:27.143973Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1", + "modified": "2017-05-31T21:33:27.143973Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "uses", + "source_ref": "attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9", + "target_ref": "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719/20170531213327021562.json b/stix2/test/v21/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719/20170531213327021562.json new file mode 100644 index 0000000..640be0c --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719/20170531213327021562.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--9f013d47-7704-41c2-9749-23d0d94af94d", + "objects": [ + { + "created": "2017-05-31T21:33:27.021562Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--592d0c31-e61f-495e-a60e-70d7be59a719", + "modified": "2017-05-31T21:33:27.021562Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "mitigates", + "source_ref": "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd", + "target_ref": "attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1/20170531213327044387.json b/stix2/test/v21/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1/20170531213327044387.json new file mode 100644 index 0000000..41be9df --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1/20170531213327044387.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--15167b24-4cee-4c96-a140-32a6c37df4b4", + "objects": [ + { + "created": "2017-05-31T21:33:27.044387Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1", + "modified": "2017-05-31T21:33:27.044387Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "uses", + "source_ref": "intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064", + "target_ref": "malware--96b08451-b27a-4ff6-893f-790e26393a8e", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d/20170531213327051532.json b/stix2/test/v21/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d/20170531213327051532.json new file mode 100644 index 0000000..ce33f67 --- /dev/null +++ b/stix2/test/v21/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d/20170531213327051532.json @@ -0,0 +1,20 @@ +{ + "id": "bundle--ff845dca-7036-416f-aae0-95030994c49f", + "objects": [ + { + "created": "2017-05-31T21:33:27.051532Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "id": "relationship--8797579b-e3be-4209-a71b-255a4d08243d", + "modified": "2017-05-31T21:33:27.051532Z", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "relationship_type": "uses", + "source_ref": "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a", + "target_ref": "malware--b42378e0-f147-496f-992a-26a49705395b", + "spec_version": "2.1", + "type": "relationship" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23/20170531213231601148.json b/stix2/test/v21/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23/20170531213231601148.json new file mode 100644 index 0000000..103e8ec --- /dev/null +++ b/stix2/test/v21/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23/20170531213231601148.json @@ -0,0 +1,39 @@ +{ + "id": "bundle--d8826afc-1561-4362-a4e3-05a4c2c3ac3c", + "objects": [ + { + "created": "2017-05-31T21:32:31.601148Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "The Net utility is a component of the Windows operating system. It is used in command-line operations for control of users, groups, services, and network connections.Net has a great deal of functionality,[[Citation: Savill 1999]] much of which is useful for an adversary, such as gathering system and network information for [[Discovery]], moving laterally through [[Windows admin shares]] using net use commands, and interacting with services.\n\nAliases: Net, net.exe", + "external_references": [ + { + "external_id": "S0039", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0039" + }, + { + "description": "Microsoft. (2006, October 18). Net.exe Utility. Retrieved September 22, 2015.", + "source_name": "Microsoft Net Utility", + "url": "https://msdn.microsoft.com/en-us/library/aa939914" + }, + { + "description": "Savill, J. (1999, March 4). Net.exe reference. Retrieved September 22, 2015.", + "source_name": "Savill 1999", + "url": "http://windowsitpro.com/windows/netexe-reference" + } + ], + "id": "tool--03342581-f790-4f03-ba41-e82e67392e23", + "tool_types": [ + "tool" + ], + "modified": "2017-05-31T21:32:31.601148Z", + "name": "Net", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "tool" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966/20170531213212684914.json b/stix2/test/v21/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966/20170531213212684914.json new file mode 100644 index 0000000..32ea7ba --- /dev/null +++ b/stix2/test/v21/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966/20170531213212684914.json @@ -0,0 +1,34 @@ +{ + "id": "bundle--7dbde18f-6f14-4bf0-8389-505c89d6d5a6", + "objects": [ + { + "created": "2017-05-31T21:32:12.684914Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "description": "Windows Credential Editor is a password dumping tool.[[Citation: Amplia WCE]]\n\nAliases: Windows Credential Editor, WCE", + "external_references": [ + { + "external_id": "S0005", + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/wiki/Software/S0005" + }, + { + "description": "Amplia Security. (n.d.). Windows Credentials Editor (WCE) F.A.Q.. Retrieved December 17, 2015.", + "source_name": "Amplia WCE", + "url": "http://www.ampliasecurity.com/research/wcefaq.html" + } + ], + "id": "tool--242f3da3-4425-4d11-8f5c-b842886da966", + "tool_types": [ + "tool" + ], + "modified": "2017-05-31T21:32:12.684914Z", + "name": "Windows Credential Editor", + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "spec_version": "2.1", + "type": "tool" + } + ], + "type": "bundle" +} diff --git a/stix2/test/v21/test_attack_pattern.py b/stix2/test/v21/test_attack_pattern.py new file mode 100644 index 0000000..9c13a12 --- /dev/null +++ b/stix2/test/v21/test_attack_pattern.py @@ -0,0 +1,87 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import ATTACK_PATTERN_ID + +EXPECTED = """{ + "type": "attack-pattern", + "spec_version": "2.1", + "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "name": "Spear Phishing", + "description": "...", + "external_references": [ + { + "source_name": "capec", + "external_id": "CAPEC-163" + } + ] +}""" + + +def test_attack_pattern_example(): + ap = stix2.v21.AttackPattern( + id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + created="2016-05-12T08:17:27.000Z", + modified="2016-05-12T08:17:27.000Z", + name="Spear Phishing", + external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-163", + }], + description="...", + ) + + assert str(ap) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "type": "attack-pattern", + "spec_version": "2.1", + "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "description": "...", + "external_references": [ + { + "external_id": "CAPEC-163", + "source_name": "capec", + }, + ], + "name": "Spear Phishing", + }, + ], +) +def test_parse_attack_pattern(data): + ap = stix2.parse(data, version="2.1") + + assert ap.type == 'attack-pattern' + assert ap.spec_version == '2.1' + assert ap.id == ATTACK_PATTERN_ID + assert ap.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert ap.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert ap.description == "..." + assert ap.external_references[0].external_id == 'CAPEC-163' + assert ap.external_references[0].source_name == 'capec' + assert ap.name == "Spear Phishing" + + +def test_attack_pattern_invalid_labels(): + with pytest.raises(stix2.exceptions.InvalidValueError): + stix2.v21.AttackPattern( + id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + created="2016-05-12T08:17:27Z", + modified="2016-05-12T08:17:27Z", + name="Spear Phishing", + labels=1, + ) + +# TODO: Add other examples diff --git a/stix2/test/v21/test_base.py b/stix2/test/v21/test_base.py new file mode 100644 index 0000000..18d3a50 --- /dev/null +++ b/stix2/test/v21/test_base.py @@ -0,0 +1,25 @@ +import datetime as dt +import json + +import pytest +import pytz + +from stix2.base import STIXJSONEncoder + + +def test_encode_json_datetime(): + now = dt.datetime(2017, 3, 22, 0, 0, 0, tzinfo=pytz.UTC) + test_dict = {'now': now} + + expected = '{"now": "2017-03-22T00:00:00Z"}' + assert json.dumps(test_dict, cls=STIXJSONEncoder) == expected + + +def test_encode_json_object(): + obj = object() + test_dict = {'obj': obj} + + with pytest.raises(TypeError) as excinfo: + json.dumps(test_dict, cls=STIXJSONEncoder) + + assert " is not JSON serializable" in str(excinfo.value) diff --git a/stix2/test/test_bundle.py b/stix2/test/v21/test_bundle.py similarity index 75% rename from stix2/test/test_bundle.py rename to stix2/test/v21/test_bundle.py index 66ffc3d..86c2d00 100644 --- a/stix2/test/test_bundle.py +++ b/stix2/test/v21/test_bundle.py @@ -7,31 +7,33 @@ import stix2 EXPECTED_BUNDLE = """{ "type": "bundle", "id": "bundle--00000000-0000-4000-8000-000000000007", - "spec_version": "2.0", "objects": [ { "type": "indicator", + "spec_version": "2.1", "id": "indicator--00000000-0000-4000-8000-000000000001", "created": "2017-01-01T12:34:56.000Z", "modified": "2017-01-01T12:34:56.000Z", - "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", - "valid_from": "2017-01-01T12:34:56Z", - "labels": [ + "indicator_types": [ "malicious-activity" - ] + ], + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z" }, { "type": "malware", + "spec_version": "2.1", "id": "malware--00000000-0000-4000-8000-000000000003", "created": "2017-01-01T12:34:56.000Z", "modified": "2017-01-01T12:34:56.000Z", "name": "Cryptolocker", - "labels": [ + "malware_types": [ "ransomware" ] }, { "type": "relationship", + "spec_version": "2.1", "id": "relationship--00000000-0000-4000-8000-000000000005", "created": "2017-01-01T12:34:56.000Z", "modified": "2017-01-01T12:34:56.000Z", @@ -45,57 +47,58 @@ EXPECTED_BUNDLE = """{ EXPECTED_BUNDLE_DICT = { "type": "bundle", "id": "bundle--00000000-0000-4000-8000-000000000007", - "spec_version": "2.0", "objects": [ { "type": "indicator", + "spec_version": "2.1", "id": "indicator--00000000-0000-4000-8000-000000000001", "created": "2017-01-01T12:34:56.000Z", "modified": "2017-01-01T12:34:56.000Z", "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "valid_from": "2017-01-01T12:34:56Z", - "labels": [ - "malicious-activity" - ] + "indicator_types": [ + "malicious-activity", + ], }, { "type": "malware", + "spec_version": "2.1", "id": "malware--00000000-0000-4000-8000-000000000003", "created": "2017-01-01T12:34:56.000Z", "modified": "2017-01-01T12:34:56.000Z", "name": "Cryptolocker", - "labels": [ - "ransomware" - ] + "malware_types": [ + "ransomware", + ], }, { "type": "relationship", + "spec_version": "2.1", "id": "relationship--00000000-0000-4000-8000-000000000005", "created": "2017-01-01T12:34:56.000Z", "modified": "2017-01-01T12:34:56.000Z", "relationship_type": "indicates", "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", - "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e" - } - ] + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + }, + ], } def test_empty_bundle(): - bundle = stix2.Bundle() + bundle = stix2.v21.Bundle() assert bundle.type == "bundle" assert bundle.id.startswith("bundle--") - assert bundle.spec_version == "2.0" with pytest.raises(AttributeError): assert bundle.objects def test_bundle_with_wrong_type(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Bundle(type="not-a-bundle") + stix2.v21.Bundle(type="not-a-bundle") - assert excinfo.value.cls == stix2.Bundle + assert excinfo.value.cls == stix2.v21.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'." @@ -103,89 +106,78 @@ def test_bundle_with_wrong_type(): def test_bundle_id_must_start_with_bundle(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.Bundle(id='my-prefix--') + stix2.v21.Bundle(id='my-prefix--') - assert excinfo.value.cls == stix2.Bundle + assert excinfo.value.cls == stix2.v21.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(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'." - - def test_create_bundle1(indicator, malware, relationship): - bundle = stix2.Bundle(objects=[indicator, malware, relationship]) + bundle = stix2.v21.Bundle(objects=[indicator, malware, relationship]) assert str(bundle) == EXPECTED_BUNDLE assert bundle.serialize(pretty=True) == EXPECTED_BUNDLE def test_create_bundle2(indicator, malware, relationship): - bundle = stix2.Bundle(objects=[indicator, malware, relationship]) + bundle = stix2.v21.Bundle(objects=[indicator, malware, relationship]) assert json.loads(bundle.serialize()) == EXPECTED_BUNDLE_DICT def test_create_bundle_with_positional_args(indicator, malware, relationship): - bundle = stix2.Bundle(indicator, malware, relationship) + bundle = stix2.v21.Bundle(indicator, malware, relationship) assert str(bundle) == EXPECTED_BUNDLE def test_create_bundle_with_positional_listarg(indicator, malware, relationship): - bundle = stix2.Bundle([indicator, malware, relationship]) + bundle = stix2.v21.Bundle([indicator, malware, relationship]) assert str(bundle) == EXPECTED_BUNDLE def test_create_bundle_with_listarg_and_positional_arg(indicator, malware, relationship): - bundle = stix2.Bundle([indicator, malware], relationship) + bundle = stix2.v21.Bundle([indicator, malware], relationship) assert str(bundle) == EXPECTED_BUNDLE def test_create_bundle_with_listarg_and_kwarg(indicator, malware, relationship): - bundle = stix2.Bundle([indicator, malware], objects=[relationship]) + bundle = stix2.v21.Bundle([indicator, malware], objects=[relationship]) assert str(bundle) == EXPECTED_BUNDLE def test_create_bundle_with_arg_listarg_and_kwarg(indicator, malware, relationship): - bundle = stix2.Bundle([indicator], malware, objects=[relationship]) + bundle = stix2.v21.Bundle([indicator], malware, objects=[relationship]) assert str(bundle) == EXPECTED_BUNDLE def test_create_bundle_invalid(indicator, malware, relationship): with pytest.raises(ValueError) as excinfo: - stix2.Bundle(objects=[1]) + stix2.v21.Bundle(objects=[1]) assert excinfo.value.reason == "This property may only contain a dictionary or object" with pytest.raises(ValueError) as excinfo: - stix2.Bundle(objects=[{}]) + stix2.v21.Bundle(objects=[{}]) assert excinfo.value.reason == "This property may only contain a non-empty dictionary or object" with pytest.raises(ValueError) as excinfo: - stix2.Bundle(objects=[{'type': 'bundle'}]) + stix2.v21.Bundle(objects=[{'type': 'bundle'}]) assert excinfo.value.reason == 'This property may not contain a Bundle object' -@pytest.mark.parametrize("version", ["2.0"]) +@pytest.mark.parametrize("version", ["2.1"]) def test_parse_bundle(version): bundle = stix2.parse(EXPECTED_BUNDLE, version=version) assert bundle.type == "bundle" assert bundle.id.startswith("bundle--") - assert bundle.spec_version == "2.0" - assert type(bundle.objects[0]) is stix2.Indicator + assert type(bundle.objects[0]) is stix2.v21.Indicator assert bundle.objects[0].type == 'indicator' assert bundle.objects[1].type == 'malware' assert bundle.objects[2].type == 'relationship' @@ -194,6 +186,7 @@ def test_parse_bundle(version): def test_parse_unknown_type(): unknown = { "type": "other", + "spec_version": "2.1", "id": "other--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "created": "2016-04-06T20:03:00Z", "modified": "2016-04-06T20:03:00Z", @@ -203,12 +196,12 @@ def test_parse_unknown_type(): } with pytest.raises(stix2.exceptions.ParseError) as excinfo: - stix2.parse(unknown) + stix2.parse(unknown, version="2.1") assert str(excinfo.value) == "Can't parse unknown object type 'other'! For custom types, use the CustomObject decorator." def test_stix_object_property(): - prop = stix2.core.STIXObjectProperty() + prop = stix2.properties.STIXObjectProperty(spec_version='2.1') - identity = stix2.Identity(name="test", identity_class="individual") + identity = stix2.v21.Identity(name="test", identity_class="individual") assert prop.clean(identity) is identity diff --git a/stix2/test/v21/test_campaign.py b/stix2/test/v21/test_campaign.py new file mode 100644 index 0000000..ad7e753 --- /dev/null +++ b/stix2/test/v21/test_campaign.py @@ -0,0 +1,62 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import CAMPAIGN_ID + +EXPECTED = """{ + "type": "campaign", + "spec_version": "2.1", + "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." +}""" + + +def test_campaign_example(): + campaign = stix2.v21.Campaign( + id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:00Z", + modified="2016-04-06T20:03:00Z", + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", + ) + + assert str(campaign) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "type": "campaign", + "spec_version": "2.1", + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created": "2016-04-06T20:03:00Z", + "modified": "2016-04-06T20:03:00Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "name": "Green Group Attacks Against Finance", + }, + ], +) +def test_parse_campaign(data): + cmpn = stix2.parse(data, version="2.1") + + assert cmpn.type == 'campaign' + assert cmpn.spec_version == '2.1' + assert cmpn.id == CAMPAIGN_ID + assert cmpn.created == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert cmpn.modified == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert cmpn.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert cmpn.description == "Campaign by Green Group against a series of targets in the financial services sector." + assert cmpn.name == "Green Group Attacks Against Finance" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_confidence.py b/stix2/test/v21/test_confidence.py new file mode 100644 index 0000000..dda1670 --- /dev/null +++ b/stix2/test/v21/test_confidence.py @@ -0,0 +1,297 @@ +import pytest + +from stix2.confidence.scales import ( + admiralty_credibility_to_value, dni_to_value, none_low_med_high_to_value, + value_to_admiralty_credibility, value_to_dni, + value_to_none_low_medium_high, value_to_wep, value_to_zero_ten, + wep_to_value, zero_ten_to_value, +) + +CONFIDENCE_ERROR_STR = "STIX Confidence value cannot be determined for %s" +RANGE_ERROR_STR = "Range of values out of bounds: %s" + + +def _between(x, val, y): + return x >= val >= y + + +def test_confidence_range_none_low_med_high(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_none_low_medium_high(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if val == 0: + assert value_to_none_low_medium_high(val) == "None" + elif _between(29, val, 1): + assert value_to_none_low_medium_high(val) == "Low" + elif _between(69, val, 30): + assert value_to_none_low_medium_high(val) == "Med" + elif _between(100, val, 70): + assert value_to_none_low_medium_high(val) == "High" + + +@pytest.mark.parametrize( + "scale_value,result", [ + ("None", 0), + ("Low", 15), + ("Med", 50), + ("High", 85), + ], +) +def test_confidence_scale_valid_none_low_med_high(scale_value, result): + val = none_low_med_high_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize( + "scale_value", [ + "Super", + "none", + "", + ], +) +def test_confidence_scale_invalid_none_low_med_high(scale_value): + with pytest.raises(ValueError) as excinfo: + none_low_med_high_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_zero_ten(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_zero_ten(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if _between(4, val, 0): + assert value_to_zero_ten(val) == "0" + elif _between(14, val, 5): + assert value_to_zero_ten(val) == "1" + elif _between(24, val, 15): + assert value_to_zero_ten(val) == "2" + elif _between(34, val, 25): + assert value_to_zero_ten(val) == "3" + elif _between(44, val, 35): + assert value_to_zero_ten(val) == "4" + elif _between(54, val, 45): + assert value_to_zero_ten(val) == "5" + elif _between(64, val, 55): + assert value_to_zero_ten(val) == "6" + elif _between(74, val, 65): + assert value_to_zero_ten(val) == "7" + elif _between(84, val, 75): + assert value_to_zero_ten(val) == "8" + elif _between(94, val, 85): + assert value_to_zero_ten(val) == "9" + elif _between(100, val, 95): + assert value_to_zero_ten(val) == "10" + + +@pytest.mark.parametrize( + "scale_value,result", [ + ("0", 0), + ("1", 10), + ("2", 20), + ("3", 30), + ("4", 40), + ("5", 50), + ("6", 60), + ("7", 70), + ("8", 80), + ("9", 90), + ("10", 100), + ], +) +def test_confidence_scale_valid_zero_ten(scale_value, result): + val = zero_ten_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize( + "scale_value", [ + "11", + 8, + "", + ], +) +def test_confidence_scale_invalid_zero_ten(scale_value): + with pytest.raises(ValueError) as excinfo: + zero_ten_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_admiralty_credibility(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_admiralty_credibility(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if _between(19, val, 0): + assert value_to_admiralty_credibility(val) == "5 - Improbable" + elif _between(39, val, 20): + assert value_to_admiralty_credibility(val) == "4 - Doubtful" + elif _between(59, val, 40): + assert value_to_admiralty_credibility(val) == "3 - Possibly True" + elif _between(79, val, 60): + assert value_to_admiralty_credibility(val) == "2 - Probably True" + elif _between(100, val, 80): + assert value_to_admiralty_credibility(val) == "1 - Confirmed by other sources" + + +@pytest.mark.parametrize( + "scale_value,result", [ + ("5 - Improbable", 10), + ("4 - Doubtful", 30), + ("3 - Possibly True", 50), + ("2 - Probably True", 70), + ("1 - Confirmed by other sources", 90), + ], +) +def test_confidence_scale_valid_admiralty_credibility(scale_value, result): + val = admiralty_credibility_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize( + "scale_value", [ + "5 - improbable", + "6 - Truth cannot be judged", + "", + ], +) +def test_confidence_scale_invalid_admiralty_credibility(scale_value): + with pytest.raises(ValueError) as excinfo: + admiralty_credibility_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_wep(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_wep(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if val == 0: + assert value_to_wep(val) == "Impossible" + elif _between(19, val, 1): + assert value_to_wep(val) == "Highly Unlikely/Almost Certainly Not" + elif _between(39, val, 20): + assert value_to_wep(val) == "Unlikely/Probably Not" + elif _between(59, val, 40): + assert value_to_wep(val) == "Even Chance" + elif _between(79, val, 60): + assert value_to_wep(val) == "Likely/Probable" + elif _between(99, val, 80): + assert value_to_wep(val) == "Highly likely/Almost Certain" + elif val == 100: + assert value_to_wep(val) == "Certain" + + +@pytest.mark.parametrize( + "scale_value,result", [ + ("Impossible", 0), + ("Highly Unlikely/Almost Certainly Not", 10), + ("Unlikely/Probably Not", 30), + ("Even Chance", 50), + ("Likely/Probable", 70), + ("Highly likely/Almost Certain", 90), + ("Certain", 100), + ], +) +def test_confidence_scale_valid_wep(scale_value, result): + val = wep_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize( + "scale_value", [ + "Unlikely / Probably Not", + "Almost certain", + "", + ], +) +def test_confidence_scale_invalid_wep(scale_value): + with pytest.raises(ValueError) as excinfo: + wep_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_dni(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_dni(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if _between(9, val, 0): + assert value_to_dni(val) == "Almost No Chance / Remote" + elif _between(19, val, 10): + assert value_to_dni(val) == "Very Unlikely / Highly Improbable" + elif _between(39, val, 20): + assert value_to_dni(val) == "Unlikely / Improbable" + elif _between(59, val, 40): + assert value_to_dni(val) == "Roughly Even Chance / Roughly Even Odds" + elif _between(79, val, 60): + assert value_to_dni(val) == "Likely / Probable" + elif _between(89, val, 80): + assert value_to_dni(val) == "Very Likely / Highly Probable" + elif _between(100, val, 90): + assert value_to_dni(val) == "Almost Certain / Nearly Certain" + + +@pytest.mark.parametrize( + "scale_value,result", [ + ("Almost No Chance / Remote", 5), + ("Very Unlikely / Highly Improbable", 15), + ("Unlikely / Improbable", 30), + ("Roughly Even Chance / Roughly Even Odds", 50), + ("Likely / Probable", 70), + ("Very Likely / Highly Probable", 85), + ("Almost Certain / Nearly Certain", 95), + ], +) +def test_confidence_scale_valid_dni(scale_value, result): + val = dni_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize( + "scale_value", [ + "Almost Certain/Nearly Certain", + "Almost Certain / nearly Certain", + "", + ], +) +def test_confidence_scale_invalid_none_dni(scale_value): + with pytest.raises(ValueError) as excinfo: + dni_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value diff --git a/stix2/test/v21/test_core.py b/stix2/test/v21/test_core.py new file mode 100644 index 0000000..19aa275 --- /dev/null +++ b/stix2/test/v21/test_core.py @@ -0,0 +1,175 @@ +import pytest + +import stix2 +from stix2 import core, exceptions + +BUNDLE = { + "type": "bundle", + "id": "bundle--00000000-0000-4000-8000-000000000007", + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z", + "indicator_types": [ + "malicious-activity", + ], + }, + { + "type": "malware", + "spec_version": "2.1", + "id": "malware--00000000-0000-4000-8000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "malware_types": [ + "ransomware", + ], + }, + { + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--00000000-0000-4000-8000-000000000005", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + }, + ], +} + + +def test_dict_to_stix2_bundle_with_version(): + with pytest.raises(exceptions.InvalidValueError) as excinfo: + core.dict_to_stix2(BUNDLE, version='2.0') + + msg = "Invalid value for Bundle 'objects': Spec version 2.0 bundles don't yet support containing objects of a different spec version." + assert str(excinfo.value) == msg + + +def test_parse_observable_with_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = core.parse_observable(observable, version='2.1') + v = 'v21' + + assert v in str(obs_obj.__class__) + + +@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") +def test_parse_observable_with_no_version(): + observable = {"type": "file", "name": "foo.exe"} + obs_obj = core.parse_observable(observable) + v = 'v21' + + assert v in str(obs_obj.__class__) + + +def test_register_object_with_version(): + bundle = core.dict_to_stix2(BUNDLE, version='2.1') + core._register_object(bundle.objects[0].__class__) + v = 'v21' + + assert bundle.objects[0].type in core.STIX2_OBJ_MAPS[v]['objects'] + assert v in str(bundle.objects[0].__class__) + + +def test_register_marking_with_version(): + core._register_marking(stix2.v21.TLP_WHITE.__class__, version='2.1') + v = 'v21' + + assert stix2.v21.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] + assert v in str(stix2.v21.TLP_WHITE.__class__) + + +@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") +def test_register_marking_with_no_version(): + # Uses default version (2.0 in this case) + core._register_marking(stix2.v21.TLP_WHITE.__class__) + v = 'v21' + + assert stix2.v21.TLP_WHITE.definition._type in core.STIX2_OBJ_MAPS[v]['markings'] + assert v in str(stix2.v21.TLP_WHITE.__class__) + + +def test_register_observable_with_default_version(): + observed_data = stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + "extensions": { + "ntfs-ext": { + "alternate_data_streams": [ + { + "name": "second.stream", + "size": 25536, + }, + ], + }, + }, + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["0"], + }, + }, + ) + core._register_observable(observed_data.objects['0'].__class__) + v = 'v21' + + assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] + assert v in str(observed_data.objects['0'].__class__) + + +def test_register_observable_extension_with_default_version(): + observed_data = stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + "extensions": { + "ntfs-ext": { + "alternate_data_streams": [ + { + "name": "second.stream", + "size": 25536, + }, + ], + }, + }, + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["0"], + }, + }, + ) + core._register_observable_extension(observed_data.objects['0'], observed_data.objects['0'].extensions['ntfs-ext'].__class__) + v = 'v21' + + assert observed_data.objects['0'].type in core.STIX2_OBJ_MAPS[v]['observables'] + assert v in str(observed_data.objects['0'].__class__) + + assert observed_data.objects['0'].extensions['ntfs-ext']._type in core.STIX2_OBJ_MAPS[v]['observable-extensions']['file'] + assert v in str(observed_data.objects['0'].extensions['ntfs-ext'].__class__) diff --git a/stix2/test/v21/test_course_of_action.py b/stix2/test/v21/test_course_of_action.py new file mode 100644 index 0000000..73e8eca --- /dev/null +++ b/stix2/test/v21/test_course_of_action.py @@ -0,0 +1,62 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import COURSE_OF_ACTION_ID + +EXPECTED = """{ + "type": "course-of-action", + "spec_version": "2.1", + "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", + "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." +}""" + + +def test_course_of_action_example(): + coa = stix2.v21.CourseOfAction( + id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", + name="Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", + description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", + ) + + assert str(coa) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", + "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", + "spec_version": "2.1", + "type": "course-of-action", + }, + ], +) +def test_parse_course_of_action(data): + coa = stix2.parse(data, version="2.1") + + assert coa.type == 'course-of-action' + assert coa.spec_version == '2.1' + assert coa.id == COURSE_OF_ACTION_ID + assert coa.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert coa.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert coa.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert coa.description == "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." + assert coa.name == "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py new file mode 100644 index 0000000..295520e --- /dev/null +++ b/stix2/test/v21/test_custom.py @@ -0,0 +1,976 @@ +import pytest + +import stix2 +import stix2.base + +from .constants import FAKE_TIME, MARKING_DEFINITION_ID + +IDENTITY_CUSTOM_PROP = stix2.v21.Identity( + name="John Smith", + identity_class="individual", + x_foo="bar", + allow_custom=True, +) + + +def test_identity_custom_property(): + with pytest.raises(ValueError) as excinfo: + stix2.v21.Identity( + id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties="foobar", + ) + assert str(excinfo.value) == "'custom_properties' must be a dictionary" + + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.v21.Identity( + id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "foo": "bar", + }, + foo="bar", + ) + assert "Unexpected properties for Identity" in str(excinfo.value) + + identity = stix2.v21.Identity( + id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "foo": "bar", + }, + ) + assert identity.foo == "bar" + + +def test_identity_custom_property_invalid(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.v21.Identity( + id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + x_foo="bar", + ) + assert excinfo.value.cls == stix2.v21.Identity + assert excinfo.value.properties == ['x_foo'] + assert "Unexpected properties for" in str(excinfo.value) + + +def test_identity_custom_property_allowed(): + identity = stix2.v21.Identity( + id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + x_foo="bar", + allow_custom=True, + ) + assert identity.x_foo == "bar" + + +@pytest.mark.parametrize( + "data", [ + """{ + "type": "identity", + "spec_version": "2.1", + "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + "created": "2015-12-21T19:59:11Z", + "modified": "2015-12-21T19:59:11Z", + "name": "John Smith", + "identity_class": "individual", + "foo": "bar" + }""", + ], +) +def test_parse_identity_custom_property(data): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.parse(data, version="2.1") + assert excinfo.value.cls == stix2.v21.Identity + assert excinfo.value.properties == ['foo'] + assert "Unexpected properties for" in str(excinfo.value) + + identity = stix2.parse(data, version="2.1", allow_custom=True) + assert identity.foo == "bar" + + +def test_custom_property_object_in_bundled_object(): + bundle = stix2.v21.Bundle(IDENTITY_CUSTOM_PROP, allow_custom=True) + + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_properties_object_in_bundled_object(): + obj = stix2.v21.Identity( + name="John Smith", + identity_class="individual", + custom_properties={ + "x_foo": "bar", + }, + ) + bundle = stix2.v21.Bundle(obj, allow_custom=True) + + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_property_dict_in_bundled_object(): + custom_identity = { + 'type': 'identity', + 'spec_version': '2.1', + 'id': 'identity--311b2d2d-f010-4473-83ec-1edf84858f4c', + 'created': '2015-12-21T19:59:11Z', + 'name': 'John Smith', + 'identity_class': 'individual', + 'x_foo': 'bar', + } + with pytest.raises(stix2.exceptions.ExtraPropertiesError): + stix2.v21.Bundle(custom_identity) + + bundle = stix2.v21.Bundle(custom_identity, allow_custom=True) + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_properties_dict_in_bundled_object(): + custom_identity = { + 'type': 'identity', + 'spec_version': '2.1', + 'id': 'identity--311b2d2d-f010-4473-83ec-1edf84858f4c', + 'created': '2015-12-21T19:59:11Z', + 'name': 'John Smith', + 'identity_class': 'individual', + 'custom_properties': { + 'x_foo': 'bar', + }, + } + bundle = stix2.v21.Bundle(custom_identity) + + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_property_in_observed_data(): + artifact = stix2.v21.File( + allow_custom=True, + name='test', + x_foo='bar', + ) + observed_data = stix2.v21.ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=1, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_custom_property_object_in_observable_extension(): + ntfs = stix2.v21.NTFSExt( + allow_custom=True, + sid=1, + x_foo='bar', + ) + artifact = stix2.v21.File( + name='test', + extensions={'ntfs-ext': ntfs}, + ) + observed_data = stix2.v21.ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=1, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_custom_property_dict_in_observable_extension(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError): + stix2.v21.File( + name='test', + extensions={ + 'ntfs-ext': { + 'sid': 1, + 'x_foo': 'bar', + }, + }, + ) + + artifact = stix2.v21.File( + allow_custom=True, + name='test', + extensions={ + 'ntfs-ext': { + 'allow_custom': True, + 'sid': 1, + 'x_foo': 'bar', + }, + }, + ) + observed_data = stix2.v21.ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=1, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_identity_custom_property_revoke(): + identity = IDENTITY_CUSTOM_PROP.revoke() + assert identity.x_foo == "bar" + + +def test_identity_custom_property_edit_markings(): + marking_obj = stix2.v21.MarkingDefinition( + id=MARKING_DEFINITION_ID, + definition_type="statement", + definition=stix2.v21.StatementMarking(statement="Copyright 2016, Example Corp"), + ) + marking_obj2 = stix2.v21.MarkingDefinition( + id=MARKING_DEFINITION_ID, + definition_type="statement", + definition=stix2.v21.StatementMarking(statement="Another one"), + ) + + # None of the following should throw exceptions + identity = IDENTITY_CUSTOM_PROP.add_markings(marking_obj) + identity2 = identity.add_markings(marking_obj2, ['x_foo']) + identity2.remove_markings(marking_obj.id) + identity2.remove_markings(marking_obj2.id, ['x_foo']) + identity2.clear_markings() + identity2.clear_markings('x_foo') + + +def test_custom_marking_no_init_1(): + @stix2.v21.CustomMarking( + 'x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj(): + pass + + no = NewObj(property1='something') + assert no.property1 == 'something' + + +def test_custom_marking_no_init_2(): + @stix2.v21.CustomMarking( + 'x-new-obj2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(object): + pass + + no2 = NewObj2(property1='something') + assert no2.property1 == 'something' + + +@stix2.v21.CustomObject( + 'x-new-type', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], +) +class NewType(object): + def __init__(self, property2=None, **kwargs): + if property2 and property2 < 10: + raise ValueError("'property2' is too small.") + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_custom_object_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewType(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_custom_object_type(): + nt = NewType(property1='something') + assert nt.property1 == 'something' + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + NewType(property2=42) + assert "No values for required properties" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + NewType(property1='something', property2=4) + assert "'property2' is too small." in str(excinfo.value) + + +def test_custom_object_no_init_1(): + @stix2.v21.CustomObject( + 'x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj(): + pass + + no = NewObj(property1='something') + assert no.property1 == 'something' + + +def test_custom_object_no_init_2(): + @stix2.v21.CustomObject( + 'x-new-obj2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(object): + pass + + no2 = NewObj2(property1='something') + assert no2.property1 == 'something' + + +def test_custom_object_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObject( + 'x', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj(object): + pass # pragma: no cover + assert "Invalid type name 'x': " in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObject( + 'x_new_object', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj2(object): + pass # pragma: no cover + assert "Invalid type name 'x_new_object':" in str(excinfo.value) + + +def test_parse_custom_object_type(): + nt_string = """{ + "type": "x-new-type", + "created": "2015-12-21T19:59:11Z", + "property1": "something" + }""" + + nt = stix2.parse(nt_string, allow_custom=True) + assert nt["property1"] == 'something' + + +def test_parse_unregistered_custom_object_type(): + nt_string = """{ + "type": "x-foobar-observable", + "created": "2015-12-21T19:59:11Z", + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse(nt_string, version="2.1") + assert "Can't parse unknown object type" in str(excinfo.value) + assert "use the CustomObject decorator." in str(excinfo.value) + + +def test_parse_unregistered_custom_object_type_w_allow_custom(): + """parse an unknown custom object, allowed by passing + 'allow_custom' flag + """ + nt_string = """{ + "type": "x-foobar-observable", + "created": "2015-12-21T19:59:11Z", + "property1": "something" + }""" + + custom_obj = stix2.parse(nt_string, version="2.1", allow_custom=True) + assert custom_obj["type"] == "x-foobar-observable" + + +@stix2.v21.CustomObservable( + 'x-new-observable', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ('x_property3', stix2.properties.BooleanProperty()), + ], +) +class NewObservable(): + def __init__(self, property2=None, **kwargs): + if property2 and property2 < 10: + raise ValueError("'property2' is too small.") + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_custom_observable_object_1(): + no = NewObservable(property1='something') + assert no.property1 == 'something' + + +def test_custom_observable_object_2(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + NewObservable(property2=42) + assert excinfo.value.properties == ['property1'] + assert "No values for required properties" in str(excinfo.value) + + +def test_custom_observable_object_3(): + with pytest.raises(ValueError) as excinfo: + NewObservable(property1='something', property2=4) + assert "'property2' is too small." in str(excinfo.value) + + +def test_custom_observable_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewObservable(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_custom_observable_object_no_init_1(): + @stix2.v21.CustomObservable( + 'x-new-observable', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObs(): + pass + + no = NewObs(property1='something') + assert no.property1 == 'something' + + +def test_custom_observable_object_no_init_2(): + @stix2.v21.CustomObservable( + 'x-new-obs2', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObs2(object): + pass + + no2 = NewObs2(property1='something') + assert no2.property1 == 'something' + + +def test_custom_observable_object_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + 'x', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObs(object): + pass # pragma: no cover + assert "Invalid observable type name 'x':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + 'x_new_obs', [ + ('property1', stix2.properties.StringProperty()), + ], + ) + class NewObs2(object): + pass # pragma: no cover + assert "Invalid observable type name 'x_new_obs':" in str(excinfo.value) + + +def test_custom_observable_object_invalid_ref_property(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + 'x-new-obs', [ + ('property_ref', stix2.properties.StringProperty()), + ], + ) + class NewObs(): + pass + assert "is named like an object reference property but is not an ObjectReferenceProperty" in str(excinfo.value) + + +def test_custom_observable_object_invalid_refs_property(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + 'x-new-obs', [ + ('property_refs', stix2.properties.StringProperty()), + ], + ) + class NewObs(): + pass + assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) + + +def test_custom_observable_object_invalid_refs_list_property(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomObservable( + 'x-new-obs', [ + ('property_refs', stix2.properties.ListProperty(stix2.properties.StringProperty)), + ], + ) + class NewObs(): + pass + assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) + + +def test_custom_observable_object_invalid_valid_refs(): + @stix2.v21.CustomObservable( + 'x-new-obs', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property_ref', stix2.properties.ObjectReferenceProperty(valid_types='email-addr')), + ], + ) + class NewObs(): + pass + + with pytest.raises(Exception) as excinfo: + NewObs( + _valid_refs=['1'], + property1='something', + property_ref='1', + ) + assert "must be created with _valid_refs as a dict, not a list" in str(excinfo.value) + + +def test_custom_no_properties_raises_exception(): + with pytest.raises(TypeError): + + @stix2.v21.CustomObject('x-new-object-type') + class NewObject1(object): + pass + + +def test_custom_wrong_properties_arg_raises_exception(): + with pytest.raises(ValueError): + + @stix2.v21.CustomObservable('x-new-object-type', (("prop", stix2.properties.BooleanProperty()))) + class NewObject2(object): + pass + + +def test_parse_custom_observable_object(): + nt_string = """{ + "type": "x-new-observable", + "property1": "something" + }""" + + nt = stix2.parse_observable(nt_string, [], version='2.1') + assert isinstance(nt, stix2.base._STIXBase) + assert nt.property1 == 'something' + + +def test_parse_unregistered_custom_observable_object(): + nt_string = """{ + "type": "x-foobar-observable", + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.CustomContentError) as excinfo: + stix2.parse_observable(nt_string, version='2.1') + assert "Can't parse unknown observable type" in str(excinfo.value) + + parsed_custom = stix2.parse_observable(nt_string, allow_custom=True, version='2.1') + assert parsed_custom['property1'] == 'something' + with pytest.raises(AttributeError) as excinfo: + assert parsed_custom.property1 == 'something' + assert not isinstance(parsed_custom, stix2.base._STIXBase) + + +def test_parse_unregistered_custom_observable_object_with_no_type(): + nt_string = """{ + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse_observable(nt_string, allow_custom=True, version='2.1') + assert "Can't parse observable with no 'type' property" in str(excinfo.value) + + +def test_parse_observed_data_with_custom_observable(): + input_str = """{ + "type": "observed-data", + "id": "observed-data--dc20c4ca-a2a3-4090-a5d5-9558c3af4758", + "created": "2016-04-06T19:58:16.000Z", + "modified": "2016-04-06T19:58:16.000Z", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "number_observed": 1, + "objects": { + "0": { + "type": "x-foobar-observable", + "property1": "something" + } + } + }""" + parsed = stix2.parse(input_str, version="2.1", allow_custom=True) + assert parsed.objects['0']['property1'] == 'something' + + +def test_parse_invalid_custom_observable_object(): + nt_string = """{ + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse_observable(nt_string, version='2.1') + assert "Can't parse observable with no 'type' property" in str(excinfo.value) + + +def test_observable_custom_property(): + with pytest.raises(ValueError) as excinfo: + NewObservable( + property1='something', + custom_properties="foobar", + ) + assert "'custom_properties' must be a dictionary" in str(excinfo.value) + + no = NewObservable( + property1='something', + custom_properties={ + "foo": "bar", + }, + ) + assert no.foo == "bar" + + +def test_observable_custom_property_invalid(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + NewObservable( + property1='something', + x_foo="bar", + ) + assert excinfo.value.properties == ['x_foo'] + assert "Unexpected properties for" in str(excinfo.value) + + +def test_observable_custom_property_allowed(): + no = NewObservable( + property1='something', + x_foo="bar", + allow_custom=True, + ) + assert no.x_foo == "bar" + + +def test_observed_data_with_custom_observable_object(): + no = NewObservable(property1='something') + ob_data = stix2.v21.ObservedData( + first_observed=FAKE_TIME, + last_observed=FAKE_TIME, + number_observed=1, + objects={'0': no}, + allow_custom=True, + ) + assert ob_data.objects['0'].property1 == 'something' + + +@stix2.v21.CustomExtension( + stix2.v21.DomainName, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], +) +class NewExtension(): + def __init__(self, property2=None, **kwargs): + if property2 and property2 < 10: + raise ValueError("'property2' is too small.") + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_custom_extension_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewExtension(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_custom_extension(): + ext = NewExtension(property1='something') + assert ext.property1 == 'something' + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + NewExtension(property2=42) + assert excinfo.value.properties == ['property1'] + assert str(excinfo.value) == "No values for required properties for _CustomExtension: (property1)." + + with pytest.raises(ValueError) as excinfo: + NewExtension(property1='something', property2=4) + assert str(excinfo.value) == "'property2' is too small." + + +def test_custom_extension_wrong_observable_type(): + # NewExtension is an extension of DomainName, not File + ext = NewExtension(property1='something') + with pytest.raises(ValueError) as excinfo: + stix2.v21.File( + name="abc.txt", + extensions={ + "ntfs-ext": ext, + }, + ) + + assert 'Cannot determine extension type' in excinfo.value.reason + + +@pytest.mark.parametrize( + "data", [ + """{ + "keys": [ + { + "test123": 123, + "test345": "aaaa" + } + ] +}""", + ], +) +def test_custom_extension_with_list_and_dict_properties_observable_type(data): + @stix2.v21.CustomExtension( + stix2.v21.UserAccount, 'some-extension', [ + ('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True)), + ], + ) + class SomeCustomExtension: + pass + + example = SomeCustomExtension(keys=[{'test123': 123, 'test345': 'aaaa'}]) + assert data == str(example) + + +def test_custom_extension_invalid_observable(): + # These extensions are being applied to improperly-created Observables. + # The Observable classes should have been created with the CustomObservable decorator. + class Foo(object): + pass + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + Foo, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class FooExtension(): + pass # pragma: no cover + assert str(excinfo.value) == "'observable' must be a valid Observable class!" + + class Bar(stix2.v21.observables._Observable): + pass + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + Bar, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class BarExtension(): + pass + assert "Unknown observable type" in str(excinfo.value) + assert "Custom observables must be created with the @CustomObservable decorator." in str(excinfo.value) + + class Baz(stix2.v21.observables._Observable): + _type = 'Baz' + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + Baz, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class BazExtension(): + pass + assert "Unknown observable type" in str(excinfo.value) + assert "Custom observables must be created with the @CustomObservable decorator." in str(excinfo.value) + + +def test_custom_extension_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + stix2.v21.File, 'x', { + 'property1': stix2.properties.StringProperty(required=True), + }, + ) + class FooExtension(): + pass # pragma: no cover + assert "Invalid extension type name 'x':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension( + stix2.v21.File, 'x_new_ext', { + 'property1': stix2.properties.StringProperty(required=True), + }, + ) + class BlaExtension(): + pass # pragma: no cover + assert "Invalid extension type name 'x_new_ext':" in str(excinfo.value) + + +def test_custom_extension_no_properties(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', None) + class BarExtension(): + pass + assert "Must supply a list, containing tuples." in str(excinfo.value) + + +def test_custom_extension_empty_properties(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', []) + class BarExtension(): + pass + assert "Must supply a list, containing tuples." in str(excinfo.value) + + +def test_custom_extension_dict_properties(): + with pytest.raises(ValueError) as excinfo: + @stix2.v21.CustomExtension(stix2.v21.DomainName, 'x-new-ext2', {}) + class BarExtension(): + pass + assert "Must supply a list, containing tuples." in str(excinfo.value) + + +def test_custom_extension_no_init_1(): + @stix2.v21.CustomExtension( + stix2.v21.DomainName, 'x-new-extension', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewExt(): + pass + + ne = NewExt(property1="foobar") + assert ne.property1 == "foobar" + + +def test_custom_extension_no_init_2(): + @stix2.v21.CustomExtension( + stix2.v21.DomainName, 'x-new-ext2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewExt2(object): + pass + + ne2 = NewExt2(property1="foobar") + assert ne2.property1 == "foobar" + + +def test_parse_observable_with_custom_extension(): + input_str = """{ + "type": "domain-name", + "value": "example.com", + "extensions": { + "x-new-ext": { + "property1": "foo", + "property2": 12 + } + } + }""" + + parsed = stix2.parse_observable(input_str, version='2.1') + assert parsed.extensions['x-new-ext'].property2 == 12 + + +@pytest.mark.parametrize( + "data", [ + # URL is not in EXT_MAP + """{ + "type": "url", + "value": "example.com", + "extensions": { + "x-foobar-ext": { + "property1": "foo", + "property2": 12 + } + } + }""", + # File is in EXT_MAP + """{ + "type": "file", + "name": "foo.txt", + "extensions": { + "x-foobar-ext": { + "property1": "foo", + "property2": 12 + } + } + }""", + ], +) +def test_parse_observable_with_unregistered_custom_extension(data): + with pytest.raises(ValueError) as excinfo: + stix2.parse_observable(data, version='2.1') + assert "Can't parse unknown extension type" in str(excinfo.value) + + parsed_ob = stix2.parse_observable(data, allow_custom=True, version='2.1') + assert parsed_ob['extensions']['x-foobar-ext']['property1'] == 'foo' + assert not isinstance(parsed_ob['extensions']['x-foobar-ext'], stix2.base._STIXBase) + + +def test_register_custom_object(): + # Not the way to register custom object. + class CustomObject2(object): + _type = 'awesome-object' + + stix2.core._register_object(CustomObject2, version="2.1") + # Note that we will always check against newest OBJ_MAP. + assert (CustomObject2._type, CustomObject2) in stix2.v21.OBJ_MAP.items() + + +def test_extension_property_location(): + assert 'extensions' in stix2.v21.OBJ_MAP_OBSERVABLE['x-new-observable']._properties + assert 'extensions' not in stix2.v21.EXT_MAP['domain-name']['x-new-ext']._properties + + +@pytest.mark.parametrize( + "data", [ + """{ + "type": "x-example", + "spec_version": "2.1", + "id": "x-example--336d8a9f-91f1-46c5-b142-6441bb9f8b8d", + "created": "2018-06-12T16:20:58.059Z", + "modified": "2018-06-12T16:20:58.059Z", + "dictionary": { + "key": { + "key_a": "value", + "key_b": "value" + } + } +}""", + ], +) +def test_custom_object_nested_dictionary(data): + @stix2.v21.CustomObject( + 'x-example', [ + ('dictionary', stix2.properties.DictionaryProperty()), + ], + ) + class Example(object): + def __init__(self, **kwargs): + pass + + example = Example( + id='x-example--336d8a9f-91f1-46c5-b142-6441bb9f8b8d', + created='2018-06-12T16:20:58.059Z', + modified='2018-06-12T16:20:58.059Z', + dictionary={'key': {'key_b': 'value', 'key_a': 'value'}}, + ) + + assert data == str(example) diff --git a/stix2/test/v21/test_datastore.py b/stix2/test/v21/test_datastore.py new file mode 100644 index 0000000..8bb5494 --- /dev/null +++ b/stix2/test/v21/test_datastore.py @@ -0,0 +1,127 @@ +import pytest + +from stix2.datastore import ( + CompositeDataSource, DataSink, DataSource, DataStoreMixin, +) +from stix2.datastore.filters import Filter + +from .constants import CAMPAIGN_MORE_KWARGS + + +def test_datasource_abstract_class_raises_error(): + with pytest.raises(TypeError): + DataSource() + + +def test_datasink_abstract_class_raises_error(): + with pytest.raises(TypeError): + DataSink() + + +def test_datastore_smoke(): + assert DataStoreMixin() is not None + + +def test_datastore_get_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().get("indicator--00000000-0000-4000-8000-000000000001") + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_all_versions_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().all_versions("indicator--00000000-0000-4000-8000-000000000001") + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_query_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().query([Filter("type", "=", "indicator")]) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_creator_of_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().creator_of(CAMPAIGN_MORE_KWARGS) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_relationships_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().relationships( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_related_to_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().related_to( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_add_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().add(CAMPAIGN_MORE_KWARGS) + assert "DataStoreMixin has no data sink to put objects in" == str(excinfo.value) + + +def test_composite_datastore_get_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().get("indicator--00000000-0000-4000-8000-000000000001") + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_all_versions_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().all_versions("indicator--00000000-0000-4000-8000-000000000001") + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_query_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().query([Filter("type", "=", "indicator")]) + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_relationships_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().relationships( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_related_to_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().related_to( + obj="indicator--00000000-0000-4000-8000-000000000001", + target_only=True, + ) + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_add_data_source_raises_error(): + with pytest.raises(TypeError) as excinfo: + ind = "indicator--00000000-0000-4000-8000-000000000001" + CompositeDataSource().add_data_source(ind) + assert "DataSource (to be added) is not of type stix2.DataSource. DataSource type is '{}'".format(type(ind)) == str(excinfo.value) + + +def test_composite_datastore_add_data_sources_raises_error(): + with pytest.raises(TypeError) as excinfo: + ind = "indicator--00000000-0000-4000-8000-000000000001" + CompositeDataSource().add_data_sources(ind) + assert "DataSource (to be added) is not of type stix2.DataSource. DataSource type is '{}'".format(type(ind)) == str(excinfo.value) + + +def test_composite_datastore_no_datasource(): + cds = CompositeDataSource() + with pytest.raises(AttributeError) as excinfo: + cds.get("indicator--00000000-0000-4000-8000-000000000001") + assert 'CompositeDataSource has no data source' in str(excinfo.value) diff --git a/stix2/test/v21/test_datastore_composite.py b/stix2/test/v21/test_datastore_composite.py new file mode 100644 index 0000000..76119c3 --- /dev/null +++ b/stix2/test/v21/test_datastore_composite.py @@ -0,0 +1,152 @@ +import pytest + +from stix2.datastore import CompositeDataSource, make_id +from stix2.datastore.filters import Filter +from stix2.datastore.memory import MemorySink, MemorySource, MemoryStore +from stix2.utils import parse_into_datetime +from stix2.v21.common import TLP_GREEN + + +def test_add_remove_composite_datasource(): + cds = CompositeDataSource() + ds1 = MemorySource() + ds2 = MemorySource() + ds3 = MemorySink() + + with pytest.raises(TypeError) as excinfo: + cds.add_data_sources([ds1, ds2, ds1, ds3]) + assert str(excinfo.value) == ( + "DataSource (to be added) is not of type " + "stix2.DataSource. DataSource type is ''" + ) + + cds.add_data_sources([ds1, ds2, ds1]) + + assert len(cds.get_all_data_sources()) == 2 + + cds.remove_data_sources([ds1.id, ds2.id]) + + assert len(cds.get_all_data_sources()) == 0 + + +def test_composite_datasource_operations(stix_objs1, stix_objs2): + BUNDLE1 = dict( + id="bundle--%s" % make_id(), + objects=stix_objs1, + type="bundle", + ) + cds1 = CompositeDataSource() + ds1_1 = MemorySource(stix_data=BUNDLE1) + ds1_2 = MemorySource(stix_data=stix_objs2) + + cds2 = CompositeDataSource() + ds2_1 = MemorySource(stix_data=BUNDLE1) + ds2_2 = MemorySource(stix_data=stix_objs2) + + cds1.add_data_sources([ds1_1, ds1_2]) + cds2.add_data_sources([ds2_1, ds2_2]) + + indicators = cds1.all_versions("indicator--00000000-0000-4000-8000-000000000001") + + # In STIX_OBJS2 changed the 'modified' property to a later time... + assert len(indicators) == 3 + + cds1.add_data_sources([cds2]) + + indicator = cds1.get("indicator--00000000-0000-4000-8000-000000000001") + + assert indicator["id"] == "indicator--00000000-0000-4000-8000-000000000001" + assert indicator["modified"] == parse_into_datetime("2017-01-31T13:49:53.935Z") + assert indicator["type"] == "indicator" + + query1 = [ + Filter("type", "=", "indicator"), + ] + + query2 = [ + Filter("valid_from", "=", "2017-01-27T13:49:53.935382Z"), + ] + + cds1.filters.add(query2) + + results = cds1.query(query1) + + # STIX_OBJS2 has indicator with later time, one with different id, one with + # original time in STIX_OBJS1 + assert len(results) == 4 + + indicator = cds1.get("indicator--00000000-0000-4000-8000-000000000001") + + assert indicator["id"] == "indicator--00000000-0000-4000-8000-000000000001" + assert indicator["modified"] == parse_into_datetime("2017-01-31T13:49:53.935Z") + assert indicator["type"] == "indicator" + + results = cds1.all_versions("indicator--00000000-0000-4000-8000-000000000001") + assert len(results) == 3 + + # Since we have filters already associated with our CompositeSource providing + # nothing returns the same as cds1.query(query1) (the associated query is query2) + results = cds1.query([]) + assert len(results) == 4 + + +def test_source_markings(): + msrc = MemorySource(TLP_GREEN) + + assert msrc.get(TLP_GREEN.id) == TLP_GREEN + assert msrc.all_versions(TLP_GREEN.id) == [TLP_GREEN] + assert msrc.query(Filter("id", "=", TLP_GREEN.id)) == [TLP_GREEN] + + +def test_sink_markings(): + # just make sure there is no crash + msink = MemorySink(TLP_GREEN) + msink.add(TLP_GREEN) + + +def test_store_markings(): + mstore = MemoryStore(TLP_GREEN) + + assert mstore.get(TLP_GREEN.id) == TLP_GREEN + assert mstore.all_versions(TLP_GREEN.id) == [TLP_GREEN] + assert mstore.query(Filter("id", "=", TLP_GREEN.id)) == [TLP_GREEN] + + +def test_source_mixed(indicator): + msrc = MemorySource([TLP_GREEN, indicator]) + + assert msrc.get(TLP_GREEN.id) == TLP_GREEN + assert msrc.all_versions(TLP_GREEN.id) == [TLP_GREEN] + assert msrc.query(Filter("id", "=", TLP_GREEN.id)) == [TLP_GREEN] + + assert msrc.get(indicator.id) == indicator + assert msrc.all_versions(indicator.id) == [indicator] + assert msrc.query(Filter("id", "=", indicator.id)) == [indicator] + + all_objs = msrc.query() + assert TLP_GREEN in all_objs + assert indicator in all_objs + assert len(all_objs) == 2 + + +def test_sink_mixed(indicator): + # just make sure there is no crash + msink = MemorySink([TLP_GREEN, indicator]) + msink.add([TLP_GREEN, indicator]) + + +def test_store_mixed(indicator): + mstore = MemoryStore([TLP_GREEN, indicator]) + + assert mstore.get(TLP_GREEN.id) == TLP_GREEN + assert mstore.all_versions(TLP_GREEN.id) == [TLP_GREEN] + assert mstore.query(Filter("id", "=", TLP_GREEN.id)) == [TLP_GREEN] + + assert mstore.get(indicator.id) == indicator + assert mstore.all_versions(indicator.id) == [indicator] + assert mstore.query(Filter("id", "=", indicator.id)) == [indicator] + + all_objs = mstore.query() + assert TLP_GREEN in all_objs + assert indicator in all_objs + assert len(all_objs) == 2 diff --git a/stix2/test/v21/test_datastore_filesystem.py b/stix2/test/v21/test_datastore_filesystem.py new file mode 100644 index 0000000..2404f3f --- /dev/null +++ b/stix2/test/v21/test_datastore_filesystem.py @@ -0,0 +1,1055 @@ +import datetime +import errno +import json +import os +import shutil +import stat + +import pytest +import pytz + +import stix2 +from stix2.datastore.filesystem import ( + AuthSet, _find_search_optimizations, _get_matching_dir_entries, + _timestamp2filename, +) +from stix2.exceptions import STIXError + +from .constants import ( + CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, INDICATOR_ID, + INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS, +) + +FS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") + + +@pytest.fixture +def fs_store(): + # create + yield stix2.FileSystemStore(FS_PATH) + + # remove campaign dir + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +@pytest.fixture +def fs_source(): + # create + fs = stix2.FileSystemSource(FS_PATH) + assert fs.stix_dir == FS_PATH + yield fs + + # remove campaign dir + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +@pytest.fixture +def fs_sink(): + # create + fs = stix2.FileSystemSink(FS_PATH) + assert fs.stix_dir == FS_PATH + yield fs + + # remove campaign dir + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +@pytest.fixture +def bad_json_files(): + # create erroneous JSON files for tests to make sure handled gracefully + + with open(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-json.txt"), "w+") as f: + f.write("Im not a JSON file") + + with open(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-bad-json.json"), "w+") as f: + f.write("Im not a JSON formatted file") + + yield True # dummy yield so can have teardown + + os.remove(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-json.txt")) + os.remove(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-bad-json.json")) + + +@pytest.fixture +def bad_stix_files(): + # create erroneous STIX JSON files for tests to make sure handled correctly + + # bad STIX object + stix_obj = { + "id": "intrusion-set--test-bad-stix", + "spec_version": "2.0", + # no "type" field + } + + with open(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-stix.json"), "w+") as f: + f.write(json.dumps(stix_obj)) + + yield True # dummy yield so can have teardown + + os.remove(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-stix.json")) + + +@pytest.fixture(scope='module') +def rel_fs_store(): + cam = stix2.v21.Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = stix2.v21.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = stix2.v21.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = stix2.v21.Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = stix2.v21.Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = stix2.v21.Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = stix2.v21.Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] + fs = stix2.FileSystemStore(FS_PATH) + for o in stix_objs: + fs.add(o) + yield fs + + for o in stix_objs: + filepath = os.path.join( + FS_PATH, o.type, o.id, + _timestamp2filename(o.modified) + '.json', + ) + + # Some test-scoped fixtures (e.g. fs_store) delete all campaigns, so by + # the time this module-scoped fixture tears itself down, it may find + # its campaigns already gone, which causes not-found errors. + try: + os.remove(filepath) + except OSError as e: + # 3 is the ERROR_PATH_NOT_FOUND windows error code. Which has an + # errno symbolic value, but not the windows meaning... + if e.errno in (errno.ENOENT, 3): + continue + raise + + +def test_filesystem_source_nonexistent_folder(): + with pytest.raises(ValueError) as excinfo: + stix2.FileSystemSource('nonexistent-folder') + assert "for STIX data does not exist" in str(excinfo) + + +def test_filesystem_sink_nonexistent_folder(): + with pytest.raises(ValueError) as excinfo: + stix2.FileSystemSink('nonexistent-folder') + assert "for STIX data does not exist" in str(excinfo) + + +def test_filesystem_source_bad_json_file(fs_source, bad_json_files): + # this tests the handling of two bad json files + # - one file should just be skipped (silently) as its a ".txt" extension + # - one file should be parsed and raise Exception bc its not JSON + try: + fs_source.get("intrusion-set--test-bad-json") + except TypeError as e: + assert "intrusion-set--test-bad-json" in str(e) + assert "could either not be parsed to JSON or was not valid STIX JSON" in str(e) + + +def test_filesystem_source_bad_stix_file(fs_source, bad_stix_files): + # this tests handling of bad STIX json object + try: + fs_source.get("intrusion-set--test-non-stix") + except STIXError as e: + assert "Can't parse object with no 'type' property" in str(e) + + +def test_filesystem_source_get_object(fs_source): + # get (latest) object + mal = fs_source.get("malware--6b616fc1-1505-48e3-8b2c-0d19337bff38") + assert mal.id == "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38" + assert mal.name == "Rover" + assert mal.modified == datetime.datetime( + 2018, 11, 16, 22, 54, 20, 390000, + pytz.utc, + ) + + +def test_filesystem_source_get_nonexistent_object(fs_source): + ind = fs_source.get("indicator--6b616fc1-1505-48e3-8b2c-0d19337bff38") + assert ind is None + + +def test_filesystem_source_all_versions(fs_source): + ids = fs_source.all_versions( + "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + ) + assert len(ids) == 2 + assert all( + id_.id == "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" + for id_ in ids + ) + assert all(id_.name == "The MITRE Corporation" for id_ in ids) + assert all(id_.type == "identity" for id_ in ids) + + +def test_filesystem_source_query_single(fs_source): + # query2 + is_2 = fs_source.query([stix2.Filter("external_references.external_id", '=', "T1027")]) + assert len(is_2) == 1 + + is_2 = is_2[0] + assert is_2.id == "attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a" + assert is_2.type == "attack-pattern" + + +def test_filesystem_source_query_multiple(fs_source): + # query + intrusion_sets = fs_source.query([stix2.Filter("type", '=', "intrusion-set")]) + assert len(intrusion_sets) == 2 + assert "intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064" in [is_.id for is_ in intrusion_sets] + assert "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a" in [is_.id for is_ in intrusion_sets] + + is_1 = [is_ for is_ in intrusion_sets if is_.id == "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a"][0] + assert "DragonOK" in is_1.aliases + assert len(is_1.external_references) == 4 + + +def test_filesystem_source_backward_compatible(fs_source): + # this specific object is outside an "ID" directory; make sure we can get + # it. + modified = datetime.datetime(2018, 11, 16, 22, 54, 20, 390000, pytz.utc) + results = fs_source.query([ + stix2.Filter("type", "=", "malware"), + stix2.Filter("id", "=", "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"), + stix2.Filter("modified", "=", modified), + ]) + + assert len(results) == 1 + result = results[0] + assert result.type == "malware" + assert result.id == "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38" + assert result.modified == modified + assert result.malware_types == ["version four"] + + +def test_filesystem_sink_add_python_stix_object(fs_sink, fs_source): + # add python stix object + camp1 = stix2.v21.Campaign( + name="Hannibal", + objective="Targeting Italian and Spanish Diplomat internet accounts", + aliases=["War Elephant"], + ) + + fs_sink.add(camp1) + + filepath = os.path.join( + FS_PATH, "campaign", camp1.id, + _timestamp2filename(camp1.modified) + ".json", + ) + assert os.path.exists(filepath) + + camp1_r = fs_source.get(camp1.id) + assert camp1_r.id == camp1.id + assert camp1_r.name == "Hannibal" + assert "War Elephant" in camp1_r.aliases + + os.remove(filepath) + + +def test_filesystem_sink_add_stix_object_dict(fs_sink, fs_source): + # add stix object dict + camp2 = { + "name": "Aurelius", + "type": "campaign", + "objective": "German and French Intelligence Services", + "aliases": ["Purple Robes"], + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created": "2017-05-31T21:31:53.197755Z", + "modified": "2017-05-31T21:31:53.197755Z", + } + + fs_sink.add(camp2) + + # Need to get the exact "modified" timestamp which would have been + # in effect at the time the object was saved to the sink, which determines + # the filename it would have been saved as. It may not be exactly the same + # as what's in the dict, since the parsing process can enforce a precision + # constraint (e.g. truncate to milliseconds), which results in a slightly + # different name. + camp2obj = stix2.parse(camp2) + filepath = os.path.join( + FS_PATH, "campaign", camp2obj["id"], + _timestamp2filename(camp2obj["modified"]) + ".json", + ) + + assert os.path.exists(filepath) + + camp2_r = fs_source.get(camp2["id"]) + assert camp2_r.id == camp2["id"] + assert camp2_r.name == camp2["name"] + assert "Purple Robes" in camp2_r.aliases + + os.remove(filepath) + + +def test_filesystem_sink_add_stix_bundle_dict(fs_sink, fs_source): + # add stix bundle dict + bund = { + "type": "bundle", + "id": "bundle--040ae5ec-2e91-4e94-b075-bc8b368e8ca3", + "objects": [ + { + "name": "Atilla", + "type": "campaign", + "objective": "Bulgarian, Albanian and Romanian Intelligence Services", + "aliases": ["Huns"], + "id": "campaign--b8f86161-ccae-49de-973a-4ca320c62478", + "created": "2017-05-31T21:31:53.197755Z", + "modified": "2017-05-31T21:31:53.197755Z", + }, + ], + } + + fs_sink.add(bund) + + camp_obj = stix2.parse(bund["objects"][0]) + filepath = os.path.join( + FS_PATH, "campaign", camp_obj["id"], + _timestamp2filename(camp_obj["modified"]) + ".json", + ) + + assert os.path.exists(filepath) + + camp3_r = fs_source.get(bund["objects"][0]["id"]) + assert camp3_r.id == bund["objects"][0]["id"] + assert camp3_r.name == bund["objects"][0]["name"] + assert "Huns" in camp3_r.aliases + + os.remove(filepath) + + +def test_filesystem_sink_add_json_stix_object(fs_sink, fs_source): + # add json-encoded stix obj + camp4 = '{"type": "campaign", "id":"campaign--6a6ca372-ba07-42cc-81ef-9840fc1f963d",'\ + ' "created":"2017-05-31T21:31:53.197755Z",'\ + ' "modified":"2017-05-31T21:31:53.197755Z",'\ + ' "name": "Ghengis Khan", "objective": "China and Russian infrastructure"}' + + fs_sink.add(camp4) + + camp4obj = stix2.parse(camp4) + filepath = os.path.join( + FS_PATH, "campaign", + "campaign--6a6ca372-ba07-42cc-81ef-9840fc1f963d", + _timestamp2filename(camp4obj["modified"]) + ".json", + ) + + assert os.path.exists(filepath) + + camp4_r = fs_source.get("campaign--6a6ca372-ba07-42cc-81ef-9840fc1f963d") + assert camp4_r.id == "campaign--6a6ca372-ba07-42cc-81ef-9840fc1f963d" + assert camp4_r.name == "Ghengis Khan" + + os.remove(filepath) + + +def test_filesystem_sink_json_stix_bundle(fs_sink, fs_source): + # add json-encoded stix bundle + bund2 = '{"type": "bundle", "id": "bundle--3d267103-8475-4d8f-b321-35ec6eccfa37",' \ + ' "spec_version": "2.0", "objects": [{"type": "campaign", "id": "campaign--2c03b8bf-82ee-433e-9918-ca2cb6e9534b",' \ + ' "created":"2017-05-31T21:31:53.197755Z",'\ + ' "modified":"2017-05-31T21:31:53.197755Z",'\ + ' "name": "Spartacus", "objective": "Oppressive regimes of Africa and Middle East"}]}' + fs_sink.add(bund2) + + bund2obj = stix2.parse(bund2) + camp_obj = bund2obj["objects"][0] + + filepath = os.path.join( + FS_PATH, "campaign", + "campaign--2c03b8bf-82ee-433e-9918-ca2cb6e9534b", + _timestamp2filename(camp_obj["modified"]) + ".json", + ) + + assert os.path.exists(filepath) + + camp5_r = fs_source.get("campaign--2c03b8bf-82ee-433e-9918-ca2cb6e9534b") + assert camp5_r.id == "campaign--2c03b8bf-82ee-433e-9918-ca2cb6e9534b" + assert camp5_r.name == "Spartacus" + + os.remove(filepath) + + +def test_filesystem_sink_add_objects_list(fs_sink, fs_source): + # add list of objects + camp6 = stix2.v21.Campaign( + name="Comanche", + objective="US Midwest manufacturing firms, oil refineries, and businesses", + aliases=["Horse Warrior"], + ) + + camp7 = { + "name": "Napolean", + "type": "campaign", + "objective": "Central and Eastern Europe military commands and departments", + "aliases": ["The Frenchmen"], + "id": "campaign--122818b6-1112-4fb0-b11b-b111107ca70a", + "created": "2017-05-31T21:31:53.197755Z", + "modified": "2017-05-31T21:31:53.197755Z", + } + + fs_sink.add([camp6, camp7]) + + camp7obj = stix2.parse(camp7) + + camp6filepath = os.path.join( + FS_PATH, "campaign", camp6.id, + _timestamp2filename(camp6["modified"]) + + ".json", + ) + camp7filepath = os.path.join( + FS_PATH, "campaign", "campaign--122818b6-1112-4fb0-b11b-b111107ca70a", + _timestamp2filename(camp7obj["modified"]) + ".json", + ) + + assert os.path.exists(camp6filepath) + assert os.path.exists(camp7filepath) + + camp6_r = fs_source.get(camp6.id) + assert camp6_r.id == camp6.id + assert "Horse Warrior" in camp6_r.aliases + + camp7_r = fs_source.get(camp7["id"]) + assert camp7_r.id == camp7["id"] + assert "The Frenchmen" in camp7_r.aliases + + # remove all added objects + os.remove(camp6filepath) + os.remove(camp7filepath) + + +def test_filesystem_sink_marking(fs_sink): + marking = stix2.v21.MarkingDefinition( + definition_type="tlp", + definition=stix2.v21.TLPMarking(tlp="green"), + ) + + fs_sink.add(marking) + marking_filepath = os.path.join( + FS_PATH, "marking-definition", marking["id"] + ".json", + ) + + assert os.path.exists(marking_filepath) + + os.remove(marking_filepath) + + +def test_filesystem_store_get_stored_as_bundle(fs_store): + coa = fs_store.get("course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f") + assert coa.id == "course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f" + assert coa.type == "course-of-action" + + +def test_filesystem_store_get_stored_as_object(fs_store): + coa = fs_store.get("course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd") + assert coa.id == "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd" + assert coa.type == "course-of-action" + + +def test_filesystem_store_all_versions(fs_store): + rels = fs_store.all_versions("relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1") + assert len(rels) == 1 + rel = rels[0] + assert rel.id == "relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1" + assert rel.type == "relationship" + + +def test_filesystem_store_query(fs_store): + # query() + tools = fs_store.query([stix2.Filter("tool_types", "in", "tool")]) + assert len(tools) == 2 + assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools] + assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools] + + +def test_filesystem_store_query_single_filter(fs_store): + query = stix2.Filter("tool_types", "in", "tool") + tools = fs_store.query(query) + assert len(tools) == 2 + assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools] + assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools] + + +def test_filesystem_store_empty_query(fs_store): + results = fs_store.query() # returns all + assert len(results) == 30 + assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [obj.id for obj in results] + assert "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" in [obj.id for obj in results] + + +def test_filesystem_store_query_multiple_filters(fs_store): + fs_store.source.filters.add(stix2.Filter("tool_types", "in", "tool")) + tools = fs_store.query(stix2.Filter("id", "=", "tool--242f3da3-4425-4d11-8f5c-b842886da966")) + assert len(tools) == 1 + assert tools[0].id == "tool--242f3da3-4425-4d11-8f5c-b842886da966" + + +def test_filesystem_store_query_dont_include_type_folder(fs_store): + results = fs_store.query(stix2.Filter("type", "!=", "tool")) + assert len(results) == 28 + + +def test_filesystem_store_add(fs_store): + # add() + camp1 = stix2.v21.Campaign( + name="Great Heathen Army", + objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", + aliases=["Ragnar"], + ) + fs_store.add(camp1) + + camp1_r = fs_store.get(camp1.id) + assert camp1_r.id == camp1.id + assert camp1_r.name == camp1.name + + filepath = os.path.join( + FS_PATH, "campaign", camp1_r.id, + _timestamp2filename(camp1_r.modified) + ".json", + ) + + # remove + os.remove(filepath) + + +def test_filesystem_store_add_as_bundle(): + fs_store = stix2.FileSystemStore(FS_PATH, bundlify=True) + + camp1 = stix2.v21.Campaign( + name="Great Heathen Army", + objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", + aliases=["Ragnar"], + ) + fs_store.add(camp1) + + filepath = os.path.join( + FS_PATH, "campaign", camp1.id, + _timestamp2filename(camp1.modified) + ".json", + ) + + with open(filepath) as bundle_file: + assert '"type": "bundle"' in bundle_file.read() + + camp1_r = fs_store.get(camp1.id) + assert camp1_r.id == camp1.id + assert camp1_r.name == camp1.name + + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +def test_filesystem_add_bundle_object(fs_store): + bundle = stix2.v21.Bundle() + fs_store.add(bundle) + + +def test_filesystem_store_add_invalid_object(fs_store): + ind = ('campaign', 'campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f') # tuple isn't valid + with pytest.raises(TypeError) as excinfo: + fs_store.add(ind) + assert 'stix_data must be' in str(excinfo.value) + assert 'a STIX object' in str(excinfo.value) + assert 'JSON formatted STIX' in str(excinfo.value) + assert 'JSON formatted STIX bundle' in str(excinfo.value) + + +def test_filesystem_store_add_marking(fs_store): + marking = stix2.v21.MarkingDefinition( + definition_type="tlp", + definition=stix2.v21.TLPMarking(tlp="green"), + ) + + fs_store.add(marking) + marking_filepath = os.path.join( + FS_PATH, "marking-definition", marking["id"] + ".json", + ) + + assert os.path.exists(marking_filepath) + + marking_r = fs_store.get(marking["id"]) + assert marking_r["id"] == marking["id"] + assert marking_r["definition"]["tlp"] == "green" + + os.remove(marking_filepath) + + +def test_filesystem_object_with_custom_property(fs_store): + camp = stix2.v21.Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) + + fs_store.add(camp) + + camp_r = fs_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_filesystem_object_with_custom_property_in_bundle(fs_store): + camp = stix2.v21.Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) + + bundle = stix2.v21.Bundle(camp, allow_custom=True) + fs_store.add(bundle) + + camp_r = fs_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_filesystem_custom_object(fs_store): + @stix2.v21.CustomObject( + 'x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ], + ) + class NewObj(): + pass + + newobj = NewObj(property1='something') + fs_store.add(newobj) + + newobj_r = fs_store.get(newobj.id) + assert newobj_r["id"] == newobj["id"] + assert newobj_r["property1"] == 'something' + + # remove dir + shutil.rmtree(os.path.join(FS_PATH, "x-new-obj"), True) + + +def test_relationships(rel_fs_store): + mal = rel_fs_store.get(MALWARE_ID) + resp = rel_fs_store.relationships(mal) + + assert len(resp) == 3 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[1] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_type(rel_fs_store): + mal = rel_fs_store.get(MALWARE_ID) + resp = rel_fs_store.relationships(mal, relationship_type='indicates') + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[0] + + +def test_relationships_by_source(rel_fs_store): + resp = rel_fs_store.relationships(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[1] + + +def test_relationships_by_target(rel_fs_store): + resp = rel_fs_store.relationships(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_type(rel_fs_store): + resp = rel_fs_store.relationships(MALWARE_ID, relationship_type='uses', target_only=True) + + assert len(resp) == 1 + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_source(rel_fs_store): + with pytest.raises(ValueError) as excinfo: + rel_fs_store.relationships(MALWARE_ID, target_only=True, source_only=True) + + assert 'not both' in str(excinfo.value) + + +def test_related_to(rel_fs_store): + mal = rel_fs_store.get(MALWARE_ID) + resp = rel_fs_store.related_to(mal) + + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_source(rel_fs_store): + resp = rel_fs_store.related_to(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_target(rel_fs_store): + resp = rel_fs_store.related_to(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + + +def test_auth_set_white1(): + auth_set = AuthSet({"A"}, set()) + + assert auth_set.auth_type == AuthSet.WHITE + assert auth_set.values == {"A"} + + +def test_auth_set_white2(): + auth_set = AuthSet(set(), set()) + + assert auth_set.auth_type == AuthSet.WHITE + assert len(auth_set.values) == 0 + + +def test_auth_set_white3(): + auth_set = AuthSet({"A", "B"}, {"B", "C"}) + + assert auth_set.auth_type == AuthSet.WHITE + assert auth_set.values == {"A"} + + +def test_auth_set_black1(): + auth_set = AuthSet(None, {"B", "C"}) + + assert auth_set.auth_type == AuthSet.BLACK + assert auth_set.values == {"B", "C"} + + +def test_optimize_types1(): + filters = [ + stix2.Filter("type", "=", "foo"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"foo"} + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types2(): + filters = [ + stix2.Filter("type", "=", "foo"), + stix2.Filter("type", "=", "bar"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert len(auth_types.values) == 0 + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types3(): + filters = [ + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter("type", "in", ["B", "C", "D"]), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"B", "C"} + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types4(): + filters = [ + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter("type", "in", ["D", "E", "F"]), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert len(auth_types.values) == 0 + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types5(): + filters = [ + stix2.Filter("type", "in", ["foo", "bar"]), + stix2.Filter("type", "!=", "bar"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"foo"} + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types6(): + filters = [ + stix2.Filter("type", "!=", "foo"), + stix2.Filter("type", "!=", "bar"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.BLACK + assert auth_types.values == {"foo", "bar"} + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types7(): + filters = [ + stix2.Filter("type", "=", "foo"), + stix2.Filter("type", "!=", "foo"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert len(auth_types.values) == 0 + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types8(): + filters = [] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.BLACK + assert len(auth_types.values) == 0 + assert auth_ids.auth_type == AuthSet.BLACK + assert len(auth_ids.values) == 0 + + +def test_optimize_types_ids1(): + filters = [ + stix2.Filter("type", "in", ["foo", "bar"]), + stix2.Filter("id", "=", "foo--00000000-0000-0000-0000-000000000000"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"foo"} + assert auth_ids.auth_type == AuthSet.WHITE + assert auth_ids.values == {"foo--00000000-0000-0000-0000-000000000000"} + + +def test_optimize_types_ids2(): + filters = [ + stix2.Filter("type", "=", "foo"), + stix2.Filter("id", "=", "bar--00000000-0000-0000-0000-000000000000"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert len(auth_types.values) == 0 + assert auth_ids.auth_type == AuthSet.WHITE + assert len(auth_ids.values) == 0 + + +def test_optimize_types_ids3(): + filters = [ + stix2.Filter("type", "in", ["foo", "bar"]), + stix2.Filter("id", "!=", "bar--00000000-0000-0000-0000-000000000000"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"foo", "bar"} + assert auth_ids.auth_type == AuthSet.BLACK + assert auth_ids.values == {"bar--00000000-0000-0000-0000-000000000000"} + + +def test_optimize_types_ids4(): + filters = [ + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter( + "id", "in", [ + "B--00000000-0000-0000-0000-000000000000", + "C--00000000-0000-0000-0000-000000000000", + "D--00000000-0000-0000-0000-000000000000", + ], + ), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"B", "C"} + assert auth_ids.auth_type == AuthSet.WHITE + assert auth_ids.values == { + "B--00000000-0000-0000-0000-000000000000", + "C--00000000-0000-0000-0000-000000000000", + } + + +def test_optimize_types_ids5(): + filters = [ + stix2.Filter("type", "in", ["A", "B", "C"]), + stix2.Filter("type", "!=", "C"), + stix2.Filter( + "id", "in", [ + "B--00000000-0000-0000-0000-000000000000", + "C--00000000-0000-0000-0000-000000000000", + "D--00000000-0000-0000-0000-000000000000", + ], + ), + stix2.Filter("id", "!=", "D--00000000-0000-0000-0000-000000000000"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"B"} + assert auth_ids.auth_type == AuthSet.WHITE + assert auth_ids.values == {"B--00000000-0000-0000-0000-000000000000"} + + +def test_optimize_types_ids6(): + filters = [ + stix2.Filter("id", "=", "A--00000000-0000-0000-0000-000000000000"), + ] + + auth_types, auth_ids = _find_search_optimizations(filters) + + assert auth_types.auth_type == AuthSet.WHITE + assert auth_types.values == {"A"} + assert auth_ids.auth_type == AuthSet.WHITE + assert auth_ids.values == {"A--00000000-0000-0000-0000-000000000000"} + + +def test_search_auth_set_white1(): + auth_set = AuthSet( + {"attack-pattern", "doesntexist"}, + set(), + ) + + results = _get_matching_dir_entries(FS_PATH, auth_set, stat.S_ISDIR) + assert results == ["attack-pattern"] + + results = _get_matching_dir_entries(FS_PATH, auth_set, stat.S_ISREG) + assert len(results) == 0 + + +def test_search_auth_set_white2(): + auth_set = AuthSet( + { + "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + "malware--92ec0cbd-2c30-44a2-b270-73f4ec949841", + + }, + { + "malware--92ec0cbd-2c30-44a2-b270-73f4ec949841", + "malware--96b08451-b27a-4ff6-893f-790e26393a8e", + "doesntexist", + }, + ) + + results = _get_matching_dir_entries( + os.path.join(FS_PATH, "malware"), + auth_set, stat.S_ISDIR, + ) + + assert results == ["malware--6b616fc1-1505-48e3-8b2c-0d19337bff38"] + + +def test_search_auth_set_white3(): + auth_set = AuthSet({"20170531213258226477", "doesntexist"}, set()) + + results = _get_matching_dir_entries( + os.path.join( + FS_PATH, "malware", + "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38", + ), + auth_set, stat.S_ISREG, ".json", + ) + + assert results == ["20170531213258226477.json"] + + +def test_search_auth_set_black1(): + auth_set = AuthSet( + None, + {"tool--242f3da3-4425-4d11-8f5c-b842886da966", "doesntexist"}, + ) + + results = _get_matching_dir_entries( + os.path.join(FS_PATH, "tool"), + auth_set, stat.S_ISDIR, + ) + + assert set(results) == { + "tool--03342581-f790-4f03-ba41-e82e67392e23", + } + + +def test_search_auth_set_white_empty(): + auth_set = AuthSet( + set(), + set(), + ) + + results = _get_matching_dir_entries(FS_PATH, auth_set, stat.S_ISDIR) + + assert len(results) == 0 + + +def test_search_auth_set_black_empty(rel_fs_store): + # Ensure rel_fs_store fixture has run so that the type directories are + # predictable (it adds "campaign"). + auth_set = AuthSet( + None, + set(), + ) + + results = _get_matching_dir_entries(FS_PATH, auth_set, stat.S_ISDIR) + + # Should get all dirs + assert set(results) == { + "attack-pattern", + "campaign", + "course-of-action", + "identity", + "indicator", + "intrusion-set", + "malware", + "marking-definition", + "relationship", + "tool", + } + + +def test_timestamp2filename_naive(): + dt = datetime.datetime( + 2010, 6, 15, + 8, 30, 10, 1234, + ) + + filename = _timestamp2filename(dt) + assert filename == "20100615083010001234" + + +def test_timestamp2filename_tz(): + # one hour west of UTC (i.e. an hour earlier) + tz = pytz.FixedOffset(-60) + dt = datetime.datetime( + 2010, 6, 15, + 7, 30, 10, 1234, + tz, + ) + + filename = _timestamp2filename(dt) + assert filename == "20100615083010001234" diff --git a/stix2/test/v21/test_datastore_filters.py b/stix2/test/v21/test_datastore_filters.py new file mode 100644 index 0000000..466d304 --- /dev/null +++ b/stix2/test/v21/test_datastore_filters.py @@ -0,0 +1,487 @@ +import pytest + +from stix2 import parse +from stix2.datastore.filters import Filter, apply_common_filters +from stix2.utils import STIXdatetime, parse_into_datetime + +stix_objs = [ + { + "created": "2017-01-27T13:49:53.997Z", + "description": "\n\nTITLE:\n\tPoison Ivy", + "id": "malware--fdd60b30-b67c-41e3-b0b9-f01faf20d111", + "spec_version": "2.1", + "malware_types": [ + "remote-access-trojan", + ], + "modified": "2017-01-27T13:49:53.997Z", + "name": "Poison Ivy", + "type": "malware", + }, + { + "created": "2014-05-08T09:00:00.000Z", + "id": "indicator--a932fcc6-e032-476c-826f-cb970a5a1ade", + "indicator_types": [ + "file-hash-watchlist", + ], + "modified": "2014-05-08T09:00:00.000Z", + "name": "File hash for Poison Ivy variant", + "pattern": "[file:hashes.'SHA-256' = 'ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2014-05-08T09:00:00.000000Z", + }, + { + "created": "2014-05-08T09:00:00.000Z", + "granular_markings": [ + { + "marking_ref": "marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed", + "selectors": [ + "relationship_type", + ], + }, + ], + "id": "relationship--2f9a9aa9-108a-4333-83e2-4fb25add0463", + "modified": "2014-05-08T09:00:00.000Z", + "object_marking_refs": [ + "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + ], + "relationship_type": "indicates", + "revoked": True, + "source_ref": "indicator--a932fcc6-e032-476c-826f-cb970a5a1ade", + "spec_version": "2.1", + "target_ref": "malware--fdd60b30-b67c-41e3-b0b9-f01faf20d111", + "type": "relationship", + }, + { + "id": "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef", + "spec_version": "2.1", + "created": "2016-02-14T00:00:00.000Z", + "created_by_ref": "identity--f1350682-3290-4e0d-be58-69e290537647", + "modified": "2016-02-14T00:00:00.000Z", + "type": "vulnerability", + "name": "CVE-2014-0160", + "description": "The (1) TLS...", + "external_references": [ + { + "source_name": "cve", + "external_id": "CVE-2014-0160", + }, + ], + "labels": ["heartbleed", "has-logo"], + }, + { + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T19:58:16.000Z", + "modified": "2016-04-06T19:58:16.000Z", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "number_observed": 1, + "objects": { + "0": { + "type": "file", + "name": "HAL 9000.exe", + }, + }, + + }, +] + + +filters = [ + Filter("type", "!=", "relationship"), + Filter("id", "=", "relationship--2f9a9aa9-108a-4333-83e2-4fb25add0463"), + Filter("malware_types", "in", "remote-access-trojan"), + Filter("created", ">", "2015-01-01T01:00:00.000Z"), + Filter("revoked", "=", True), + Filter("revoked", "!=", True), + Filter("object_marking_refs", "=", "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"), + Filter("granular_markings.selectors", "in", "relationship_type"), + Filter("granular_markings.marking_ref", "=", "marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed"), + Filter("external_references.external_id", "in", "CVE-2014-0160,CVE-2017-6608"), + Filter("created_by_ref", "=", "identity--f1350682-3290-4e0d-be58-69e290537647"), + Filter("object_marking_refs", "=", "marking-definition--613f2e26-0000-4000-8000-b8e91df99dc9"), + Filter("granular_markings.selectors", "in", "description"), + Filter("external_references.source_name", "=", "CVE"), + Filter("objects", "=", {"0": {"type": "file", "name": "HAL 9000.exe"}}), + Filter("objects", "contains", {"type": "file", "name": "HAL 9000.exe"}), + Filter("labels", "contains", "heartbleed"), +] + +# same as above objects but converted to real Python STIX2 objects +# to test filters against true Python STIX2 objects +real_stix_objs = [parse(stix_obj) for stix_obj in stix_objs] + + +def test_filter_ops_check(): + # invalid filters - non supported operators + + with pytest.raises(ValueError) as excinfo: + # create Filter that has an operator that is not allowed + Filter('modified', '*', 'not supported operator') + assert str(excinfo.value) == "Filter operator '*' not supported for specified property: 'modified'" + + with pytest.raises(ValueError) as excinfo: + Filter("type", "%", "4") + assert "Filter operator '%' not supported for specified property" in str(excinfo.value) + + +def test_filter_value_type_check(): + # invalid filters - non supported value types + + with pytest.raises(TypeError) as excinfo: + Filter('created', '=', object()) + # On Python 2, the type of object() is `` On Python 3, it's ``. + assert any([s in str(excinfo.value) for s in ["", "''"]]) + assert "is not supported. The type must be a Python immutable type or dictionary" in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + Filter("type", "=", complex(2, -1)) + assert any([s in str(excinfo.value) for s in ["", "''"]]) + assert "is not supported. The type must be a Python immutable type or dictionary" in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + Filter("type", "=", set([16, 23])) + assert any([s in str(excinfo.value) for s in ["", "''"]]) + assert "is not supported. The type must be a Python immutable type or dictionary" in str(excinfo.value) + + +def test_filter_type_underscore_check(): + # check that Filters where property="type", value (name) doesnt have underscores + with pytest.raises(ValueError) as excinfo: + Filter("type", "=", "oh_underscore") + assert "Filter for property 'type' cannot have its value 'oh_underscore'" in str(excinfo.value) + + +def test_apply_common_filters0(): + # "Return any object whose type is not relationship" + resp = list(apply_common_filters(stix_objs, [filters[0]])) + ids = [r['id'] for r in resp] + assert stix_objs[0]['id'] in ids + assert stix_objs[1]['id'] in ids + assert stix_objs[3]['id'] in ids + assert len(ids) == 4 + + resp = list(apply_common_filters(real_stix_objs, [filters[0]])) + ids = [r.id for r in resp] + assert real_stix_objs[0].id in ids + assert real_stix_objs[1].id in ids + assert real_stix_objs[3].id in ids + assert len(ids) == 4 + + +def test_apply_common_filters1(): + # "Return any object that matched id relationship--2f9a9aa9-108a-4333-83e2-4fb25add0463" + resp = list(apply_common_filters(stix_objs, [filters[1]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[1]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters2(): + # "Return any object that contains remote-access-trojan in labels" + resp = list(apply_common_filters(stix_objs, [filters[2]])) + assert resp[0]['id'] == stix_objs[0]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[2]])) + assert resp[0].id == real_stix_objs[0].id + assert len(resp) == 1 + + +def test_apply_common_filters3(): + # "Return any object created after 2015-01-01T01:00:00.000Z" + resp = list(apply_common_filters(stix_objs, [filters[3]])) + assert resp[0]['id'] == stix_objs[0]['id'] + assert len(resp) == 3 + + resp = list(apply_common_filters(real_stix_objs, [filters[3]])) + assert len(resp) == 3 + assert resp[0].id == real_stix_objs[0].id + + +def test_apply_common_filters4(): + # "Return any revoked object" + resp = list(apply_common_filters(stix_objs, [filters[4]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[4]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters5(): + # "Return any object whose not revoked" + resp = list(apply_common_filters(stix_objs, [filters[5]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[5]])) + assert len(resp) == 4 + + +def test_apply_common_filters6(): + # "Return any object that matches marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9 in object_marking_refs" + resp = list(apply_common_filters(stix_objs, [filters[6]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[6]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters7(): + # "Return any object that contains relationship_type in their selectors AND + # also has marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed in marking_ref" + resp = list(apply_common_filters(stix_objs, [filters[7], filters[8]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[7], filters[8]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters8(): + # "Return any object that contains CVE-2014-0160,CVE-2017-6608 in their external_id" + resp = list(apply_common_filters(stix_objs, [filters[9]])) + assert resp[0]['id'] == stix_objs[3]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[9]])) + assert resp[0].id == real_stix_objs[3].id + assert len(resp) == 1 + + +def test_apply_common_filters9(): + # "Return any object that matches created_by_ref identity--00000000-0000-0000-0000-b8e91df99dc9" + resp = list(apply_common_filters(stix_objs, [filters[10]])) + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[10]])) + assert len(resp) == 1 + + +def test_apply_common_filters10(): + # "Return any object that matches marking-definition--613f2e26-0000-4000-8000-b8e91df99dc9 in object_marking_refs" (None) + resp = list(apply_common_filters(stix_objs, [filters[11]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[11]])) + assert len(resp) == 0 + + +def test_apply_common_filters11(): + # "Return any object that contains description in its selectors" (None) + resp = list(apply_common_filters(stix_objs, [filters[12]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[12]])) + assert len(resp) == 0 + + +def test_apply_common_filters12(): + # "Return any object that matches CVE in source_name" (None, case sensitive) + resp = list(apply_common_filters(stix_objs, [filters[13]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[13]])) + assert len(resp) == 0 + + +def test_apply_common_filters13(): + # Return any object that matches file object in "objects" + resp = list(apply_common_filters(stix_objs, [filters[14]])) + assert resp[0]["id"] == stix_objs[4]["id"] + assert len(resp) == 1 + # important additional check to make sure original File dict was + # not converted to File object. (this was a deep bug found) + assert isinstance(resp[0]["objects"]["0"], dict) + + resp = list(apply_common_filters(real_stix_objs, [filters[14]])) + assert resp[0].id == real_stix_objs[4].id + assert len(resp) == 1 + + +def test_apply_common_filters14(): + # Return any object that contains a specific File Cyber Observable Object + resp = list(apply_common_filters(stix_objs, [filters[15]])) + assert resp[0]['id'] == stix_objs[4]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[15]])) + assert resp[0].id == real_stix_objs[4].id + assert len(resp) == 1 + + +def test_apply_common_filters15(): + # Return any object that contains 'heartbleed' in "labels" + resp = list(apply_common_filters(stix_objs, [filters[16]])) + assert resp[0]['id'] == stix_objs[3]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[16]])) + assert resp[0].id == real_stix_objs[3].id + assert len(resp) == 1 + + +def test_datetime_filter_behavior(): + """if a filter is initialized with its value being a datetime object + OR the STIX object property being filtered on is a datetime object, all + resulting comparisons executed are done on the string representations + of the datetime objects, as the Filter functionality will convert + all datetime objects to there string forms using format_datetim() + + This test makes sure all datetime comparisons are carried out correctly + """ + filter_with_dt_obj = Filter("created", "=", parse_into_datetime("2016-02-14T00:00:00.000Z", "millisecond")) + filter_with_str = Filter("created", "=", "2016-02-14T00:00:00.000Z") + + # compare datetime obj to filter w/ datetime obj + resp = list(apply_common_filters(real_stix_objs, [filter_with_dt_obj])) + assert len(resp) == 1 + assert resp[0]["id"] == "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef" + assert isinstance(resp[0].created, STIXdatetime) # make sure original object not altered + + # compare datetime string to filter w/ str + resp = list(apply_common_filters(stix_objs, [filter_with_str])) + assert len(resp) == 1 + assert resp[0]["id"] == "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef" + + # compare datetime obj to filter w/ str + resp = list(apply_common_filters(real_stix_objs, [filter_with_str])) + assert len(resp) == 1 + assert resp[0]["id"] == "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef" + assert isinstance(resp[0].created, STIXdatetime) # make sure original object not altered + + +def test_filters0(stix_objs2, real_stix_objs2): + # "Return any object modified before 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", "<", "2017-01-28T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[1]['id'] + assert len(resp) == 2 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("modified", "<", parse_into_datetime("2017-01-28T13:49:53.935Z"))])) + assert resp[0].id == real_stix_objs2[1].id + assert len(resp) == 2 + + +def test_filters1(stix_objs2, real_stix_objs2): + # "Return any object modified after 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", ">", "2017-01-28T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("modified", ">", parse_into_datetime("2017-01-28T13:49:53.935Z"))])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 1 + + +def test_filters2(stix_objs2, real_stix_objs2): + # "Return any object modified after or on 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", ">=", "2017-01-27T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 3 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("modified", ">=", parse_into_datetime("2017-01-27T13:49:53.935Z"))])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 3 + + +def test_filters3(stix_objs2, real_stix_objs2): + # "Return any object modified before or on 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", "<=", "2017-01-27T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[1]['id'] + assert len(resp) == 2 + + # "Return any object modified before or on 2017-01-28T13:49:53.935Z" + fv = Filter("modified", "<=", parse_into_datetime("2017-01-27T13:49:53.935Z")) + resp = list(apply_common_filters(real_stix_objs2, [fv])) + assert resp[0].id == real_stix_objs2[1].id + assert len(resp) == 2 + + +def test_filters4(): + # Assert invalid Filter cannot be created + with pytest.raises(ValueError) as excinfo: + Filter("modified", "?", "2017-01-27T13:49:53.935Z") + assert str(excinfo.value) == ( + "Filter operator '?' not supported " + "for specified property: 'modified'" + ) + + +def test_filters5(stix_objs2, real_stix_objs2): + # "Return any object whose id is not indicator--00000000-0000-4000-8000-000000000002" + resp = list(apply_common_filters(stix_objs2, [Filter("id", "!=", "indicator--00000000-0000-4000-8000-000000000002")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("id", "!=", "indicator--00000000-0000-4000-8000-000000000002")])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 1 + + +def test_filters6(stix_objs2, real_stix_objs2): + # Test filtering on non-common property + resp = list(apply_common_filters(stix_objs2, [Filter("name", "=", "Malicious site hosting downloader")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 3 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("name", "=", "Malicious site hosting downloader")])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 3 + + +def test_filters7(stix_objs2, real_stix_objs2): + # Test filtering on embedded property + obsvd_data_obj = { + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T19:58:16.000Z", + "modified": "2016-04-06T19:58:16.000Z", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f", + }, + "extensions": { + "pdf-ext": { + "version": "1.7", + "document_info_dict": { + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02", + }, + "pdfid0": "DFCE52BD827ECF765649852119D", + "pdfid1": "57A1E0F9ED2AE523E313C", + }, + }, + }, + }, + } + + stix_objects = list(stix_objs2) + [obsvd_data_obj] + real_stix_objects = list(real_stix_objs2) + [parse(obsvd_data_obj)] + + resp = list(apply_common_filters(stix_objects, [Filter("objects.0.extensions.pdf-ext.version", ">", "1.2")])) + assert resp[0]['id'] == stix_objects[3]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objects, [Filter("objects.0.extensions.pdf-ext.version", ">", "1.2")])) + assert resp[0].id == real_stix_objects[3].id + assert len(resp) == 1 diff --git a/stix2/test/v21/test_datastore_memory.py b/stix2/test/v21/test_datastore_memory.py new file mode 100644 index 0000000..eb30f07 --- /dev/null +++ b/stix2/test/v21/test_datastore_memory.py @@ -0,0 +1,432 @@ +import os +import shutil + +import pytest + +from stix2 import Filter, MemorySource, MemoryStore, properties +from stix2.datastore import make_id +from stix2.utils import parse_into_datetime +from stix2.v21 import ( + Bundle, Campaign, CustomObject, Identity, Indicator, Malware, Relationship, +) + +from .constants import ( + CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, INDICATOR_ID, + INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS, +) + +IND1 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} +IND2 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} +IND3 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.936Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} +IND4 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} +IND5 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} +IND6 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000001", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-31T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} +IND7 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} +IND8 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--00000000-0000-4000-8000-000000000002", + "indicator_types": [ + "url-watchlist", + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "spec_version": "2.1", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z", +} + +STIX_OBJS2 = [IND6, IND7, IND8] +STIX_OBJS1 = [IND1, IND2, IND3, IND4, IND5] + + +@pytest.fixture +def mem_store(): + yield MemoryStore(STIX_OBJS1) + + +@pytest.fixture +def mem_source(): + yield MemorySource(STIX_OBJS1) + + +@pytest.fixture +def rel_mem_store(): + cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] + yield MemoryStore(stix_objs) + + +@pytest.fixture +def fs_mem_store(request, mem_store): + filename = mem_store.save_to_file('memory_test/mem_store.json') + + def fin(): + # teardown, executed regardless of exception + shutil.rmtree(os.path.dirname(filename)) + request.addfinalizer(fin) + + return filename + + +@pytest.fixture +def fs_mem_store_no_name(request, mem_store): + filename = mem_store.save_to_file('memory_test/') + + def fin(): + # teardown, executed regardless of exception + shutil.rmtree(os.path.dirname(filename)) + request.addfinalizer(fin) + + return filename + + +def test_memory_source_get(mem_source): + resp = mem_source.get("indicator--00000000-0000-4000-8000-000000000001") + assert resp["id"] == "indicator--00000000-0000-4000-8000-000000000001" + + +def test_memory_source_get_nonexistant_object(mem_source): + resp = mem_source.get("tool--8d0b222c-7a3b-44a0-b9c6-31b051efb32e") + assert resp is None + + +def test_memory_store_all_versions(mem_store): + # Add bundle of items to sink + mem_store.add(dict( + id="bundle--%s" % make_id(), + objects=STIX_OBJS2, + type="bundle", + )) + + resp = mem_store.all_versions("indicator--00000000-0000-4000-8000-000000000001") + assert len(resp) == 3 + + +def test_memory_store_query(mem_store): + query = [Filter('type', '=', 'malware')] + resp = mem_store.query(query) + assert len(resp) == 0 + + +def test_memory_store_query_single_filter(mem_store): + query = Filter('id', '=', 'indicator--00000000-0000-4000-8000-000000000001') + resp = mem_store.query(query) + assert len(resp) == 2 + + +def test_memory_store_query_empty_query(mem_store): + resp = mem_store.query() + # sort since returned in random order + resp = sorted(resp, key=lambda k: (k['id'], k['modified'])) + assert len(resp) == 3 + assert resp[0]['id'] == 'indicator--00000000-0000-4000-8000-000000000001' + assert resp[0]['modified'] == parse_into_datetime('2017-01-27T13:49:53.935Z') + assert resp[1]['id'] == 'indicator--00000000-0000-4000-8000-000000000001' + assert resp[1]['modified'] == parse_into_datetime('2017-01-27T13:49:53.936Z') + assert resp[2]['id'] == 'indicator--00000000-0000-4000-8000-000000000002' + assert resp[2]['modified'] == parse_into_datetime('2017-01-27T13:49:53.935Z') + + +def test_memory_store_query_multiple_filters(mem_store): + mem_store.source.filters.add(Filter('type', '=', 'indicator')) + query = Filter('id', '=', 'indicator--00000000-0000-4000-8000-000000000001') + resp = mem_store.query(query) + assert len(resp) == 2 + + +def test_memory_store_save_load_file(fs_mem_store): + filename = fs_mem_store # the fixture fs_mem_store yields filename where the memory store was written to + + # STIX2 contents of mem_store have already been written to file + # (this is done in fixture 'fs_mem_store'), so can already read-in here + contents = open(os.path.abspath(filename)).read() + + assert '"id": "indicator--00000000-0000-4000-8000-000000000001",' in contents + assert '"id": "indicator--00000000-0000-4000-8000-000000000001",' in contents + + mem_store2 = MemoryStore() + mem_store2.load_from_file(filename) + assert mem_store2.get("indicator--00000000-0000-4000-8000-000000000001") + assert mem_store2.get("indicator--00000000-0000-4000-8000-000000000001") + + +def test_memory_store_save_load_file_no_name_provided(fs_mem_store_no_name): + filename = fs_mem_store_no_name # the fixture fs_mem_store yields filename where the memory store was written to + + # STIX2 contents of mem_store have already been written to file + # (this is done in fixture 'fs_mem_store'), so can already read-in here + contents = open(os.path.abspath(filename)).read() + + assert '"id": "indicator--00000000-0000-4000-8000-000000000001",' in contents + assert '"id": "indicator--00000000-0000-4000-8000-000000000001",' in contents + + mem_store2 = MemoryStore() + mem_store2.load_from_file(filename) + assert mem_store2.get("indicator--00000000-0000-4000-8000-000000000001") + assert mem_store2.get("indicator--00000000-0000-4000-8000-000000000001") + + +def test_memory_store_add_invalid_object(mem_store): + ind = ('indicator', IND1) # tuple isn't valid + with pytest.raises(TypeError): + mem_store.add(ind) + + +def test_memory_store_object_with_custom_property(mem_store): + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) + + mem_store.add(camp) + + camp_r = mem_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_memory_store_object_creator_of_present(mem_store): + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + created_by_ref="identity--e4196283-7420-4277-a7a3-d57f61ef1389", + x_empire="Roman", + allow_custom=True, + ) + + iden = Identity( + id="identity--e4196283-7420-4277-a7a3-d57f61ef1389", + name="Foo Corp.", + identity_class="corporation", + ) + + mem_store.add(camp) + mem_store.add(iden) + + camp_r = mem_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + assert mem_store.creator_of(camp_r) == iden + + +def test_memory_store_object_creator_of_missing(mem_store): + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) + + mem_store.add(camp) + + camp_r = mem_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + assert mem_store.creator_of(camp) is None + + +def test_memory_store_object_with_custom_property_in_bundle(mem_store): + camp = Campaign( + name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True, + ) + + bundle = Bundle(camp, allow_custom=True) + mem_store.add(bundle) + + camp_r = mem_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_memory_store_custom_object(mem_store): + @CustomObject( + 'x-new-obj', [ + ('property1', properties.StringProperty(required=True)), + ], + ) + class NewObj(): + pass + + newobj = NewObj(property1='something') + mem_store.add(newobj) + + newobj_r = mem_store.get(newobj.id) + assert newobj_r.id == newobj.id + assert newobj_r.property1 == 'something' + + +def test_relationships(rel_mem_store): + mal = rel_mem_store.get(MALWARE_ID) + resp = rel_mem_store.relationships(mal) + + assert len(resp) == 3 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[1] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_type(rel_mem_store): + mal = rel_mem_store.get(MALWARE_ID) + resp = rel_mem_store.relationships(mal, relationship_type='indicates') + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[0] + + +def test_relationships_by_source(rel_mem_store): + resp = rel_mem_store.relationships(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[1] + + +def test_relationships_by_target(rel_mem_store): + resp = rel_mem_store.relationships(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_type(rel_mem_store): + resp = rel_mem_store.relationships(MALWARE_ID, relationship_type='uses', target_only=True) + + assert len(resp) == 1 + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_source(rel_mem_store): + with pytest.raises(ValueError) as excinfo: + rel_mem_store.relationships(MALWARE_ID, target_only=True, source_only=True) + + assert 'not both' in str(excinfo.value) + + +def test_related_to(rel_mem_store): + mal = rel_mem_store.get(MALWARE_ID) + resp = rel_mem_store.related_to(mal) + + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_source(rel_mem_store): + resp = rel_mem_store.related_to(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_target(rel_mem_store): + resp = rel_mem_store.related_to(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + + +def test_object_family_internal_components(mem_source): + # Testing internal components. + str_representation = str(mem_source._data['indicator--00000000-0000-4000-8000-000000000001']) + repr_representation = repr(mem_source._data['indicator--00000000-0000-4000-8000-000000000001']) + + assert "latest=2017-01-27 13:49:53.936000+00:00>>" in str_representation + assert "latest=2017-01-27 13:49:53.936000+00:00>>" in repr_representation diff --git a/stix2/test/v21/test_datastore_taxii.py b/stix2/test/v21/test_datastore_taxii.py new file mode 100644 index 0000000..528546a --- /dev/null +++ b/stix2/test/v21/test_datastore_taxii.py @@ -0,0 +1,423 @@ +import json + +from medallion.filters.basic_filter import BasicFilter +import pytest +from requests.models import Response +import six +from taxii2client import Collection, _filter_kwargs_to_query_params + +import stix2 +from stix2.datastore import DataSourceError +from stix2.datastore.filters import Filter + +COLLECTION_URL = 'https://example.com/api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/' + + +class MockTAXIICollectionEndpoint(Collection): + """Mock for taxii2_client.TAXIIClient""" + + def __init__(self, url, collection_info): + super(MockTAXIICollectionEndpoint, self).__init__( + url, collection_info=collection_info, + ) + self.objects = [] + + def add_objects(self, bundle): + self._verify_can_write() + if isinstance(bundle, six.string_types): + bundle = json.loads(bundle, encoding='utf-8') + for object in bundle.get("objects", []): + self.objects.append(object) + + def get_objects(self, **filter_kwargs): + self._verify_can_read() + query_params = _filter_kwargs_to_query_params(filter_kwargs) + assert isinstance(query_params, dict) + full_filter = BasicFilter(query_params) + objs = full_filter.process_filter( + self.objects, + ("id", "type", "version"), + [], + ) + if objs: + return stix2.v21.Bundle(objects=objs) + else: + resp = Response() + resp.status_code = 404 + resp.raise_for_status() + + def get_object(self, id, **filter_kwargs): + self._verify_can_read() + query_params = _filter_kwargs_to_query_params(filter_kwargs) + assert isinstance(query_params, dict) + full_filter = BasicFilter(query_params) + + # In this endpoint we must first filter objects by id beforehand. + objects = [x for x in self.objects if x["id"] == id] + if objects: + filtered_objects = full_filter.process_filter( + objects, + ("version",), + [], + ) + else: + filtered_objects = [] + if filtered_objects: + return stix2.v21.Bundle(objects=filtered_objects) + else: + resp = Response() + resp.status_code = 404 + resp.raise_for_status() + + +@pytest.fixture +def collection(stix_objs1): + mock = MockTAXIICollectionEndpoint( + COLLECTION_URL, { + "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", + "title": "Writable Collection", + "description": "This collection is a dropbox for submitting indicators", + "can_read": True, + "can_write": True, + "media_types": [ + "application/vnd.oasis.stix+json; version=2.0", + ], + }, + ) + + mock.objects.extend(stix_objs1) + return mock + + +@pytest.fixture +def collection_no_rw_access(stix_objs1): + mock = MockTAXIICollectionEndpoint( + COLLECTION_URL, { + "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", + "title": "Not writeable or readable Collection", + "description": "This collection is a dropbox for submitting indicators", + "can_read": False, + "can_write": False, + "media_types": [ + "application/vnd.oasis.stix+json; version=2.0", + ], + }, + ) + + mock.objects.extend(stix_objs1) + return mock + + +def test_ds_taxii(collection): + ds = stix2.TAXIICollectionSource(collection) + assert ds.collection is not None + + +def test_add_stix2_object(collection): + tc_sink = stix2.TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = stix2.v21.ThreatActor( + name="Teddy Bear", + threat_actor_types=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + ) + + tc_sink.add(ta) + + +def test_add_stix2_with_custom_object(collection): + tc_sink = stix2.TAXIICollectionStore(collection, allow_custom=True) + + # create new STIX threat-actor + ta = stix2.v21.ThreatActor( + name="Teddy Bear", + threat_actor_types=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + foo="bar", + allow_custom=True, + ) + + tc_sink.add(ta) + + +def test_add_list_object(collection, indicator): + tc_sink = stix2.TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = stix2.v21.ThreatActor( + name="Teddy Bear", + threat_actor_types=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + ) + + tc_sink.add([ta, indicator]) + + +def test_get_object_found(collection): + tc_source = stix2.TAXIICollectionSource(collection) + result = tc_source.query([ + stix2.Filter("id", "=", "indicator--00000000-0000-4000-8000-000000000001"), + ]) + assert result + + +def test_get_object_not_found(collection): + tc_source = stix2.TAXIICollectionSource(collection) + result = tc_source.get("indicator--00000000-0000-4000-8000-000000000012") + assert result is None + + +def test_add_stix2_bundle_object(collection): + tc_sink = stix2.TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = stix2.v21.ThreatActor( + name="Teddy Bear", + threat_actor_types=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + ) + + tc_sink.add(stix2.v21.Bundle(objects=[ta])) + + +def test_add_str_object(collection): + tc_sink = stix2.TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = """{ + "type": "threat-actor", + "spec_version": "2.1", + "id": "threat-actor--eddff64f-feb1-4469-b07c-499a73c96415", + "created": "2018-04-23T16:40:50.847Z", + "modified": "2018-04-23T16:40:50.847Z", + "name": "Teddy Bear", + "threat_actor_types": [ + "nation-state" + ], + "goals": [ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector" + ], + "sophistication": "innovator", + "resource_level": "government" + }""" + + tc_sink.add(ta) + + +def test_add_dict_object(collection): + tc_sink = stix2.TAXIICollectionSink(collection) + + ta = { + "type": "threat-actor", + "spec_version": "2.1", + "id": "threat-actor--eddff64f-feb1-4469-b07c-499a73c96415", + "created": "2018-04-23T16:40:50.847Z", + "modified": "2018-04-23T16:40:50.847Z", + "name": "Teddy Bear", + "goals": [ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + "sophistication": "innovator", + "resource_level": "government", + "threat_actor_types": [ + "nation-state", + ], + } + + tc_sink.add(ta) + + +def test_add_dict_bundle_object(collection): + tc_sink = stix2.TAXIICollectionSink(collection) + + ta = { + "type": "bundle", + "id": "bundle--860ccc8d-56c9-4fda-9384-84276fb52fb1", + "objects": [ + { + "type": "threat-actor", + "spec_version": "2.1", + "id": "threat-actor--dc5a2f41-f76e-425a-81fe-33afc7aabd75", + "created": "2018-04-23T18:45:11.390Z", + "modified": "2018-04-23T18:45:11.390Z", + "name": "Teddy Bear", + "goals": [ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + "sophistication": "innovator", + "resource_level": "government", + "threat_actor_types": [ + "nation-state", + ], + }, + ], + } + + tc_sink.add(ta) + + +def test_get_stix2_object(collection): + tc_sink = stix2.TAXIICollectionSource(collection) + + objects = tc_sink.get("indicator--00000000-0000-4000-8000-000000000001") + + assert objects + + +def test_parse_taxii_filters(collection): + query = [ + Filter("added_after", "=", "2016-02-01T00:00:01.000Z"), + Filter("id", "=", "taxii stix object ID"), + Filter("type", "=", "taxii stix object ID"), + Filter("version", "=", "first"), + Filter("created_by_ref", "=", "Bane"), + ] + + taxii_filters_expected = [ + Filter("added_after", "=", "2016-02-01T00:00:01.000Z"), + Filter("id", "=", "taxii stix object ID"), + Filter("type", "=", "taxii stix object ID"), + Filter("version", "=", "first"), + ] + + ds = stix2.TAXIICollectionSource(collection) + + taxii_filters = ds._parse_taxii_filters(query) + + assert taxii_filters == taxii_filters_expected + + +def test_add_get_remove_filter(collection): + ds = stix2.TAXIICollectionSource(collection) + + # First 3 filters are valid, remaining properties are erroneous in some way + valid_filters = [ + Filter('type', '=', 'malware'), + Filter('id', '!=', 'stix object id'), + Filter('threat_actor_types', 'in', ["heartbleed", "malicious-activity"]), + ] + + assert len(ds.filters) == 0 + + ds.filters.add(valid_filters[0]) + assert len(ds.filters) == 1 + + # Addin the same filter again will have no effect since `filters` acts + # like a set + ds.filters.add(valid_filters[0]) + assert len(ds.filters) == 1 + + ds.filters.add(valid_filters[1]) + assert len(ds.filters) == 2 + + ds.filters.add(valid_filters[2]) + assert len(ds.filters) == 3 + + assert valid_filters == [f for f in ds.filters] + + # remove + ds.filters.remove(valid_filters[0]) + + assert len(ds.filters) == 2 + + ds.filters.add(valid_filters) + + +def test_get_all_versions(collection): + ds = stix2.TAXIICollectionStore(collection) + + indicators = ds.all_versions('indicator--00000000-0000-4000-8000-000000000001') + # There are 3 indicators but 2 share the same 'modified' timestamp + assert len(indicators) == 2 + + +def test_can_read_error(collection_no_rw_access): + """create a TAXIICOllectionSource with a taxii2client.Collection + instance that does not have read access, check ValueError exception is raised""" + + with pytest.raises(DataSourceError) as excinfo: + stix2.TAXIICollectionSource(collection_no_rw_access) + assert "Collection object provided does not have read access" in str(excinfo.value) + + +def test_can_write_error(collection_no_rw_access): + """create a TAXIICOllectionSink with a taxii2client.Collection + instance that does not have write access, check ValueError exception is raised""" + + with pytest.raises(DataSourceError) as excinfo: + stix2.TAXIICollectionSink(collection_no_rw_access) + assert "Collection object provided does not have write access" in str(excinfo.value) + + +def test_get_404(): + """a TAXIICollectionSource.get() call that receives an HTTP 404 response + code from the taxii2client should be be returned as None. + + TAXII spec states that a TAXII server can return a 404 for nonexistent + resources or lack of access. Decided that None is acceptable reponse + to imply that state of the TAXII endpoint. + """ + + class TAXIICollection404(): + can_read = True + + def get_object(self, id, version=None): + resp = Response() + resp.status_code = 404 + resp.raise_for_status() + + ds = stix2.TAXIICollectionSource(TAXIICollection404()) + + # this will raise 404 from mock TAXII Client but TAXIICollectionStore + # should handle gracefully and return None + stix_obj = ds.get("indicator--1") + assert stix_obj is None + + +def test_all_versions_404(collection): + """ a TAXIICollectionSource.all_version() call that recieves an HTTP 404 + response code from the taxii2client should be returned as an exception""" + + ds = stix2.TAXIICollectionStore(collection) + + with pytest.raises(DataSourceError) as excinfo: + ds.all_versions("indicator--1") + assert "are either not found or access is denied" in str(excinfo.value) + assert "404" in str(excinfo.value) + + +def test_query_404(collection): + """ a TAXIICollectionSource.query() call that recieves an HTTP 404 + response code from the taxii2client should be returned as an exception""" + + ds = stix2.TAXIICollectionStore(collection) + query = [Filter("type", "=", "malware")] + + with pytest.raises(DataSourceError) as excinfo: + ds.query(query=query) + assert "are either not found or access is denied" in str(excinfo.value) + assert "404" in str(excinfo.value) diff --git a/stix2/test/v21/test_environment.py b/stix2/test/v21/test_environment.py new file mode 100644 index 0000000..e08971e --- /dev/null +++ b/stix2/test/v21/test_environment.py @@ -0,0 +1,374 @@ +import pytest + +import stix2 + +from .constants import ( + CAMPAIGN_ID, CAMPAIGN_KWARGS, FAKE_TIME, IDENTITY_ID, IDENTITY_KWARGS, + INDICATOR_ID, INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, + RELATIONSHIP_IDS, +) + + +@pytest.fixture +def ds(): + cam = stix2.v21.Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = stix2.v21.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = stix2.v21.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = stix2.v21.Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = stix2.v21.Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = stix2.v21.Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = stix2.v21.Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] + yield stix2.MemoryStore(stix_objs) + + +def test_object_factory_created_by_ref_str(): + factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID) + ind = factory.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + assert ind.created_by_ref == IDENTITY_ID + + +def test_object_factory_created_by_ref_obj(): + id_obj = stix2.v21.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + factory = stix2.ObjectFactory(created_by_ref=id_obj) + ind = factory.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + assert ind.created_by_ref == IDENTITY_ID + + +def test_object_factory_override_default(): + factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID) + new_id = "identity--983b3172-44fe-4a80-8091-eb8098841fe8" + ind = factory.create(stix2.v21.Indicator, created_by_ref=new_id, **INDICATOR_KWARGS) + assert ind.created_by_ref == new_id + + +def test_object_factory_created(): + factory = stix2.ObjectFactory(created=FAKE_TIME) + ind = factory.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + assert ind.created == FAKE_TIME + assert ind.modified == FAKE_TIME + + +def test_object_factory_external_reference(): + ext_ref = stix2.v21.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + ) + factory = stix2.ObjectFactory(external_references=ext_ref) + ind = factory.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + assert ind.external_references[0].source_name == "ACME Threat Intel" + assert ind.external_references[0].description == "Threat report" + + ind2 = factory.create(stix2.v21.Indicator, external_references=None, **INDICATOR_KWARGS) + assert 'external_references' not in ind2 + + +def test_object_factory_obj_markings(): + stmt_marking = stix2.v21.StatementMarking("Copyright 2016, Example Corp") + mark_def = stix2.v21.MarkingDefinition( + definition_type="statement", + definition=stmt_marking, + ) + factory = stix2.ObjectFactory(object_marking_refs=[mark_def, stix2.v21.TLP_AMBER]) + ind = factory.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + assert mark_def.id in ind.object_marking_refs + assert stix2.v21.TLP_AMBER.id in ind.object_marking_refs + + factory = stix2.ObjectFactory(object_marking_refs=stix2.v21.TLP_RED) + ind = factory.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + assert stix2.v21.TLP_RED.id in ind.object_marking_refs + + +def test_object_factory_list_append(): + ext_ref = stix2.v21.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report from ACME", + ) + ext_ref2 = stix2.v21.ExternalReference( + source_name="Yet Another Threat Report", + description="Threat report from YATR", + ) + ext_ref3 = stix2.v21.ExternalReference( + source_name="Threat Report #3", + description="One more threat report", + ) + factory = stix2.ObjectFactory(external_references=ext_ref) + ind = factory.create(stix2.v21.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS) + assert ind.external_references[1].source_name == "Yet Another Threat Report" + + ind = factory.create(stix2.v21.Indicator, external_references=[ext_ref2, ext_ref3], **INDICATOR_KWARGS) + assert ind.external_references[2].source_name == "Threat Report #3" + + +def test_object_factory_list_replace(): + ext_ref = stix2.v21.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report from ACME", + ) + ext_ref2 = stix2.v21.ExternalReference( + source_name="Yet Another Threat Report", + description="Threat report from YATR", + ) + factory = stix2.ObjectFactory(external_references=ext_ref, list_append=False) + ind = factory.create(stix2.v21.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS) + assert len(ind.external_references) == 1 + assert ind.external_references[0].source_name == "Yet Another Threat Report" + + +def test_environment_functions(): + env = stix2.Environment( + stix2.ObjectFactory(created_by_ref=IDENTITY_ID), + stix2.MemoryStore(), + ) + + # Create a STIX object + ind = env.create(stix2.v21.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + assert ind.created_by_ref == IDENTITY_ID + + # Add objects to datastore + ind2 = ind.new_version(labels=['benign']) + env.add([ind, ind2]) + + # Get both versions of the object + resp = env.all_versions(INDICATOR_ID) + assert len(resp) == 2 + + # Get just the most recent version of the object + resp = env.get(INDICATOR_ID) + assert resp['labels'][0] == 'benign' + + # Search on something other than id + query = [stix2.Filter('type', '=', 'vulnerability')] + resp = env.query(query) + assert len(resp) == 0 + + # See different results after adding filters to the environment + env.add_filters([ + stix2.Filter('type', '=', 'indicator'), + stix2.Filter('created_by_ref', '=', IDENTITY_ID), + ]) + env.add_filter(stix2.Filter('labels', '=', 'benign')) # should be 'malicious-activity' + resp = env.get(INDICATOR_ID) + assert resp['labels'][0] == 'benign' # should be 'malicious-activity' + + +def test_environment_source_and_sink(): + ind = stix2.v21.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + env = stix2.Environment(source=stix2.MemorySource([ind]), sink=stix2.MemorySink([ind])) + assert env.get(INDICATOR_ID).indicator_types[0] == 'malicious-activity' + + +def test_environment_datastore_and_sink(): + with pytest.raises(ValueError) as excinfo: + stix2.Environment( + factory=stix2.ObjectFactory(), + store=stix2.MemoryStore(), sink=stix2.MemorySink, + ) + assert 'Data store already provided' in str(excinfo.value) + + +def test_environment_no_datastore(): + env = stix2.Environment(factory=stix2.ObjectFactory()) + + with pytest.raises(AttributeError) as excinfo: + env.add(stix2.v21.Indicator(**INDICATOR_KWARGS)) + assert 'Environment has no data sink to put objects in' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.get(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.all_versions(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.query(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.relationships(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.related_to(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + +def test_environment_add_filters(): + env = stix2.Environment(factory=stix2.ObjectFactory()) + env.add_filters([INDICATOR_ID]) + env.add_filter(INDICATOR_ID) + + +def test_environment_datastore_and_no_object_factory(): + # Uses a default object factory + env = stix2.Environment(store=stix2.MemoryStore()) + ind = env.create(stix2.v21.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + assert ind.id == INDICATOR_ID + + +def test_parse_malware(): + env = stix2.Environment() + data = """{ + "type": "malware", + "spec_version": "2.1", + "id": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "malware_types": [ + "ransomware" + ] + }""" + mal = env.parse(data, version="2.1") + + assert mal.type == 'malware' + assert mal.spec_version == '2.1' + assert mal.id == MALWARE_ID + assert mal.created == FAKE_TIME + assert mal.modified == FAKE_TIME + assert mal.malware_types == ['ransomware'] + assert mal.name == "Cryptolocker" + + +def test_creator_of(): + identity = stix2.v21.Identity(**IDENTITY_KWARGS) + factory = stix2.ObjectFactory(created_by_ref=identity.id) + env = stix2.Environment(store=stix2.MemoryStore(), factory=factory) + env.add(identity) + + ind = env.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + creator = env.creator_of(ind) + assert creator is identity + + +def test_creator_of_no_datasource(): + identity = stix2.v21.Identity(**IDENTITY_KWARGS) + factory = stix2.ObjectFactory(created_by_ref=identity.id) + env = stix2.Environment(factory=factory) + + ind = env.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + with pytest.raises(AttributeError) as excinfo: + env.creator_of(ind) + assert 'Environment has no data source' in str(excinfo.value) + + +def test_creator_of_not_found(): + identity = stix2.v21.Identity(**IDENTITY_KWARGS) + factory = stix2.ObjectFactory(created_by_ref=identity.id) + env = stix2.Environment(store=stix2.MemoryStore(), factory=factory) + + ind = env.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + creator = env.creator_of(ind) + assert creator is None + + +def test_creator_of_no_created_by_ref(): + env = stix2.Environment(store=stix2.MemoryStore()) + ind = env.create(stix2.v21.Indicator, **INDICATOR_KWARGS) + creator = env.creator_of(ind) + assert creator is None + + +def test_relationships(ds): + env = stix2.Environment(store=ds) + mal = env.get(MALWARE_ID) + resp = env.relationships(mal) + + assert len(resp) == 3 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[1] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_no_id(ds): + env = stix2.Environment(store=ds) + mal = { + "type": "malware", + "name": "some variant", + } + with pytest.raises(ValueError) as excinfo: + env.relationships(mal) + assert "object has no 'id' property" in str(excinfo.value) + + +def test_relationships_by_type(ds): + env = stix2.Environment(store=ds) + mal = env.get(MALWARE_ID) + resp = env.relationships(mal, relationship_type='indicates') + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[0] + + +def test_relationships_by_source(ds): + env = stix2.Environment(store=ds) + resp = env.relationships(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[1] + + +def test_relationships_by_target(ds): + env = stix2.Environment(store=ds) + resp = env.relationships(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_type(ds): + env = stix2.Environment(store=ds) + resp = env.relationships(MALWARE_ID, relationship_type='uses', target_only=True) + + assert len(resp) == 1 + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_source(ds): + env = stix2.Environment(store=ds) + with pytest.raises(ValueError) as excinfo: + env.relationships(MALWARE_ID, target_only=True, source_only=True) + + assert 'not both' in str(excinfo.value) + + +def test_related_to(ds): + env = stix2.Environment(store=ds) + mal = env.get(MALWARE_ID) + resp = env.related_to(mal) + + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_no_id(ds): + env = stix2.Environment(store=ds) + mal = { + "type": "malware", + "name": "some variant", + } + with pytest.raises(ValueError) as excinfo: + env.related_to(mal) + assert "object has no 'id' property" in str(excinfo.value) + + +def test_related_to_by_source(ds): + env = stix2.Environment(store=ds) + resp = env.related_to(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == IDENTITY_ID + + +def test_related_to_by_target(ds): + env = stix2.Environment(store=ds) + resp = env.related_to(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) diff --git a/stix2/test/v21/test_external_reference.py b/stix2/test/v21/test_external_reference.py new file mode 100644 index 0000000..d192a11 --- /dev/null +++ b/stix2/test/v21/test_external_reference.py @@ -0,0 +1,122 @@ +"""Tests for stix.ExternalReference""" + +import re + +import pytest + +import stix2 + +VERIS = """{ + "source_name": "veris", + "url": "https://github.com/vz-risk/VCDB/blob/master/data/json/0001AA7F-C601-424A-B2B8-BE6C9F5164E7.json", + "hashes": { + "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b" + }, + "external_id": "0001AA7F-C601-424A-B2B8-BE6C9F5164E7" +}""" + + +def test_external_reference_veris(): + ref = stix2.v21.ExternalReference( + source_name="veris", + external_id="0001AA7F-C601-424A-B2B8-BE6C9F5164E7", + hashes={ + "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b", + }, + url="https://github.com/vz-risk/VCDB/blob/master/data/json/0001AA7F-C601-424A-B2B8-BE6C9F5164E7.json", + ) + + assert str(ref) == VERIS + + +CAPEC = """{ + "source_name": "capec", + "external_id": "CAPEC-550" +}""" + + +def test_external_reference_capec(): + ref = stix2.v21.ExternalReference( + source_name="capec", + external_id="CAPEC-550", + ) + + assert str(ref) == CAPEC + assert re.match("ExternalReference\\(source_name=u?'capec', external_id=u?'CAPEC-550'\\)", repr(ref)) + + +CAPEC_URL = """{ + "source_name": "capec", + "url": "http://capec.mitre.org/data/definitions/550.html", + "external_id": "CAPEC-550" +}""" + + +def test_external_reference_capec_url(): + ref = stix2.v21.ExternalReference( + source_name="capec", + external_id="CAPEC-550", + url="http://capec.mitre.org/data/definitions/550.html", + ) + + assert str(ref) == CAPEC_URL + + +THREAT_REPORT = """{ + "source_name": "ACME Threat Intel", + "description": "Threat report", + "url": "http://www.example.com/threat-report.pdf" +}""" + + +def test_external_reference_threat_report(): + ref = stix2.v21.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + url="http://www.example.com/threat-report.pdf", + ) + + assert str(ref) == THREAT_REPORT + + +BUGZILLA = """{ + "source_name": "ACME Bugzilla", + "url": "https://www.example.com/bugs/1370", + "external_id": "1370" +}""" + + +def test_external_reference_bugzilla(): + ref = stix2.v21.ExternalReference( + source_name="ACME Bugzilla", + external_id="1370", + url="https://www.example.com/bugs/1370", + ) + + assert str(ref) == BUGZILLA + + +OFFLINE = """{ + "source_name": "ACME Threat Intel", + "description": "Threat report" +}""" + + +def test_external_reference_offline(): + ref = stix2.v21.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + ) + + assert str(ref) == OFFLINE + assert re.match("ExternalReference\\(source_name=u?'ACME Threat Intel', description=u?'Threat report'\\)", repr(ref)) + # Yikes! This works + assert eval("stix2." + repr(ref)) == ref + + +def test_external_reference_source_required(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.ExternalReference() + + assert excinfo.value.cls == stix2.v21.ExternalReference + assert excinfo.value.properties == ["source_name"] diff --git a/stix2/test/v21/test_fixtures.py b/stix2/test/v21/test_fixtures.py new file mode 100644 index 0000000..23ad8a4 --- /dev/null +++ b/stix2/test/v21/test_fixtures.py @@ -0,0 +1,18 @@ +import uuid + +from stix2 import utils + +from .constants import FAKE_TIME + + +def test_clock(clock): + assert utils.STIXdatetime.now() == FAKE_TIME + + +def test_my_uuid4_fixture(uuid4): + assert uuid.uuid4() == "00000000-0000-4000-8000-000000000001" + assert uuid.uuid4() == "00000000-0000-4000-8000-000000000002" + assert uuid.uuid4() == "00000000-0000-4000-8000-000000000003" + for _ in range(256): + uuid.uuid4() + assert uuid.uuid4() == "00000000-0000-4000-8000-000000000104" diff --git a/stix2/test/v21/test_granular_markings.py b/stix2/test/v21/test_granular_markings.py new file mode 100644 index 0000000..9f7234e --- /dev/null +++ b/stix2/test/v21/test_granular_markings.py @@ -0,0 +1,1090 @@ +import pytest + +from stix2 import markings +from stix2.exceptions import MarkingNotFoundError +from stix2.v21 import TLP_RED, Malware + +from .constants import MALWARE_MORE_KWARGS as MALWARE_KWARGS_CONST +from .constants import MARKING_IDS + +"""Tests for the Data Markings API.""" + +MALWARE_KWARGS = MALWARE_KWARGS_CONST.copy() + + +def test_add_marking_mark_one_selector_multiple_refs(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +@pytest.mark.parametrize( + "data", [ + ( + Malware(**MALWARE_KWARGS), + Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + MALWARE_KWARGS, + dict( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + Malware(**MALWARE_KWARGS), + Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": TLP_RED.id, + }, + ], + **MALWARE_KWARGS + ), + TLP_RED, + ), + ], +) +def test_add_marking_mark_multiple_selector_one_refs(data): + before = data[0] + after = data[1] + + before = markings.add_markings(before, data[2], ["description", "name"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_add_marking_mark_multiple_selector_multiple_refs(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description", "name"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_add_marking_mark_another_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0]], ["name"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_add_marking_mark_same_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0]], ["description"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +@pytest.mark.parametrize( + "data,marking", [ + ( + {"description": "test description"}, + [ + ["title"], ["marking-definition--1", "marking-definition--2"], + "", ["marking-definition--1", "marking-definition--2"], + [], ["marking-definition--1", "marking-definition--2"], + [""], ["marking-definition--1", "marking-definition--2"], + ["description"], [""], + ["description"], [], + ["description"], ["marking-definition--1", 456], + ], + ), + ], +) +def test_add_marking_bad_selector(data, marking): + with pytest.raises(AssertionError): + markings.add_markings(data, marking[0], marking[1]) + + +GET_MARKINGS_TEST_DATA = { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45, + }, + ], + "x": { + "y": [ + "hello", + 88, + ], + "z": { + "foo1": "bar", + "foo2": 65, + }, + }, + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"], + }, + { + "marking_ref": "2", + "selectors": ["c"], + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"], + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"], + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"], + }, + { + "marking_ref": "6", + "selectors": ["x"], + }, + { + "marking_ref": "7", + "selectors": ["x.y"], + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"], + }, + { + "marking_ref": "9", + "selectors": ["x.z"], + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"], + }, + ], +} + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_smoke(data): + """Test get_markings does not fail.""" + assert len(markings.get_markings(data, "a")) >= 1 + assert markings.get_markings(data, "a") == ["1"] + + +@pytest.mark.parametrize( + "data", [ + GET_MARKINGS_TEST_DATA, + {"b": 1234}, + ], +) +def test_get_markings_not_marked(data): + """Test selector that is not marked returns empty list.""" + results = markings.get_markings(data, "b") + assert len(results) == 0 + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_multiple_selectors(data): + """Test multiple selectors return combination of markings.""" + total = markings.get_markings(data, ["x.y", "x.z"]) + xy_markings = markings.get_markings(data, ["x.y"]) + xz_markings = markings.get_markings(data, ["x.z"]) + + assert set(xy_markings).issubset(total) + assert set(xz_markings).issubset(total) + assert set(xy_markings).union(xz_markings).issuperset(total) + + +@pytest.mark.parametrize( + "data,selector", [ + (GET_MARKINGS_TEST_DATA, "foo"), + (GET_MARKINGS_TEST_DATA, ""), + (GET_MARKINGS_TEST_DATA, []), + (GET_MARKINGS_TEST_DATA, [""]), + (GET_MARKINGS_TEST_DATA, "x.z.[-2]"), + (GET_MARKINGS_TEST_DATA, "c.f"), + (GET_MARKINGS_TEST_DATA, "c.[2].i"), + (GET_MARKINGS_TEST_DATA, "c.[3]"), + (GET_MARKINGS_TEST_DATA, "d"), + (GET_MARKINGS_TEST_DATA, "x.[0]"), + (GET_MARKINGS_TEST_DATA, "z.y.w"), + (GET_MARKINGS_TEST_DATA, "x.z.[1]"), + (GET_MARKINGS_TEST_DATA, "x.z.foo3"), + ], +) +def test_get_markings_bad_selector(data, selector): + """Test bad selectors raise exception""" + with pytest.raises(AssertionError): + markings.get_markings(data, selector) + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_positional_arguments_combinations(data): + """Test multiple combinations for inherited and descendant markings.""" + assert set(markings.get_markings(data, "a", False, False)) == set(["1"]) + assert set(markings.get_markings(data, "a", True, False)) == set(["1"]) + assert set(markings.get_markings(data, "a", True, True)) == set(["1"]) + assert set(markings.get_markings(data, "a", False, True)) == set(["1"]) + + assert set(markings.get_markings(data, "b", False, False)) == set([]) + assert set(markings.get_markings(data, "b", True, False)) == set([]) + assert set(markings.get_markings(data, "b", True, True)) == set([]) + assert set(markings.get_markings(data, "b", False, True)) == set([]) + + assert set(markings.get_markings(data, "c", False, False)) == set(["2"]) + assert set(markings.get_markings(data, "c", True, False)) == set(["2"]) + assert set(markings.get_markings(data, "c", True, True)) == set(["2", "3", "4", "5"]) + assert set(markings.get_markings(data, "c", False, True)) == set(["2", "3", "4", "5"]) + + assert set(markings.get_markings(data, "c.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "c.[0]", True, False)) == set(["2"]) + assert set(markings.get_markings(data, "c.[0]", True, True)) == set(["2"]) + assert set(markings.get_markings(data, "c.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "c.[1]", False, False)) == set(["3"]) + assert set(markings.get_markings(data, "c.[1]", True, False)) == set(["2", "3"]) + assert set(markings.get_markings(data, "c.[1]", True, True)) == set(["2", "3"]) + assert set(markings.get_markings(data, "c.[1]", False, True)) == set(["3"]) + + assert set(markings.get_markings(data, "c.[2]", False, False)) == set(["4"]) + assert set(markings.get_markings(data, "c.[2]", True, False)) == set(["2", "4"]) + assert set(markings.get_markings(data, "c.[2]", True, True)) == set(["2", "4", "5"]) + assert set(markings.get_markings(data, "c.[2]", False, True)) == set(["4", "5"]) + + assert set(markings.get_markings(data, "c.[2].g", False, False)) == set(["5"]) + assert set(markings.get_markings(data, "c.[2].g", True, False)) == set(["2", "4", "5"]) + assert set(markings.get_markings(data, "c.[2].g", True, True)) == set(["2", "4", "5"]) + assert set(markings.get_markings(data, "c.[2].g", False, True)) == set(["5"]) + + assert set(markings.get_markings(data, "x", False, False)) == set(["6"]) + assert set(markings.get_markings(data, "x", True, False)) == set(["6"]) + assert set(markings.get_markings(data, "x", True, True)) == set(["6", "7", "8", "9", "10"]) + assert set(markings.get_markings(data, "x", False, True)) == set(["6", "7", "8", "9", "10"]) + + assert set(markings.get_markings(data, "x.y", False, False)) == set(["7"]) + assert set(markings.get_markings(data, "x.y", True, False)) == set(["6", "7"]) + assert set(markings.get_markings(data, "x.y", True, True)) == set(["6", "7", "8"]) + assert set(markings.get_markings(data, "x.y", False, True)) == set(["7", "8"]) + + assert set(markings.get_markings(data, "x.y.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "x.y.[0]", True, False)) == set(["6", "7"]) + assert set(markings.get_markings(data, "x.y.[0]", True, True)) == set(["6", "7"]) + assert set(markings.get_markings(data, "x.y.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.y.[1]", False, False)) == set(["8"]) + assert set(markings.get_markings(data, "x.y.[1]", True, False)) == set(["6", "7", "8"]) + assert set(markings.get_markings(data, "x.y.[1]", True, True)) == set(["6", "7", "8"]) + assert set(markings.get_markings(data, "x.y.[1]", False, True)) == set(["8"]) + + assert set(markings.get_markings(data, "x.z", False, False)) == set(["9"]) + assert set(markings.get_markings(data, "x.z", True, False)) == set(["6", "9"]) + assert set(markings.get_markings(data, "x.z", True, True)) == set(["6", "9", "10"]) + assert set(markings.get_markings(data, "x.z", False, True)) == set(["9", "10"]) + + assert set(markings.get_markings(data, "x.z.foo1", False, False)) == set([]) + assert set(markings.get_markings(data, "x.z.foo1", True, False)) == set(["6", "9"]) + assert set(markings.get_markings(data, "x.z.foo1", True, True)) == set(["6", "9"]) + assert set(markings.get_markings(data, "x.z.foo1", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.z.foo2", False, False)) == set(["10"]) + assert set(markings.get_markings(data, "x.z.foo2", True, False)) == set(["6", "9", "10"]) + assert set(markings.get_markings(data, "x.z.foo2", True, True)) == set(["6", "9", "10"]) + assert set(markings.get_markings(data, "x.z.foo2", False, True)) == set(["10"]) + + +@pytest.mark.parametrize( + "data", [ + ( + Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[1]], + ), + ( + dict( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[1]], + ), + ], +) +def test_remove_marking_remove_one_selector_with_multiple_refs(data): + before = markings.remove_markings(data[0], data[1], ["description"]) + assert "granular_markings" not in before + + +def test_remove_marking_remove_multiple_selector_one_ref(): + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, MARKING_IDS[0], ["description", "modified"]) + assert "granular_markings" not in before + + +def test_remove_marking_mark_one_selector_from_multiple_ones(): + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_remove_marking_mark_one_selector_markings_from_multiple_ones(): + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_remove_marking_mark_mutilple_selector_multiple_refs(): + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description", "modified"]) + assert "granular_markings" not in before + + +def test_remove_marking_mark_another_property_same_marking(): + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["modified"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_remove_marking_mark_same_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["description"]) + assert "granular_markings" not in before + + +def test_remove_no_markings(): + before = { + "description": "test description", + } + after = markings.remove_markings(before, ["marking-definition--1"], ["description"]) + assert before == after + + +def test_remove_marking_bad_selector(): + before = { + "description": "test description", + } + with pytest.raises(AssertionError): + markings.remove_markings(before, ["marking-definition--1", "marking-definition--2"], ["title"]) + + +def test_remove_marking_not_present(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + with pytest.raises(MarkingNotFoundError): + markings.remove_markings(before, [MARKING_IDS[1]], ["description"]) + + +IS_MARKED_TEST_DATA = [ + Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + { + "selectors": ["malware_types", "description"], + "marking_ref": MARKING_IDS[2], + }, + { + "selectors": ["malware_types", "description"], + "marking_ref": MARKING_IDS[3], + }, + ], + **MALWARE_KWARGS + ), + dict( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + { + "selectors": ["malware_types", "description"], + "marking_ref": MARKING_IDS[2], + }, + { + "selectors": ["malware_types", "description"], + "marking_ref": MARKING_IDS[3], + }, + ], + **MALWARE_KWARGS + ), +] + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_smoke(data): + """Smoke test is_marked call does not fail.""" + assert markings.is_marked(data, selectors=["description"]) + assert markings.is_marked(data, selectors=["modified"]) is False + + +@pytest.mark.parametrize( + "data,selector", [ + (IS_MARKED_TEST_DATA[0], "foo"), + (IS_MARKED_TEST_DATA[0], ""), + (IS_MARKED_TEST_DATA[0], []), + (IS_MARKED_TEST_DATA[0], [""]), + (IS_MARKED_TEST_DATA[0], "x.z.[-2]"), + (IS_MARKED_TEST_DATA[0], "c.f"), + (IS_MARKED_TEST_DATA[0], "c.[2].i"), + (IS_MARKED_TEST_DATA[1], "c.[3]"), + (IS_MARKED_TEST_DATA[1], "d"), + (IS_MARKED_TEST_DATA[1], "x.[0]"), + (IS_MARKED_TEST_DATA[1], "z.y.w"), + (IS_MARKED_TEST_DATA[1], "x.z.[1]"), + (IS_MARKED_TEST_DATA[1], "x.z.foo3"), + ], +) +def test_is_marked_invalid_selector(data, selector): + """Test invalid selector raises an error.""" + with pytest.raises(AssertionError): + markings.is_marked(data, selectors=selector) + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_mix_selector(data): + """Test valid selector, one marked and one not marked returns True.""" + assert markings.is_marked(data, selectors=["description", "malware_types"]) + assert markings.is_marked(data, selectors=["description"]) + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_valid_selector_no_refs(data): + """Test that a valid selector return True when it has marking refs and False when not.""" + assert markings.is_marked(data, selectors=["description"]) + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[3]], ["description"]) + assert markings.is_marked(data, [MARKING_IDS[2]], ["description"]) + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[5]], ["description"]) is False + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_valid_selector_and_refs(data): + """Test that a valid selector returns True when marking_refs match.""" + assert markings.is_marked(data, [MARKING_IDS[1]], ["description"]) + assert markings.is_marked(data, [MARKING_IDS[1]], ["modified"]) is False + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_valid_selector_multiple_refs(data): + """Test that a valid selector returns True if aall marking_refs match. + Otherwise False.""" + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[3]], ["malware_types"]) + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[1]], ["malware_types"]) is False + assert markings.is_marked(data, MARKING_IDS[2], ["malware_types"]) + assert markings.is_marked(data, ["marking-definition--1234"], ["malware_types"]) is False + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_no_marking_refs(data): + """Test that a valid content selector with no marking_refs returns True + if there is a granular_marking that asserts that field, False + otherwise.""" + assert markings.is_marked(data, selectors=["type"]) is False + assert markings.is_marked(data, selectors=["malware_types"]) + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_no_selectors(data): + """Test that we're ensuring 'selectors' is provided.""" + with pytest.raises(TypeError) as excinfo: + markings.granular_markings.is_marked(data) + assert "'selectors' must be provided" in str(excinfo.value) + + +def test_is_marked_positional_arguments_combinations(): + """Test multiple combinations for inherited and descendant markings.""" + test_sdo = \ + { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45, + }, + ], + "x": { + "y": [ + "hello", + 88, + ], + "z": { + "foo1": "bar", + "foo2": 65, + }, + }, + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"], + }, + { + "marking_ref": "2", + "selectors": ["c"], + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"], + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"], + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"], + }, + { + "marking_ref": "6", + "selectors": ["x"], + }, + { + "marking_ref": "7", + "selectors": ["x.y"], + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"], + }, + { + "marking_ref": "9", + "selectors": ["x.z"], + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"], + }, + ], + } + + assert markings.is_marked(test_sdo, ["1"], "a", False, False) + assert markings.is_marked(test_sdo, ["1"], "a", True, False) + assert markings.is_marked(test_sdo, ["1"], "a", True, True) + assert markings.is_marked(test_sdo, ["1"], "a", False, True) + + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, "b", inherited=True, descendants=False) is False + assert markings.is_marked(test_sdo, "b", inherited=True, descendants=True) is False + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["2"], "c", False, False) + assert markings.is_marked(test_sdo, ["2"], "c", True, False) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5"], "c", True, True) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5"], "c", False, True) + + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["2"], "c.[0]", True, False) + assert markings.is_marked(test_sdo, ["2"], "c.[0]", True, True) + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, False) + assert markings.is_marked(test_sdo, ["2", "3"], "c.[1]", True, False) + assert markings.is_marked(test_sdo, ["2", "3"], "c.[1]", True, True) + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, True) + + assert markings.is_marked(test_sdo, ["4"], "c.[2]", False, False) + assert markings.is_marked(test_sdo, ["2", "4"], "c.[2]", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5"], "c.[2]", True, True) + assert markings.is_marked(test_sdo, ["4", "5"], "c.[2]", False, True) + + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, False) + assert markings.is_marked(test_sdo, ["2", "4", "5"], "c.[2].g", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5"], "c.[2].g", True, True) + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, True) + + assert markings.is_marked(test_sdo, ["6"], "x", False, False) + assert markings.is_marked(test_sdo, ["6"], "x", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10"], "x", True, True) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10"], "x", False, True) + + assert markings.is_marked(test_sdo, ["7"], "x.y", False, False) + assert markings.is_marked(test_sdo, ["6", "7"], "x.y", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8"], "x.y", True, True) + assert markings.is_marked(test_sdo, ["7", "8"], "x.y", False, True) + + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "7"], "x.y.[0]", True, False) + assert markings.is_marked(test_sdo, ["6", "7"], "x.y.[0]", True, True) + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, False) + assert markings.is_marked(test_sdo, ["6", "7", "8"], "x.y.[1]", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8"], "x.y.[1]", True, True) + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, True) + + assert markings.is_marked(test_sdo, ["9"], "x.z", False, False) + assert markings.is_marked(test_sdo, ["6", "9"], "x.z", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10"], "x.z", True, True) + assert markings.is_marked(test_sdo, ["9", "10"], "x.z", False, True) + + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "9"], "x.z.foo1", True, False) + assert markings.is_marked(test_sdo, ["6", "9"], "x.z.foo1", True, True) + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, False) + assert markings.is_marked(test_sdo, ["6", "9", "10"], "x.z.foo2", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10"], "x.z.foo2", True, True) + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, True) + + +def test_create_sdo_with_invalid_marking(): + with pytest.raises(AssertionError) as excinfo: + Malware( + granular_markings=[ + { + "selectors": ["foo"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + assert str(excinfo.value) == "Selector foo in Malware is not valid!" + + +def test_set_marking_mark_one_selector_multiple_refs(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_set_marking_mark_multiple_selector_one_refs(): + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0]], ["description", "modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_set_marking_mark_multiple_selector_multiple_refs_from_none(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1], + }, + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description", "modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_set_marking_mark_another_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1], + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[2], + }, + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[1], MARKING_IDS[2]], ["description"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +@pytest.mark.parametrize( + "marking", [ + ([MARKING_IDS[4], MARKING_IDS[5]], ["foo"]), + ([MARKING_IDS[4], MARKING_IDS[5]], ""), + ([MARKING_IDS[4], MARKING_IDS[5]], []), + ([MARKING_IDS[4], MARKING_IDS[5]], [""]), + ], +) +def test_set_marking_bad_selector(marking): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + + with pytest.raises(AssertionError): + before = markings.set_markings(before, marking[0], marking[1]) + + assert before == after + + +def test_set_marking_mark_same_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0]], ["description"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +CLEAR_MARKINGS_TEST_DATA = [ + Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["modified", "description"], + "marking_ref": MARKING_IDS[1], + }, + { + "selectors": ["modified", "description", "type"], + "marking_ref": MARKING_IDS[2], + }, + ], + **MALWARE_KWARGS + ), + dict( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0], + }, + { + "selectors": ["modified", "description"], + "marking_ref": MARKING_IDS[1], + }, + { + "selectors": ["modified", "description", "type"], + "marking_ref": MARKING_IDS[2], + }, + ], + **MALWARE_KWARGS + ), +] + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_smoke(data): + """Test clear_marking call does not fail.""" + data = markings.clear_markings(data, "modified") + assert markings.is_marked(data, "modified") is False + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_multiple_selectors(data): + """Test clearing markings for multiple selectors effectively removes associated markings.""" + data = markings.clear_markings(data, ["type", "description"]) + assert markings.is_marked(data, ["type", "description"]) is False + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_one_selector(data): + """Test markings associated with one selector were removed.""" + data = markings.clear_markings(data, "description") + assert markings.is_marked(data, "description") is False + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_all_selectors(data): + data = markings.clear_markings(data, ["description", "type", "modified"]) + assert markings.is_marked(data, "description") is False + assert "granular_markings" not in data + + +@pytest.mark.parametrize( + "data,selector", [ + (CLEAR_MARKINGS_TEST_DATA[0], "foo"), + (CLEAR_MARKINGS_TEST_DATA[0], ""), + (CLEAR_MARKINGS_TEST_DATA[1], []), + (CLEAR_MARKINGS_TEST_DATA[1], [""]), + ], +) +def test_clear_marking_bad_selector(data, selector): + """Test bad selector raises exception.""" + with pytest.raises(AssertionError): + markings.clear_markings(data, selector) + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_not_present(data): + """Test clearing markings for a selector that has no associated markings.""" + with pytest.raises(MarkingNotFoundError): + markings.clear_markings(data, ["malware_types"]) diff --git a/stix2/test/v21/test_identity.py b/stix2/test/v21/test_identity.py new file mode 100644 index 0000000..da99de4 --- /dev/null +++ b/stix2/test/v21/test_identity.py @@ -0,0 +1,82 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import IDENTITY_ID + +EXPECTED = """{ + "type": "identity", + "spec_version": "2.1", + "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.000Z", + "name": "John Smith", + "identity_class": "individual" +}""" + + +def test_identity_example(): + identity = stix2.v21.Identity( + id="identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11.000Z", + modified="2015-12-21T19:59:11.000Z", + name="John Smith", + identity_class="individual", + ) + + assert str(identity) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2015-12-21T19:59:11.000Z", + "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + "identity_class": "individual", + "modified": "2015-12-21T19:59:11.000Z", + "name": "John Smith", + "spec_version": "2.1", + "type": "identity", + }, + ], +) +def test_parse_identity(data): + identity = stix2.parse(data, version="2.1") + + assert identity.type == 'identity' + assert identity.spec_version == '2.1' + assert identity.id == IDENTITY_ID + assert identity.created == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert identity.modified == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert identity.name == "John Smith" + + +def test_parse_no_type(): + with pytest.raises(stix2.exceptions.ParseError): + stix2.parse( + """ + { + "id": "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.000Z", + "name": "John Smith", + "identity_class": "individual" + }""", version="2.1", + ) + + +def test_identity_with_custom(): + identity = stix2.v21.Identity( + name="John Smith", + identity_class="individual", + custom_properties={'x_foo': 'bar'}, + ) + + assert identity.x_foo == "bar" + assert "x_foo" in identity.object_properties() + +# TODO: Add other examples diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py new file mode 100644 index 0000000..628bdff --- /dev/null +++ b/stix2/test/v21/test_indicator.py @@ -0,0 +1,201 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS + +EXPECTED_INDICATOR = """{ + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "created": "2017-01-01T00:00:01.000Z", + "modified": "2017-01-01T00:00:01.000Z", + "indicator_types": [ + "malicious-activity" + ], + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "1970-01-01T00:00:01Z" +}""" + +EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" + type='indicator', + spec_version='2.1', + id='indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7', + created='2017-01-01T00:00:01.000Z', + modified='2017-01-01T00:00:01.000Z', + indicator_types=['malicious-activity'], + pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + valid_from='1970-01-01T00:00:01Z' +""".split()) + ")" + + +def test_indicator_with_all_required_properties(): + now = dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + epoch = dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + + ind = stix2.v21.Indicator( + type="indicator", + id=INDICATOR_ID, + created=now, + modified=now, + pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + valid_from=epoch, + indicator_types=['malicious-activity'], + ) + + assert ind.revoked is False + assert str(ind) == EXPECTED_INDICATOR + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(ind)) + assert rep == EXPECTED_INDICATOR_REPR + + +def test_indicator_autogenerated_properties(indicator): + assert indicator.type == 'indicator' + assert indicator.spec_version == '2.1' + assert indicator.id == 'indicator--00000000-0000-4000-8000-000000000001' + assert indicator.created == FAKE_TIME + assert indicator.modified == FAKE_TIME + assert indicator.indicator_types == ['malicious-activity'] + assert indicator.pattern == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']" + assert indicator.valid_from == FAKE_TIME + + assert indicator['type'] == 'indicator' + assert indicator['spec_version'] == '2.1' + assert indicator['id'] == 'indicator--00000000-0000-4000-8000-000000000001' + assert indicator['created'] == FAKE_TIME + assert indicator['modified'] == FAKE_TIME + assert indicator['indicator_types'] == ['malicious-activity'] + assert indicator['pattern'] == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']" + assert indicator['valid_from'] == FAKE_TIME + + +def test_indicator_type_must_be_indicator(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Indicator(type='xxx', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Indicator(id='my-prefix--', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.v21.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_properties(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.Indicator() + + assert excinfo.value.cls == stix2.v21.Indicator + assert excinfo.value.properties == ["indicator_types", "pattern"] + assert str(excinfo.value) == "No values for required properties for Indicator: (indicator_types, pattern)." + + +def test_indicator_required_property_pattern(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.Indicator(indicator_types=['malicious-activity']) + + assert excinfo.value.cls == stix2.v21.Indicator + assert excinfo.value.properties == ["pattern"] + + +def test_indicator_created_ref_invalid_format(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Indicator(created_by_ref='myprefix--12345678', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Indicator(revoked='no', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.v21.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(stix2.exceptions.ImmutableError) as excinfo: + indicator.valid_from = dt.datetime.now() + + assert str(excinfo.value) == "Cannot modify 'valid_from' property in 'Indicator' after creation." + + +def test_invalid_kwarg_to_indicator(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.v21.Indicator(my_custom_property="foo", **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.v21.Indicator + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Indicator: (my_custom_property)." + + +def test_created_modified_time_are_identical_by_default(): + """By default, the created and modified times should be the same.""" + ind = stix2.v21.Indicator(**INDICATOR_KWARGS) + + assert ind.created == ind.modified + + +@pytest.mark.parametrize( + "data", [ + EXPECTED_INDICATOR, + { + "type": "indicator", + "id": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "created": "2017-01-01T00:00:01Z", + "modified": "2017-01-01T00:00:01Z", + "indicator_types": [ + "malicious-activity", + ], + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "1970-01-01T00:00:01Z", + }, + ], +) +def test_parse_indicator(data): + idctr = stix2.parse(data, version="2.1") + + assert idctr.type == 'indicator' + assert idctr.spec_version == '2.1' + assert idctr.id == INDICATOR_ID + assert idctr.created == dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + assert idctr.modified == dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + assert idctr.valid_from == dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + assert idctr.indicator_types[0] == "malicious-activity" + assert idctr.pattern == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']" + + +def test_invalid_indicator_pattern(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Indicator( + indicator_types=['malicious-activity'], + pattern="file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e'", + ) + assert excinfo.value.cls == stix2.v21.Indicator + assert excinfo.value.prop_name == 'pattern' + assert 'input is missing square brackets' in excinfo.value.reason + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Indicator( + indicator_types=['malicious-activity'], + pattern='[file:hashes.MD5 = "d41d8cd98f00b204e9800998ecf8427e"]', + ) + assert excinfo.value.cls == stix2.v21.Indicator + assert excinfo.value.prop_name == 'pattern' + assert 'mismatched input' in excinfo.value.reason diff --git a/stix2/test/v21/test_intrusion_set.py b/stix2/test/v21/test_intrusion_set.py new file mode 100644 index 0000000..d87780c --- /dev/null +++ b/stix2/test/v21/test_intrusion_set.py @@ -0,0 +1,81 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import INTRUSION_SET_ID + +EXPECTED = """{ + "type": "intrusion-set", + "spec_version": "2.1", + "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Bobcat Breakin", + "description": "Incidents usually feature a shared TTP of a bobcat being released...", + "aliases": [ + "Zookeeper" + ], + "goals": [ + "acquisition-theft", + "harassment", + "damage" + ] +}""" + + +def test_intrusion_set_example(): + intrusion_set = stix2.v21.IntrusionSet( + id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", + name="Bobcat Breakin", + description="Incidents usually feature a shared TTP of a bobcat being released...", + aliases=["Zookeeper"], + goals=["acquisition-theft", "harassment", "damage"], + ) + + assert str(intrusion_set) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "aliases": [ + "Zookeeper", + ], + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Incidents usually feature a shared TTP of a bobcat being released...", + "goals": [ + "acquisition-theft", + "harassment", + "damage", + ], + "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Bobcat Breakin", + "spec_version": "2.1", + "type": "intrusion-set", + }, + ], +) +def test_parse_intrusion_set(data): + intset = stix2.parse(data) + + assert intset.type == "intrusion-set" + assert intset.spec_version == '2.1' + assert intset.id == INTRUSION_SET_ID + assert intset.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert intset.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert intset.goals == ["acquisition-theft", "harassment", "damage"] + assert intset.aliases == ["Zookeeper"] + assert intset.description == "Incidents usually feature a shared TTP of a bobcat being released..." + assert intset.name == "Bobcat Breakin" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_kill_chain_phases.py b/stix2/test/v21/test_kill_chain_phases.py new file mode 100644 index 0000000..0acc538 --- /dev/null +++ b/stix2/test/v21/test_kill_chain_phases.py @@ -0,0 +1,61 @@ +"""Tests for stix.ExternalReference""" + +import pytest + +import stix2 + +LMCO_RECON = """{ + "kill_chain_name": "lockheed-martin-cyber-kill-chain", + "phase_name": "reconnaissance" +}""" + + +def test_lockheed_martin_cyber_kill_chain(): + recon = stix2.v21.KillChainPhase( + kill_chain_name="lockheed-martin-cyber-kill-chain", + phase_name="reconnaissance", + ) + + assert str(recon) == LMCO_RECON + + +FOO_PRE_ATTACK = """{ + "kill_chain_name": "foo", + "phase_name": "pre-attack" +}""" + + +def test_kill_chain_example(): + preattack = stix2.v21.KillChainPhase( + kill_chain_name="foo", + phase_name="pre-attack", + ) + + assert str(preattack) == FOO_PRE_ATTACK + + +def test_kill_chain_required_properties(): + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.KillChainPhase() + + assert excinfo.value.cls == stix2.v21.KillChainPhase + assert excinfo.value.properties == ["kill_chain_name", "phase_name"] + + +def test_kill_chain_required_property_chain_name(): + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.KillChainPhase(phase_name="weaponization") + + assert excinfo.value.cls == stix2.v21.KillChainPhase + assert excinfo.value.properties == ["kill_chain_name"] + + +def test_kill_chain_required_property_phase_name(): + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.KillChainPhase(kill_chain_name="lockheed-martin-cyber-kill-chain") + + assert excinfo.value.cls == stix2.v21.KillChainPhase + assert excinfo.value.properties == ["phase_name"] diff --git a/stix2/test/v21/test_language_content.py b/stix2/test/v21/test_language_content.py new file mode 100644 index 0000000..4f541e0 --- /dev/null +++ b/stix2/test/v21/test_language_content.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +import datetime as dt + +import pytz + +import stix2 + +CAMPAIGN_ID = "campaign--12a111f0-b824-4baf-a224-83b80237a094" + +LANGUAGE_CONTENT_ID = "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d" + +TEST_CAMPAIGN = """{ + "type": "campaign", + "spec_version": "2.1", + "id": "campaign--12a111f0-b824-4baf-a224-83b80237a094", + "lang": "en", + "created": "2017-02-08T21:31:22.007Z", + "modified": "2017-02-08T21:31:22.007Z", + "name": "Bank Attack", + "description": "More information about bank attack" +}""" + +TEST_LANGUAGE_CONTENT = u"""{ + "type": "language-content", + "spec_version": "2.1", + "id": "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d", + "created": "2017-02-08T21:31:22.007Z", + "modified": "2017-02-08T21:31:22.007Z", + "object_ref": "campaign--12a111f0-b824-4baf-a224-83b80237a094", + "object_modified": "2017-02-08T21:31:22.007Z", + "contents": { + "de": { + "description": "Weitere Informationen über Banküberfall", + "name": "Bank Angriff 1" + }, + "fr": { + "description": "Plus d'informations sur la crise bancaire", + "name": "Attaque Bank 1" + } + } +}""" + + +def test_language_content_campaign(): + now = dt.datetime(2017, 2, 8, 21, 31, 22, microsecond=7000, tzinfo=pytz.utc) + + lc = stix2.v21.LanguageContent( + type='language-content', + id=LANGUAGE_CONTENT_ID, + created=now, + modified=now, + object_ref=CAMPAIGN_ID, + object_modified=now, + contents={ + 'de': { + 'name': 'Bank Angriff 1', + 'description': 'Weitere Informationen über Banküberfall', + }, + 'fr': { + 'name': 'Attaque Bank 1', + 'description': 'Plus d\'informations sur la crise bancaire', + }, + }, + ) + + camp = stix2.parse(TEST_CAMPAIGN, version='2.1') + + # In order to provide the same representation, we need to disable escaping + # in json.dumps(). https://docs.python.org/3/library/json.html#json.dumps + # or https://docs.python.org/2/library/json.html#json.dumps + assert lc.serialize(pretty=True, ensure_ascii=False) == TEST_LANGUAGE_CONTENT + assert lc.modified == camp.modified diff --git a/stix2/test/v21/test_location.py b/stix2/test/v21/test_location.py new file mode 100644 index 0000000..62fd9e0 --- /dev/null +++ b/stix2/test/v21/test_location.py @@ -0,0 +1,265 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import LOCATION_ID + +EXPECTED_LOCATION_1 = """{ + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 48.8566, + "longitude": 2.3522 +}""" + +EXPECTED_LOCATION_1_REPR = "Location(" + " ".join(""" + type='location', + spec_version='2.1', + id='location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64', + created='2016-04-06T20:03:00.000Z', + modified='2016-04-06T20:03:00.000Z', + latitude=48.8566, + longitude=2.3522""".split()) + ")" + +EXPECTED_LOCATION_2 = """{ + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "region": "north-america" +} +""" + +EXPECTED_LOCATION_2_REPR = "Location(" + " ".join(""" + type='location', + spec_version='2.1', + id='location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64', + created='2016-04-06T20:03:00.000Z', + modified='2016-04-06T20:03:00.000Z', + region='north-america'""".split()) + ")" + + +def test_location_with_some_required_properties(): + now = dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + + loc = stix2.v21.Location( + type="location", + id=LOCATION_ID, + created=now, + modified=now, + latitude=48.8566, + longitude=2.3522, + ) + + assert str(loc) == EXPECTED_LOCATION_1 + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(loc)) + assert rep == EXPECTED_LOCATION_1_REPR + + +@pytest.mark.parametrize( + "data", [ + EXPECTED_LOCATION_2, + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "region": "north-america", + }, + ], +) +def test_parse_location(data): + location = stix2.parse(data, version="2.1") + + assert location.type == 'location' + assert location.spec_version == '2.1' + assert location.id == LOCATION_ID + assert location.created == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert location.modified == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert location.region == 'north-america' + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(location)) + assert rep == EXPECTED_LOCATION_2_REPR + + +@pytest.mark.parametrize( + "data", [ + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 90.01, + "longitude": 0.0, + }, + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": -90.1, + "longitude": 0.0, + }, + ], +) +def test_location_bad_latitude(data): + with pytest.raises(ValueError) as excinfo: + stix2.parse(data) + + assert "Invalid value for Location 'latitude'" in str(excinfo.value) + + +@pytest.mark.parametrize( + "data", [ + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 80, + "longitude": 180.1, + }, + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 80, + "longitude": -180.1, + }, + ], +) +def test_location_bad_longitude(data): + with pytest.raises(ValueError) as excinfo: + stix2.parse(data) + + assert "Invalid value for Location 'longitude'" in str(excinfo.value) + + +@pytest.mark.parametrize( + "data", [ + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "longitude": 175.7, + "precision": 20, + }, + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 80, + "precision": 20, + }, + ], +) +def test_location_properties_missing_when_precision_is_present(data): + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.parse(data) + + assert any(x in str(excinfo.value) for x in ("(latitude, precision)", "(longitude, precision)")) + + +@pytest.mark.parametrize( + "data", [ + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 18.468842, + "longitude": -66.120711, + "precision": -100.0, + }, + ], +) +def test_location_negative_precision(data): + with pytest.raises(ValueError) as excinfo: + stix2.parse(data) + + assert "Invalid value for Location 'precision'" in str(excinfo.value) + + +@pytest.mark.parametrize( + "data,msg", [ + ( + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 18.468842, + "precision": 5.0, + }, + "(longitude, precision) are not met.", + ), + ( + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "longitude": 160.7, + "precision": 5.0, + }, + "(latitude, precision) are not met.", + ), + ], +) +def test_location_latitude_dependency_missing(data, msg): + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.parse(data) + + assert msg in str(excinfo.value) + + +@pytest.mark.parametrize( + "data,msg", [ + ( + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 18.468842, + }, + "(longitude, latitude) are not met.", + ), + ( + { + "type": "location", + "spec_version": "2.1", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "longitude": 160.7, + }, + "(latitude, longitude) are not met.", + ), + ], +) +def test_location_lat_or_lon_dependency_missing(data, msg): + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.parse(data) + + assert msg in str(excinfo.value) diff --git a/stix2/test/v21/test_malware.py b/stix2/test/v21/test_malware.py new file mode 100644 index 0000000..3ae96d9 --- /dev/null +++ b/stix2/test/v21/test_malware.py @@ -0,0 +1,166 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS + +EXPECTED_MALWARE = """{ + "type": "malware", + "spec_version": "2.1", + "id": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "name": "Cryptolocker", + "malware_types": [ + "ransomware" + ] +}""" + + +def test_malware_with_all_required_properties(): + now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + + mal = stix2.v21.Malware( + type="malware", + id=MALWARE_ID, + created=now, + modified=now, + malware_types=["ransomware"], + name="Cryptolocker", + ) + + assert str(mal) == EXPECTED_MALWARE + + +def test_malware_autogenerated_properties(malware): + assert malware.type == 'malware' + assert malware.id == 'malware--00000000-0000-4000-8000-000000000001' + assert malware.created == FAKE_TIME + assert malware.modified == FAKE_TIME + assert malware.malware_types == ['ransomware'] + assert malware.name == "Cryptolocker" + + assert malware['type'] == 'malware' + assert malware['id'] == 'malware--00000000-0000-4000-8000-000000000001' + assert malware['created'] == FAKE_TIME + assert malware['modified'] == FAKE_TIME + assert malware['malware_types'] == ['ransomware'] + assert malware['name'] == "Cryptolocker" + + +def test_malware_type_must_be_malware(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Malware(type='xxx', **MALWARE_KWARGS) + + assert excinfo.value.cls == stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Malware(id='my-prefix--', **MALWARE_KWARGS) + + assert excinfo.value.cls == stix2.v21.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_properties(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.Malware() + + assert excinfo.value.cls == stix2.v21.Malware + assert excinfo.value.properties == ["malware_types", "name"] + + +def test_malware_required_property_name(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.Malware(malware_types=['ransomware']) + + assert excinfo.value.cls == stix2.v21.Malware + assert excinfo.value.properties == ["name"] + + +def test_cannot_assign_to_malware_attributes(malware): + with pytest.raises(stix2.exceptions.ImmutableError) as excinfo: + malware.name = "Cryptolocker II" + + assert str(excinfo.value) == "Cannot modify 'name' property in 'Malware' after creation." + + +def test_invalid_kwarg_to_malware(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.v21.Malware(my_custom_property="foo", **MALWARE_KWARGS) + + assert excinfo.value.cls == stix2.v21.Malware + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Malware: (my_custom_property)." + + +@pytest.mark.parametrize( + "data", [ + EXPECTED_MALWARE, + { + "type": "malware", + "spec_version": "2.1", + "id": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "malware_types": ["ransomware"], + "name": "Cryptolocker", + }, + ], +) +def test_parse_malware(data): + mal = stix2.parse(data) + + assert mal.type == 'malware' + assert mal.spec_version == '2.1' + assert mal.id == MALWARE_ID + assert mal.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert mal.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert mal.malware_types == ['ransomware'] + assert mal.name == 'Cryptolocker' + + +def test_parse_malware_invalid_labels(): + data = re.compile('\\[.+\\]', re.DOTALL).sub('1', EXPECTED_MALWARE) + with pytest.raises(ValueError) as excinfo: + stix2.parse(data) + assert "Invalid value for Malware 'malware_types'" in str(excinfo.value) + + +def test_parse_malware_kill_chain_phases(): + kill_chain = """ + "kill_chain_phases": [ + { + "kill_chain_name": "lockheed-martin-cyber-kill-chain", + "phase_name": "reconnaissance" + } + ]""" + data = EXPECTED_MALWARE.replace('malware"', 'malware",%s' % kill_chain) + mal = stix2.parse(data, version="2.1") + assert mal.kill_chain_phases[0].kill_chain_name == "lockheed-martin-cyber-kill-chain" + assert mal.kill_chain_phases[0].phase_name == "reconnaissance" + assert mal['kill_chain_phases'][0]['kill_chain_name'] == "lockheed-martin-cyber-kill-chain" + assert mal['kill_chain_phases'][0]['phase_name'] == "reconnaissance" + + +def test_parse_malware_clean_kill_chain_phases(): + kill_chain = """ + "kill_chain_phases": [ + { + "kill_chain_name": "lockheed-martin-cyber-kill-chain", + "phase_name": 1 + } + ]""" + data = EXPECTED_MALWARE.replace('2.1"', '2.1",%s' % kill_chain) + mal = stix2.parse(data, version="2.1") + assert mal['kill_chain_phases'][0]['phase_name'] == "1" diff --git a/stix2/test/v21/test_markings.py b/stix2/test/v21/test_markings.py new file mode 100644 index 0000000..7782236 --- /dev/null +++ b/stix2/test/v21/test_markings.py @@ -0,0 +1,275 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 +from stix2.v21 import TLP_WHITE + +from .constants import MARKING_DEFINITION_ID + +EXPECTED_TLP_MARKING_DEFINITION = """{ + "type": "marking-definition", + "spec_version": "2.1", + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "created": "2017-01-20T00:00:00.000Z", + "definition_type": "tlp", + "definition": { + "tlp": "white" + } +}""" + +EXPECTED_STATEMENT_MARKING_DEFINITION = """{ + "type": "marking-definition", + "spec_version": "2.1", + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "created": "2017-01-20T00:00:00Z", + "definition_type": "statement", + "definition": { + "statement": "Copyright 2016, Example Corp" + } +}""" + +EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING = """{ + "type": "campaign", + "spec_version": "2.1", + "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.", + "object_marking_refs": [ + "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" + ] +}""" + +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 = """{ + "type": "campaign", + "spec_version": "2.1", + "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": [ + { + "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "selectors": [ + "description" + ] + } + ] +}""" + + +def test_marking_def_example_with_tlp(): + assert str(TLP_WHITE) == EXPECTED_TLP_MARKING_DEFINITION + + +def test_marking_def_example_with_statement_positional_argument(): + marking_definition = stix2.v21.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_kwargs_statement(): + kwargs = dict(statement="Copyright 2016, Example Corp") + marking_definition = stix2.v21.MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="statement", + definition=stix2.StatementMarking(**kwargs), + ) + + assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION + + +def test_marking_def_invalid_type(): + with pytest.raises(ValueError): + stix2.v21.MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="my-definition-type", + definition=stix2.StatementMarking("Copyright 2016, Example Corp"), + ) + + +def test_campaign_with_markings_example(): + campaign = stix2.v21.Campaign( + id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:00Z", + modified="2016-04-06T20:03:00Z", + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", + object_marking_refs=TLP_WHITE, + ) + assert str(campaign) == EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING + + +def test_granular_example(): + granular_marking = stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.GranularMarking( + marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + selectors=["abc[0]"], # missing "." + ) + + assert excinfo.value.cls == stix2.v21.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(): + campaign = stix2.v21.Campaign( + id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:00Z", + modified="2016-04-06T20:03:00Z", + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", + granular_markings=[ + stix2.v21.GranularMarking( + marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + selectors=["description"], + ), + ], + ) + assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS + + +@pytest.mark.parametrize( + "data", [ + EXPECTED_TLP_MARKING_DEFINITION, + { + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "spec_version": "2.1", + "type": "marking-definition", + "created": "2017-01-20T00:00:00Z", + "definition": { + "tlp": "white", + }, + "definition_type": "tlp", + }, + ], +) +def test_parse_marking_definition(data): + gm = stix2.parse(data, version="2.1") + + assert gm.type == 'marking-definition' + assert gm.spec_version == '2.1' + assert gm.id == MARKING_DEFINITION_ID + assert gm.created == dt.datetime(2017, 1, 20, 0, 0, 0, tzinfo=pytz.utc) + assert gm.definition.tlp == "white" + assert gm.definition_type == "tlp" + + +@stix2.v21.CustomMarking( + 'x-new-marking-type', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], +) +class NewMarking(object): + def __init__(self, property2=None, **kwargs): + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_registered_custom_marking(): + nm = NewMarking(property1='something', property2=55) + + marking_def = stix2.v21.MarkingDefinition( + id="marking-definition--00000000-0000-4000-8000-000000000012", + created="2017-01-22T00:00:00.000Z", + definition_type="x-new-marking-type", + definition=nm, + ) + + assert marking_def.type == "marking-definition" + assert marking_def.id == "marking-definition--00000000-0000-4000-8000-000000000012" + assert marking_def.created == dt.datetime(2017, 1, 22, 0, 0, 0, tzinfo=pytz.utc) + assert marking_def.definition.property1 == "something" + assert marking_def.definition.property2 == 55 + assert marking_def.definition_type == "x-new-marking-type" + + +def test_registered_custom_marking_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewMarking(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_not_registered_marking_raises_exception(): + with pytest.raises(ValueError) as excinfo: + # Used custom object on purpose to demonstrate a not-registered marking + @stix2.v21.CustomObject( + 'x-new-marking-type2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ], + ) + class NewObject2(object): + def __init__(self, property2=None, **kwargs): + return + + no = NewObject2(property1='something', property2=55) + + stix2.v21.MarkingDefinition( + id="marking-definition--00000000-0000-4000-8000-000000000012", + created="2017-01-22T00:00:00.000Z", + definition_type="x-new-marking-type2", + definition=no, + ) + + assert str(excinfo.value) == "definition_type must be a valid marking type" + + +def test_marking_wrong_type_construction(): + with pytest.raises(ValueError) as excinfo: + # Test passing wrong type for properties. + @stix2.v21.CustomMarking('x-new-marking-type2', ("a", "b")) + class NewObject3(object): + pass + + assert str(excinfo.value) == "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]" + + +def test_campaign_add_markings(): + campaign = stix2.v21.Campaign( + id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:00Z", + modified="2016-04-06T20:03:00Z", + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", + ) + campaign = campaign.add_markings(TLP_WHITE) + assert campaign.object_marking_refs[0] == TLP_WHITE.id diff --git a/stix2/test/v21/test_note.py b/stix2/test/v21/test_note.py new file mode 100644 index 0000000..a9807e8 --- /dev/null +++ b/stix2/test/v21/test_note.py @@ -0,0 +1,120 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import CAMPAIGN_ID, NOTE_ID + +CONTENT = ( + 'This note indicates the various steps taken by the threat' + ' analyst team to investigate this specific campaign. Step' + ' 1) Do a scan 2) Review scanned results for identified ' + 'hosts not known by external intel... etc' +) + +EXPECTED_NOTE = """{ + "type": "note", + "spec_version": "2.1", + "id": "note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "abstract": "Tracking Team Note#1", + "content": "%s", + "authors": [ + "John Doe" + ], + "object_refs": [ + "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" + ], + "external_references": [ + { + "source_name": "job-tracker", + "external_id": "job-id-1234" + } + ] +}""" % CONTENT + +EXPECTED_OPINION_REPR = "Note(" + " ".join(( + """ + type='note', + spec_version='2.1', + id='note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061', + created='2016-05-12T08:17:27.000Z', + modified='2016-05-12T08:17:27.000Z', + abstract='Tracking Team Note#1', + content='%s', + authors=['John Doe'], + object_refs=['campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f'], + external_references=[ExternalReference(source_name='job-tracker', external_id='job-id-1234')] +""" % CONTENT +).split()) + ")" + + +def test_note_with_required_properties(): + now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + + note = stix2.v21.Note( + type='note', + id=NOTE_ID, + created=now, + modified=now, + abstract='Tracking Team Note#1', + object_refs=[CAMPAIGN_ID], + authors=['John Doe'], + content=CONTENT, + external_references=[ + { + 'source_name': 'job-tracker', + 'external_id': 'job-id-1234', + }, + ], + ) + + assert str(note) == EXPECTED_NOTE + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(note)) + assert rep == EXPECTED_OPINION_REPR + + +@pytest.mark.parametrize( + "data", [ + EXPECTED_NOTE, + { + "type": "note", + "spec_version": "2.1", + "id": "note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "abstract": "Tracking Team Note#1", + "content": CONTENT, + "authors": [ + "John Doe", + ], + "object_refs": [ + "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + ], + "external_references": [ + { + "source_name": "job-tracker", + "external_id": "job-id-1234", + }, + ], + }, + ], +) +def test_parse_note(data): + note = stix2.parse(data, version="2.1") + + assert note.type == 'note' + assert note.spec_version == '2.1' + assert note.id == NOTE_ID + assert note.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert note.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert note.object_refs[0] == CAMPAIGN_ID + assert note.authors[0] == 'John Doe' + assert note.abstract == 'Tracking Team Note#1' + assert note.content == CONTENT + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(note)) + assert rep == EXPECTED_OPINION_REPR diff --git a/stix2/test/v21/test_object_markings.py b/stix2/test/v21/test_object_markings.py new file mode 100644 index 0000000..d43aad5 --- /dev/null +++ b/stix2/test/v21/test_object_markings.py @@ -0,0 +1,596 @@ +import pytest + +from stix2 import exceptions, markings +from stix2.v21 import TLP_AMBER, Malware + +from .constants import FAKE_TIME, MALWARE_ID +from .constants import MALWARE_KWARGS as MALWARE_KWARGS_CONST +from .constants import MARKING_IDS + +"""Tests for the Data Markings API.""" + +MALWARE_KWARGS = MALWARE_KWARGS_CONST.copy() +MALWARE_KWARGS.update({ + 'id': MALWARE_ID, + 'created': FAKE_TIME, + 'modified': FAKE_TIME, +}) + + +@pytest.mark.parametrize( + "data", [ + ( + Malware(**MALWARE_KWARGS), + Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + MALWARE_KWARGS, + dict( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + MARKING_IDS[0], + ), + ( + Malware(**MALWARE_KWARGS), + Malware( + object_marking_refs=[TLP_AMBER.id], + **MALWARE_KWARGS + ), + TLP_AMBER, + ), + ], +) +def test_add_markings_one_marking(data): + before = data[0] + after = data[1] + + before = markings.add_markings(before, data[2], None) + + for m in before["object_marking_refs"]: + assert m in after["object_marking_refs"] + + +def test_add_markings_multiple_marking(): + before = Malware( + **MALWARE_KWARGS + ) + + after = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1]], + **MALWARE_KWARGS + ) + + before = markings.add_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], None) + + for m in before["object_marking_refs"]: + assert m in after["object_marking_refs"] + + +def test_add_markings_combination(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1]], + granular_markings=[ + { + "selectors": ["malware_types"], + "marking_ref": MARKING_IDS[2], + }, + { + "selectors": ["name"], + "marking_ref": MARKING_IDS[3], + }, + ], + **MALWARE_KWARGS + ) + + before = markings.add_markings(before, MARKING_IDS[0], None) + before = markings.add_markings(before, MARKING_IDS[1], None) + before = markings.add_markings(before, MARKING_IDS[2], "malware_types") + before = markings.add_markings(before, MARKING_IDS[3], "name") + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + for m in before["object_marking_refs"]: + assert m in after["object_marking_refs"] + + +@pytest.mark.parametrize( + "data", [ + ([""]), + (""), + ([]), + ([MARKING_IDS[0], 456]), + ], +) +def test_add_markings_bad_markings(data): + before = Malware( + **MALWARE_KWARGS + ) + with pytest.raises(exceptions.InvalidValueError): + before = markings.add_markings(before, data, None) + + assert "object_marking_refs" not in before + + +GET_MARKINGS_TEST_DATA = \ + { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45, + }, + ], + "x": { + "y": [ + "hello", + 88, + ], + "z": { + "foo1": "bar", + "foo2": 65, + }, + }, + "object_marking_refs": ["11"], + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"], + }, + { + "marking_ref": "2", + "selectors": ["c"], + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"], + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"], + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"], + }, + { + "marking_ref": "6", + "selectors": ["x"], + }, + { + "marking_ref": "7", + "selectors": ["x.y"], + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"], + }, + { + "marking_ref": "9", + "selectors": ["x.z"], + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"], + }, + ], + } + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_object_marking(data): + assert set(markings.get_markings(data, None)) == set(["11"]) + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_object_and_granular_combinations(data): + """Test multiple combinations for inherited and descendant markings.""" + assert set(markings.get_markings(data, "a", False, False)) == set(["1"]) + assert set(markings.get_markings(data, "a", True, False)) == set(["1", "11"]) + assert set(markings.get_markings(data, "a", True, True)) == set(["1", "11"]) + assert set(markings.get_markings(data, "a", False, True)) == set(["1"]) + + assert set(markings.get_markings(data, "b", False, False)) == set([]) + assert set(markings.get_markings(data, "b", True, False)) == set(["11"]) + assert set(markings.get_markings(data, "b", True, True)) == set(["11"]) + assert set(markings.get_markings(data, "b", False, True)) == set([]) + + assert set(markings.get_markings(data, "c", False, False)) == set(["2"]) + assert set(markings.get_markings(data, "c", True, False)) == set(["2", "11"]) + assert set(markings.get_markings(data, "c", True, True)) == set(["2", "3", "4", "5", "11"]) + assert set(markings.get_markings(data, "c", False, True)) == set(["2", "3", "4", "5"]) + + assert set(markings.get_markings(data, "c.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "c.[0]", True, False)) == set(["2", "11"]) + assert set(markings.get_markings(data, "c.[0]", True, True)) == set(["2", "11"]) + assert set(markings.get_markings(data, "c.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "c.[1]", False, False)) == set(["3"]) + assert set(markings.get_markings(data, "c.[1]", True, False)) == set(["2", "3", "11"]) + assert set(markings.get_markings(data, "c.[1]", True, True)) == set(["2", "3", "11"]) + assert set(markings.get_markings(data, "c.[1]", False, True)) == set(["3"]) + + assert set(markings.get_markings(data, "c.[2]", False, False)) == set(["4"]) + assert set(markings.get_markings(data, "c.[2]", True, False)) == set(["2", "4", "11"]) + assert set(markings.get_markings(data, "c.[2]", True, True)) == set(["2", "4", "5", "11"]) + assert set(markings.get_markings(data, "c.[2]", False, True)) == set(["4", "5"]) + + assert set(markings.get_markings(data, "c.[2].g", False, False)) == set(["5"]) + assert set(markings.get_markings(data, "c.[2].g", True, False)) == set(["2", "4", "5", "11"]) + assert set(markings.get_markings(data, "c.[2].g", True, True)) == set(["2", "4", "5", "11"]) + assert set(markings.get_markings(data, "c.[2].g", False, True)) == set(["5"]) + + assert set(markings.get_markings(data, "x", False, False)) == set(["6"]) + assert set(markings.get_markings(data, "x", True, False)) == set(["6", "11"]) + assert set(markings.get_markings(data, "x", True, True)) == set(["6", "7", "8", "9", "10", "11"]) + assert set(markings.get_markings(data, "x", False, True)) == set(["6", "7", "8", "9", "10"]) + + assert set(markings.get_markings(data, "x.y", False, False)) == set(["7"]) + assert set(markings.get_markings(data, "x.y", True, False)) == set(["6", "7", "11"]) + assert set(markings.get_markings(data, "x.y", True, True)) == set(["6", "7", "8", "11"]) + assert set(markings.get_markings(data, "x.y", False, True)) == set(["7", "8"]) + + assert set(markings.get_markings(data, "x.y.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "x.y.[0]", True, False)) == set(["6", "7", "11"]) + assert set(markings.get_markings(data, "x.y.[0]", True, True)) == set(["6", "7", "11"]) + assert set(markings.get_markings(data, "x.y.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.y.[1]", False, False)) == set(["8"]) + assert set(markings.get_markings(data, "x.y.[1]", True, False)) == set(["6", "7", "8", "11"]) + assert set(markings.get_markings(data, "x.y.[1]", True, True)) == set(["6", "7", "8", "11"]) + assert set(markings.get_markings(data, "x.y.[1]", False, True)) == set(["8"]) + + assert set(markings.get_markings(data, "x.z", False, False)) == set(["9"]) + assert set(markings.get_markings(data, "x.z", True, False)) == set(["6", "9", "11"]) + assert set(markings.get_markings(data, "x.z", True, True)) == set(["6", "9", "10", "11"]) + assert set(markings.get_markings(data, "x.z", False, True)) == set(["9", "10"]) + + assert set(markings.get_markings(data, "x.z.foo1", False, False)) == set([]) + assert set(markings.get_markings(data, "x.z.foo1", True, False)) == set(["6", "9", "11"]) + assert set(markings.get_markings(data, "x.z.foo1", True, True)) == set(["6", "9", "11"]) + assert set(markings.get_markings(data, "x.z.foo1", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.z.foo2", False, False)) == set(["10"]) + assert set(markings.get_markings(data, "x.z.foo2", True, False)) == set(["6", "9", "10", "11"]) + assert set(markings.get_markings(data, "x.z.foo2", True, True)) == set(["6", "9", "10", "11"]) + assert set(markings.get_markings(data, "x.z.foo2", False, True)) == set(["10"]) + + +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + Malware(**MALWARE_KWARGS), + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ), + MALWARE_KWARGS, + ), + ], +) +def test_remove_markings_object_level(data): + before = data[0] + after = data[1] + + before = markings.remove_markings(before, MARKING_IDS[0], None) + + assert 'object_marking_refs' not in before + assert 'object_marking_refs' not in after + + modified = after['modified'] + after = markings.remove_markings(after, MARKING_IDS[0], None) + modified == after['modified'] + + +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + Malware( + object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[2]], + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + dict( + object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[2]], + ), + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], TLP_AMBER.id], + **MALWARE_KWARGS + ), + Malware( + object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], TLP_AMBER], + ), + ], +) +def test_remove_markings_multiple(data): + before = data[0] + after = data[1] + + before = markings.remove_markings(before, data[2], None) + + assert before['object_marking_refs'] == after['object_marking_refs'] + + +def test_remove_markings_bad_markings(): + before = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ) + with pytest.raises(AssertionError) as excinfo: + markings.remove_markings(before, [MARKING_IDS[4]], None) + assert str(excinfo.value) == "Marking ['%s'] was not found in Malware!" % MARKING_IDS[4] + + +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + Malware(**MALWARE_KWARGS), + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + MALWARE_KWARGS, + ), + ], +) +def test_clear_markings(data): + before = data[0] + after = data[1] + + before = markings.clear_markings(before, None) + + assert 'object_marking_refs' not in before + assert 'object_marking_refs' not in after + + +def test_is_marked_object_and_granular_combinations(): + """Test multiple combinations for inherited and descendant markings.""" + test_sdo = \ + { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45, + }, + ], + "x": { + "y": [ + "hello", + 88, + ], + "z": { + "foo1": "bar", + "foo2": 65, + }, + }, + "object_marking_refs": "11", + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"], + }, + { + "marking_ref": "2", + "selectors": ["c"], + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"], + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"], + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"], + }, + { + "marking_ref": "6", + "selectors": ["x"], + }, + { + "marking_ref": "7", + "selectors": ["x.y"], + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"], + }, + { + "marking_ref": "9", + "selectors": ["x.z"], + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"], + }, + ], + } + + assert markings.is_marked(test_sdo, ["1"], "a", False, False) + assert markings.is_marked(test_sdo, ["1", "11"], "a", True, False) + assert markings.is_marked(test_sdo, ["1", "11"], "a", True, True) + assert markings.is_marked(test_sdo, ["1"], "a", False, True) + + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["11"], "b", True, False) + assert markings.is_marked(test_sdo, ["11"], "b", True, True) + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["2"], "c", False, False) + assert markings.is_marked(test_sdo, ["2", "11"], "c", True, False) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5", "11"], "c", True, True) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5"], "c", False, True) + + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["2", "11"], "c.[0]", True, False) + assert markings.is_marked(test_sdo, ["2", "11"], "c.[0]", True, True) + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, False) + assert markings.is_marked(test_sdo, ["2", "3", "11"], "c.[1]", True, False) + assert markings.is_marked(test_sdo, ["2", "3", "11"], "c.[1]", True, True) + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, True) + + assert markings.is_marked(test_sdo, ["4"], "c.[2]", False, False) + assert markings.is_marked(test_sdo, ["2", "4", "11"], "c.[2]", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5", "11"], "c.[2]", True, True) + assert markings.is_marked(test_sdo, ["4", "5"], "c.[2]", False, True) + + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, False) + assert markings.is_marked(test_sdo, ["2", "4", "5", "11"], "c.[2].g", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5", "11"], "c.[2].g", True, True) + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, True) + + assert markings.is_marked(test_sdo, ["6"], "x", False, False) + assert markings.is_marked(test_sdo, ["6", "11"], "x", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10", "11"], "x", True, True) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10"], "x", False, True) + + assert markings.is_marked(test_sdo, ["7"], "x.y", False, False) + assert markings.is_marked(test_sdo, ["6", "7", "11"], "x.y", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "11"], "x.y", True, True) + assert markings.is_marked(test_sdo, ["7", "8"], "x.y", False, True) + + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "7", "11"], "x.y.[0]", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "11"], "x.y.[0]", True, True) + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "11"], "x.y.[1]", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "11"], "x.y.[1]", True, True) + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, True) + + assert markings.is_marked(test_sdo, ["9"], "x.z", False, False) + assert markings.is_marked(test_sdo, ["6", "9", "11"], "x.z", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10", "11"], "x.z", True, True) + assert markings.is_marked(test_sdo, ["9", "10"], "x.z", False, True) + + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "9", "11"], "x.z.foo1", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "11"], "x.z.foo1", True, True) + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, False) + assert markings.is_marked(test_sdo, ["6", "9", "10", "11"], "x.z.foo2", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10", "11"], "x.z.foo2", True, True) + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, True) + + assert markings.is_marked(test_sdo, ["11"], None, True, True) + assert markings.is_marked(test_sdo, ["2"], None, True, True) is False + + +@pytest.mark.parametrize( + "data", [ + ( + Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + Malware(**MALWARE_KWARGS), + ), + ( + dict( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ), + MALWARE_KWARGS, + ), + ], +) +def test_is_marked_no_markings(data): + marked = data[0] + nonmarked = data[1] + + assert markings.is_marked(marked) + assert markings.is_marked(nonmarked) is False + + +def test_set_marking(): + before = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ) + after = Malware( + object_marking_refs=[MARKING_IDS[4], MARKING_IDS[5]], + **MALWARE_KWARGS + ) + + before = markings.set_markings(before, [MARKING_IDS[4], MARKING_IDS[5]], None) + + for m in before["object_marking_refs"]: + assert m in [MARKING_IDS[4], MARKING_IDS[5]] + + assert [MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]] not in before["object_marking_refs"] + + for x in before["object_marking_refs"]: + assert x in after["object_marking_refs"] + + +@pytest.mark.parametrize( + "data", [ + ([]), + ([""]), + (""), + ([MARKING_IDS[4], 687]), + ], +) +def test_set_marking_bad_input(data): + before = Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ) + after = Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ) + with pytest.raises(exceptions.InvalidValueError): + before = markings.set_markings(before, data, None) + + assert before == after diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py new file mode 100644 index 0000000..5a5881a --- /dev/null +++ b/stix2/test/v21/test_observed_data.py @@ -0,0 +1,1301 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import OBSERVED_DATA_ID + +OBJECTS_REGEX = re.compile('\"objects\": {(?:.*?)(?:(?:[^{]*?)|(?:{[^{]*?}))*}', re.DOTALL) + + +EXPECTED = """{ + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T19:58:16.000Z", + "modified": "2016-04-06T19:58:16.000Z", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "name": "foo.exe" + } + } +}""" + + +def test_observed_data_example(): + observed_data = stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + }, + }, + ) + + assert str(observed_data) == EXPECTED + + +EXPECTED_WITH_REF = """{ + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T19:58:16.000Z", + "modified": "2016-04-06T19:58:16.000Z", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "name": "foo.exe" + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": [ + "0" + ] + } + } +}""" + + +def test_observed_data_example_with_refs(): + observed_data = stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["0"], + }, + }, + ) + + assert str(observed_data) == EXPECTED_WITH_REF + + +def test_observed_data_example_with_bad_refs(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "type": "file", + "name": "foo.exe", + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["2"], + }, + }, + ) + + assert excinfo.value.cls == stix2.v21.ObservedData + assert excinfo.value.prop_name == "objects" + assert excinfo.value.reason == "Invalid object reference for 'Directory:contains_refs': '2' is not a valid object in local scope" + + +def test_observed_data_example_with_non_dictionary(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects="file: foo.exe", + ) + + assert excinfo.value.cls == stix2.v21.ObservedData + assert excinfo.value.prop_name == "objects" + assert 'must contain a dictionary' in excinfo.value.reason + + +def test_observed_data_example_with_empty_dictionary(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={}, + ) + + assert excinfo.value.cls == stix2.v21.ObservedData + assert excinfo.value.prop_name == "objects" + assert 'must contain a non-empty dictionary' in excinfo.value.reason + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "created": "2016-04-06T19:58:16.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "modified": "2016-04-06T19:58:16.000Z", + "number_observed": 50, + "objects": { + "0": { + "name": "foo.exe", + "type": "file", + }, + }, + }, + ], +) +def test_parse_observed_data(data): + odata = stix2.parse(data, version="2.1") + + assert odata.type == 'observed-data' + assert odata.spec_version == '2.1' + assert odata.id == OBSERVED_DATA_ID + assert odata.created == dt.datetime(2016, 4, 6, 19, 58, 16, tzinfo=pytz.utc) + assert odata.modified == dt.datetime(2016, 4, 6, 19, 58, 16, tzinfo=pytz.utc) + assert odata.first_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert odata.last_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert odata.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert odata.objects["0"].type == "file" + + +@pytest.mark.parametrize( + "data", [ + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "payload_bin": "VBORw0KGgoAAAANSUhEUgAAADI==" + }""", + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + "hashes": { + "MD5": "6826f9a05da08134006557758bb3afbb" + } + }""", + ], +) +def test_parse_artifact_valid(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + odata = stix2.parse(odata_str, version="2.1") + assert odata.objects["0"].type == "artifact" + + +@pytest.mark.parametrize( + "data", [ + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "payload_bin": "abcVBORw0KGgoAAAANSUhEUgAAADI==" + }""", + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + "hashes": { + "MD5": "a" + } + }""", + ], +) +def test_parse_artifact_invalid(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + with pytest.raises(ValueError): + stix2.parse(odata_str, version="2.1") + + +def test_artifact_example_dependency_error(): + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.v21.Artifact(url="http://example.com/sirvizio.exe") + + assert excinfo.value.dependencies == [("hashes", "url")] + assert str(excinfo.value) == "The property dependencies for Artifact: (hashes, url) are not met." + + +@pytest.mark.parametrize( + "data", [ + """"0": { + "type": "autonomous-system", + "number": 15139, + "name": "Slime Industries", + "rir": "ARIN" + }""", + ], +) +def test_parse_autonomous_system_valid(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + odata = stix2.parse(odata_str, version="2.1") + assert odata.objects["0"].type == "autonomous-system" + assert odata.objects["0"].number == 15139 + assert odata.objects["0"].name == "Slime Industries" + assert odata.objects["0"].rir == "ARIN" + + +@pytest.mark.parametrize( + "data", [ + """{ + "type": "email-addr", + "value": "john@example.com", + "display_name": "John Doe", + "belongs_to_ref": "0" + }""", + ], +) +def test_parse_email_address(data): + odata = stix2.parse_observable(data, {"0": "user-account"}, version='2.1') + assert odata.type == "email-addr" + + odata_str = re.compile('"belongs_to_ref": "0"', re.DOTALL).sub('"belongs_to_ref": "3"', data) + with pytest.raises(stix2.exceptions.InvalidObjRefError): + stix2.parse_observable(odata_str, {"0": "user-account"}, version='2.1') + + +@pytest.mark.parametrize( + "data", [ + """ + { + "type": "email-message", + "is_multipart": true, + "content_type": "multipart/mixed", + "date": "2016-06-19T14:20:40.000Z", + "from_ref": "1", + "to_refs": [ + "2" + ], + "cc_refs": [ + "3" + ], + "subject": "Check out this picture of a cat!", + "additional_header_fields": { + "Content-Disposition": "inline", + "X-Mailer": "Mutt/1.5.23", + "X-Originating-IP": "198.51.100.3" + }, + "body_multipart": [ + { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!" + }, + { + "content_type": "image/png", + "content_disposition": "attachment; filename=\\"tabby.png\\"", + "body_raw_ref": "4" + }, + { + "content_type": "application/zip", + "content_disposition": "attachment; filename=\\"tabby_pics.zip\\"", + "body_raw_ref": "5" + } + ] + } + """, + ], +) +def test_parse_email_message(data): + valid_refs = { + "0": "email-message", + "1": "email-addr", + "2": "email-addr", + "3": "email-addr", + "4": "artifact", + "5": "file", + } + odata = stix2.parse_observable(data, valid_refs, version='2.1') + assert odata.type == "email-message" + assert odata.body_multipart[0].content_disposition == "inline" + + +@pytest.mark.parametrize( + "data", [ + """ + { + "type": "email-message", + "from_ref": "0", + "to_refs": ["1"], + "is_multipart": true, + "date": "1997-11-21T15:55:06.000Z", + "subject": "Saying Hello", + "body": "Cats are funny!" + } + """, + ], +) +def test_parse_email_message_not_multipart(data): + valid_refs = { + "0": "email-addr", + "1": "email-addr", + } + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.parse_observable(data, valid_refs, version='2.1') + + assert excinfo.value.cls == stix2.v21.EmailMessage + assert excinfo.value.dependencies == [("is_multipart", "body")] + + +@pytest.mark.parametrize( + "data", [ + """"0": { + "type": "file", + "hashes": { + "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a" + } + }, + "1": { + "type": "file", + "hashes": { + "SHA-256": "19c549ec2628b989382f6b280cbd7bb836a0b461332c0fe53511ce7d584b89d3" + } + }, + "2": { + "type": "file", + "hashes": { + "SHA-256": "0969de02ecf8a5f003e3f6d063d848c8a193aada092623f8ce408c15bcb5f038" + } + }, + "3": { + "type": "file", + "name": "foo.zip", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + }, + "mime_type": "application/zip", + "extensions": { + "archive-ext": { + "contains_refs": [ + "0", + "1", + "2" + ] + } + } + }""", + ], +) +def test_parse_file_archive(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + odata = stix2.parse(odata_str, version="2.1") + assert all(x in odata.objects["3"].extensions['archive-ext'].contains_refs + for x in ["0", "1", "2"]) + + +@pytest.mark.parametrize( + "data", [ + """ + { + "type": "email-message", + "is_multipart": true, + "content_type": "multipart/mixed", + "date": "2016-06-19T14:20:40.000Z", + "from_ref": "1", + "to_refs": [ + "2" + ], + "cc_refs": [ + "3" + ], + "subject": "Check out this picture of a cat!", + "additional_header_fields": { + "Content-Disposition": "inline", + "X-Mailer": "Mutt/1.5.23", + "X-Originating-IP": "198.51.100.3" + }, + "body_multipart": [ + { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!" + }, + { + "content_type": "image/png", + "content_disposition": "attachment; filename=\\"tabby.png\\"" + }, + { + "content_type": "application/zip", + "content_disposition": "attachment; filename=\\"tabby_pics.zip\\"", + "body_raw_ref": "5" + } + ] + } + """, + ], +) +def test_parse_email_message_with_at_least_one_error(data): + valid_refs = { + "0": "email-message", + "1": "email-addr", + "2": "email-addr", + "3": "email-addr", + "4": "artifact", + "5": "file", + } + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.parse_observable(data, valid_refs, version='2.1') + + assert excinfo.value.cls == stix2.v21.EmailMIMEComponent + assert excinfo.value.properties == ["body", "body_raw_ref"] + assert "At least one of the" in str(excinfo.value) + assert "must be populated" in str(excinfo.value) + + +@pytest.mark.parametrize( + "data", [ + """ + { + "type": "network-traffic", + "src_ref": "0", + "dst_ref": "1", + "protocols": [ + "tcp" + ] + } + """, + ], +) +def test_parse_basic_tcp_traffic(data): + odata = stix2.parse_observable( + data, {"0": "ipv4-addr", "1": "ipv4-addr"}, + version='2.1', + ) + + assert odata.type == "network-traffic" + assert odata.src_ref == "0" + assert odata.dst_ref == "1" + assert odata.protocols == ["tcp"] + + +@pytest.mark.parametrize( + "data", [ + """ + { + "type": "network-traffic", + "src_port": 2487, + "dst_port": 1723, + "protocols": [ + "ipv4", + "pptp" + ], + "src_byte_count": 35779, + "dst_byte_count": 935750, + "encapsulates_refs": [ + "4" + ] + } + """, + ], +) +def test_parse_basic_tcp_traffic_with_error(data): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.parse_observable(data, {"4": "network-traffic"}, version='2.1') + + assert excinfo.value.cls == stix2.v21.NetworkTraffic + assert excinfo.value.properties == ["dst_ref", "src_ref"] + + +EXPECTED_PROCESS_OD = """{ + "created": "2016-04-06T19:58:16.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "first_observed": "2015-12-21T19:00:00Z", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "last_observed": "2015-12-21T19:00:00Z", + "modified": "2016-04-06T19:58:16.000Z", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100fSHA" + }, + }, + "1": { + "type": "process", + "pid": 1221, + "created": "2016-01-20T14:11:25.55Z", + "command_line": "./gedit-bin --new-window", + "binary_ref": "0" + } + }, + "spec_version": "2.1", + "type": "observed-data" +}""" + + +def test_observed_data_with_process_example(): + observed_data = stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "type": "file", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f", + }, + }, + "1": { + "type": "process", + "pid": 1221, + "created": "2016-01-20T14:11:25.55Z", + "command_line": "./gedit-bin --new-window", + "image_ref": "0", + }, + }, + ) + + assert observed_data.objects["0"].type == "file" + assert observed_data.objects["0"].hashes["SHA-256"] == "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + assert observed_data.objects["1"].type == "process" + assert observed_data.objects["1"].pid == 1221 + assert observed_data.objects["1"].command_line == "./gedit-bin --new-window" + + +# creating cyber observables directly + +def test_artifact_example(): + art = stix2.v21.Artifact( + mime_type="image/jpeg", + url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + hashes={ + "MD5": "6826f9a05da08134006557758bb3afbb", + }, + ) + assert art.mime_type == "image/jpeg" + assert art.url == "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg" + assert art.hashes["MD5"] == "6826f9a05da08134006557758bb3afbb" + + +def test_artifact_mutual_exclusion_error(): + with pytest.raises(stix2.exceptions.MutuallyExclusivePropertiesError) as excinfo: + stix2.v21.Artifact( + mime_type="image/jpeg", + url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + hashes={ + "MD5": "6826f9a05da08134006557758bb3afbb", + }, + payload_bin="VBORw0KGgoAAAANSUhEUgAAADI==", + ) + + assert excinfo.value.cls == stix2.v21.Artifact + assert excinfo.value.properties == ["payload_bin", "url"] + assert 'are mutually exclusive' in str(excinfo.value) + + +def test_directory_example(): + dir = stix2.v21.Directory( + _valid_refs={"1": "file"}, + path='/usr/lib', + created="2015-12-21T19:00:00Z", + modified="2015-12-24T19:00:00Z", + accessed="2015-12-21T20:00:00Z", + contains_refs=["1"], + ) + + assert dir.path == '/usr/lib' + assert dir.created == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert dir.modified == dt.datetime(2015, 12, 24, 19, 0, 0, tzinfo=pytz.utc) + assert dir.accessed == dt.datetime(2015, 12, 21, 20, 0, 0, tzinfo=pytz.utc) + assert dir.contains_refs == ["1"] + + +def test_directory_example_ref_error(): + with pytest.raises(stix2.exceptions.InvalidObjRefError) as excinfo: + stix2.v21.Directory( + _valid_refs=[], + path='/usr/lib', + created="2015-12-21T19:00:00Z", + modified="2015-12-24T19:00:00Z", + accessed="2015-12-21T20:00:00Z", + contains_refs=["1"], + ) + + assert excinfo.value.cls == stix2.v21.Directory + assert excinfo.value.prop_name == "contains_refs" + + +def test_domain_name_example(): + dn = stix2.v21.DomainName( + _valid_refs={"1": 'domain-name'}, + value="example.com", + resolves_to_refs=["1"], + ) + + assert dn.value == "example.com" + assert dn.resolves_to_refs == ["1"] + + +def test_domain_name_example_invalid_ref_type(): + with pytest.raises(stix2.exceptions.InvalidObjRefError) as excinfo: + stix2.v21.DomainName( + _valid_refs={"1": "file"}, + value="example.com", + resolves_to_refs=["1"], + ) + + assert excinfo.value.cls == stix2.v21.DomainName + assert excinfo.value.prop_name == "resolves_to_refs" + + +def test_file_example(): + f = stix2.v21.File( + name="qwerty.dll", + hashes={ + "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a", + }, + size=100, + magic_number_hex="1C", + mime_type="application/msword", + created="2016-12-21T19:00:00Z", + modified="2016-12-24T19:00:00Z", + accessed="2016-12-21T20:00:00Z", + ) + + assert f.name == "qwerty.dll" + assert f.size == 100 + assert f.magic_number_hex == "1C" + assert f.hashes["SHA-256"] == "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a" + assert f.mime_type == "application/msword" + assert f.created == dt.datetime(2016, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert f.modified == dt.datetime(2016, 12, 24, 19, 0, 0, tzinfo=pytz.utc) + assert f.accessed == dt.datetime(2016, 12, 21, 20, 0, 0, tzinfo=pytz.utc) + + +def test_file_example_with_NTFSExt(): + f = stix2.v21.File( + name="abc.txt", + extensions={ + "ntfs-ext": { + "alternate_data_streams": [ + { + "name": "second.stream", + "size": 25536, + }, + ], + }, + }, + ) + + assert f.name == "abc.txt" + assert f.extensions["ntfs-ext"].alternate_data_streams[0].size == 25536 + + +def test_file_example_with_empty_NTFSExt(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.v21.File( + name="abc.txt", + extensions={ + "ntfs-ext": {}, + }, + ) + + assert excinfo.value.cls == stix2.v21.NTFSExt + assert excinfo.value.properties == sorted(list(stix2.NTFSExt._properties.keys())) + + +def test_file_example_with_PDFExt(): + f = stix2.v21.File( + name="qwerty.dll", + extensions={ + "pdf-ext": { + "version": "1.7", + "document_info_dict": { + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02", + }, + "pdfid0": "DFCE52BD827ECF765649852119D", + "pdfid1": "57A1E0F9ED2AE523E313C", + }, + }, + ) + + assert f.name == "qwerty.dll" + assert f.extensions["pdf-ext"].version == "1.7" + assert f.extensions["pdf-ext"].document_info_dict["Title"] == "Sample document" + + +def test_file_example_with_PDFExt_Object(): + f = stix2.v21.File( + name="qwerty.dll", + extensions={ + "pdf-ext": stix2.v21.PDFExt( + version="1.7", + document_info_dict={ + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02", + }, + pdfid0="DFCE52BD827ECF765649852119D", + pdfid1="57A1E0F9ED2AE523E313C", + ), + }, + ) + + assert f.name == "qwerty.dll" + assert f.extensions["pdf-ext"].version == "1.7" + assert f.extensions["pdf-ext"].document_info_dict["Title"] == "Sample document" + + +def test_file_example_with_RasterImageExt_Object(): + f = stix2.v21.File( + name="qwerty.jpeg", + extensions={ + "raster-image-ext": { + "bits_per_pixel": 123, + "exif_tags": { + "Make": "Nikon", + "Model": "D7000", + "XResolution": 4928, + "YResolution": 3264, + }, + }, + }, + ) + assert f.name == "qwerty.jpeg" + assert f.extensions["raster-image-ext"].bits_per_pixel == 123 + assert f.extensions["raster-image-ext"].exif_tags["XResolution"] == 4928 + + +RASTER_IMAGE_EXT = """{ +"type": "observed-data", +"spec_version": "2.1", +"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", +"created": "2016-04-06T19:58:16.000Z", +"modified": "2016-04-06T19:58:16.000Z", +"first_observed": "2015-12-21T19:00:00Z", +"last_observed": "2015-12-21T19:00:00Z", +"number_observed": 1, +"objects": { + "0": { + "type": "file", + "name": "picture.jpg", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + }, + "extensions": { + "raster-image-ext": { + "image_height": 768, + "image_width": 1024, + "bits_per_pixel": 72, + "exif_tags": { + "Make": "Nikon", + "Model": "D7000", + "XResolution": 4928, + "YResolution": 3264 + } + } + } + } +} +} +""" + + +def test_raster_image_ext_parse(): + obj = stix2.parse(RASTER_IMAGE_EXT, version="2.1") + assert obj.objects["0"].extensions['raster-image-ext'].image_width == 1024 + + +def test_raster_images_ext_create(): + ext = stix2.v21.RasterImageExt(image_width=1024) + assert "image_width" in str(ext) + + +def test_file_example_with_WindowsPEBinaryExt(): + f = stix2.v21.File( + name="qwerty.dll", + extensions={ + "windows-pebinary-ext": { + "pe_type": "exe", + "machine_hex": "014c", + "number_of_sections": 4, + "time_date_stamp": "2016-01-22T12:31:12Z", + "pointer_to_symbol_table_hex": "74726144", + "number_of_symbols": 4542568, + "size_of_optional_header": 224, + "characteristics_hex": "818f", + "optional_header": { + "magic_hex": "010b", + "major_linker_version": 2, + "minor_linker_version": 25, + "size_of_code": 512, + "size_of_initialized_data": 283648, + "size_of_uninitialized_data": 0, + "address_of_entry_point": 4096, + "base_of_code": 4096, + "base_of_data": 8192, + "image_base": 14548992, + "section_alignment": 4096, + "file_alignment": 4096, + "major_os_version": 1, + "minor_os_version": 0, + "major_image_version": 0, + "minor_image_version": 0, + "major_subsystem_version": 4, + "minor_subsystem_version": 0, + "win32_version_value_hex": "00", + "size_of_image": 299008, + "size_of_headers": 4096, + "checksum_hex": "00", + "subsystem_hex": "03", + "dll_characteristics_hex": "00", + "size_of_stack_reserve": 100000, + "size_of_stack_commit": 8192, + "size_of_heap_reserve": 100000, + "size_of_heap_commit": 4096, + "loader_flags_hex": "abdbffde", + "number_of_rva_and_sizes": 3758087646, + }, + "sections": [ + { + "name": "CODE", + "entropy": 0.061089, + }, + { + "name": "DATA", + "entropy": 7.980693, + }, + { + "name": "NicolasB", + "entropy": 0.607433, + }, + { + "name": ".idata", + "entropy": 0.607433, + }, + ], + }, + }, + ) + assert f.name == "qwerty.dll" + assert f.extensions["windows-pebinary-ext"].sections[2].entropy == 0.607433 + + +def test_file_example_encryption_error(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.v21.File(magic_number_hex="010b") + + assert excinfo.value.cls == stix2.v21.File + assert "At least one of the (hashes, name)" in str(excinfo.value) + + +def test_ip4_address_example(): + ip4 = stix2.v21.IPv4Address( + _valid_refs={"4": "mac-addr", "5": "mac-addr"}, + value="198.51.100.3", + resolves_to_refs=["4", "5"], + ) + + assert ip4.value == "198.51.100.3" + assert ip4.resolves_to_refs == ["4", "5"] + + +def test_ip4_address_example_cidr(): + ip4 = stix2.v21.IPv4Address(value="198.51.100.0/24") + + assert ip4.value == "198.51.100.0/24" + + +def test_ip6_address_example(): + ip6 = stix2.v21.IPv6Address(value="2001:0db8:85a3:0000:0000:8a2e:0370:7334") + + assert ip6.value == "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + + +def test_mac_address_example(): + ip6 = stix2.v21.MACAddress(value="d2:fb:49:24:37:18") + + assert ip6.value == "d2:fb:49:24:37:18" + + +def test_network_traffic_example(): + nt = stix2.v21.NetworkTraffic( + _valid_refs={"0": "ipv4-addr", "1": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + dst_ref="1", + ) + assert nt.protocols == ["tcp"] + assert nt.src_ref == "0" + assert nt.dst_ref == "1" + + +def test_network_traffic_http_request_example(): + h = stix2.v21.HTTPRequestExt( + request_method="get", + request_value="/download.html", + request_version="http/1.1", + request_header={ + "Accept-Encoding": "gzip,deflate", + "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", + "Host": "www.example.com", + }, + ) + nt = stix2.v21.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'http-request-ext': h}, + ) + assert nt.extensions['http-request-ext'].request_method == "get" + assert nt.extensions['http-request-ext'].request_value == "/download.html" + assert nt.extensions['http-request-ext'].request_version == "http/1.1" + assert nt.extensions['http-request-ext'].request_header['Accept-Encoding'] == "gzip,deflate" + assert nt.extensions['http-request-ext'].request_header['User-Agent'] == "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113" + assert nt.extensions['http-request-ext'].request_header['Host'] == "www.example.com" + + +def test_network_traffic_icmp_example(): + h = stix2.v21.ICMPExt(icmp_type_hex="08", icmp_code_hex="00") + nt = stix2.v21.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'icmp-ext': h}, + ) + assert nt.extensions['icmp-ext'].icmp_type_hex == "08" + assert nt.extensions['icmp-ext'].icmp_code_hex == "00" + + +def test_network_traffic_socket_example(): + h = stix2.v21.SocketExt( + is_listening=True, + address_family="AF_INET", + protocol_family="PF_INET", + socket_type="SOCK_STREAM", + ) + nt = stix2.v21.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'socket-ext': h}, + ) + assert nt.extensions['socket-ext'].is_listening + assert nt.extensions['socket-ext'].address_family == "AF_INET" + assert nt.extensions['socket-ext'].protocol_family == "PF_INET" + assert nt.extensions['socket-ext'].socket_type == "SOCK_STREAM" + + +def test_network_traffic_tcp_example(): + h = stix2.v21.TCPExt(src_flags_hex="00000002") + nt = stix2.v21.NetworkTraffic( + _valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'tcp-ext': h}, + ) + assert nt.extensions['tcp-ext'].src_flags_hex == "00000002" + + +def test_mutex_example(): + m = stix2.v21.Mutex(name="barney") + + assert m.name == "barney" + + +def test_process_example(): + p = stix2.v21.Process( + _valid_refs={"0": "file"}, + pid=1221, + created="2016-01-20T14:11:25.55Z", + command_line="./gedit-bin --new-window", + image_ref="0", + ) + + assert p.command_line == "./gedit-bin --new-window" + + +def test_process_example_empty_error(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.v21.Process() + + assert excinfo.value.cls == stix2.v21.Process + properties_of_process = list(stix2.v21.Process._properties.keys()) + properties_of_process.remove("type") + assert excinfo.value.properties == sorted(properties_of_process) + msg = "At least one of the ({1}) properties for {0} must be populated." + msg = msg.format( + stix2.v21.Process.__name__, + ", ".join(sorted(properties_of_process)), + ) + assert str(excinfo.value) == msg + + +def test_process_example_empty_with_extensions(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.v21.Process(extensions={ + "windows-process-ext": {}, + }) + + assert excinfo.value.cls == stix2.v21.WindowsProcessExt + properties_of_extension = list(stix2.v21.WindowsProcessExt._properties.keys()) + assert excinfo.value.properties == sorted(properties_of_extension) + + +def test_process_example_windows_process_ext(): + proc = stix2.v21.Process( + pid=314, + extensions={ + "windows-process-ext": { + "aslr_enabled": True, + "dep_enabled": True, + "priority": "HIGH_PRIORITY_CLASS", + "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309", + }, + }, + ) + assert proc.extensions["windows-process-ext"].aslr_enabled + assert proc.extensions["windows-process-ext"].dep_enabled + assert proc.extensions["windows-process-ext"].priority == "HIGH_PRIORITY_CLASS" + assert proc.extensions["windows-process-ext"].owner_sid == "S-1-5-21-186985262-1144665072-74031268-1309" + + +def test_process_example_windows_process_ext_empty(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.v21.Process( + pid=1221, + extensions={ + "windows-process-ext": {}, + }, + ) + + assert excinfo.value.cls == stix2.v21.WindowsProcessExt + properties_of_extension = list(stix2.v21.WindowsProcessExt._properties.keys()) + assert excinfo.value.properties == sorted(properties_of_extension) + + +def test_process_example_extensions_empty(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Process(extensions={}) + + assert excinfo.value.cls == stix2.v21.Process + assert excinfo.value.prop_name == 'extensions' + assert 'non-empty dictionary' in excinfo.value.reason + + +def test_process_example_with_WindowsProcessExt_Object(): + p = stix2.v21.Process(extensions={ + "windows-process-ext": stix2.v21.WindowsProcessExt( + aslr_enabled=True, + dep_enabled=True, + priority="HIGH_PRIORITY_CLASS", + owner_sid="S-1-5-21-186985262-1144665072-74031268-1309", + ), # noqa + }) + + assert p.extensions["windows-process-ext"].dep_enabled + assert p.extensions["windows-process-ext"].owner_sid == "S-1-5-21-186985262-1144665072-74031268-1309" + + +def test_process_example_with_WindowsServiceExt(): + p = stix2.v21.Process(extensions={ + "windows-service-ext": { + "service_name": "sirvizio", + "display_name": "Sirvizio", + "start_type": "SERVICE_AUTO_START", + "service_type": "SERVICE_WIN32_OWN_PROCESS", + "service_status": "SERVICE_RUNNING", + }, + }) + + assert p.extensions["windows-service-ext"].service_name == "sirvizio" + assert p.extensions["windows-service-ext"].service_type == "SERVICE_WIN32_OWN_PROCESS" + + +def test_process_example_with_WindowsProcessServiceExt(): + p = stix2.v21.Process(extensions={ + "windows-service-ext": { + "service_name": "sirvizio", + "display_name": "Sirvizio", + "start_type": "SERVICE_AUTO_START", + "service_type": "SERVICE_WIN32_OWN_PROCESS", + "service_status": "SERVICE_RUNNING", + }, + "windows-process-ext": { + "aslr_enabled": True, + "dep_enabled": True, + "priority": "HIGH_PRIORITY_CLASS", + "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309", + }, + }) + + assert p.extensions["windows-service-ext"].service_name == "sirvizio" + assert p.extensions["windows-service-ext"].service_type == "SERVICE_WIN32_OWN_PROCESS" + assert p.extensions["windows-process-ext"].dep_enabled + assert p.extensions["windows-process-ext"].owner_sid == "S-1-5-21-186985262-1144665072-74031268-1309" + + +def test_software_example(): + s = stix2.v21.Software( + name="Word", + cpe="cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*", + version="2002", + vendor="Microsoft", + ) + + assert s.name == "Word" + assert s.cpe == "cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*" + assert s.version == "2002" + assert s.vendor == "Microsoft" + + +def test_url_example(): + s = stix2.v21.URL(value="https://example.com/research/index.html") + + assert s.type == "url" + assert s.value == "https://example.com/research/index.html" + + +def test_user_account_example(): + a = stix2.v21.UserAccount( + user_id="1001", + account_login="jdoe", + account_type="unix", + display_name="John Doe", + is_service_account=False, + is_privileged=False, + can_escalate_privs=True, + account_created="2016-01-20T12:31:12Z", + credential_last_changed="2016-01-20T14:27:43Z", + account_first_login="2016-01-20T14:26:07Z", + account_last_login="2016-07-22T16:08:28Z", + ) + + assert a.user_id == "1001" + assert a.account_login == "jdoe" + assert a.account_type == "unix" + assert a.display_name == "John Doe" + assert not a.is_service_account + assert not a.is_privileged + assert a.can_escalate_privs + assert a.account_created == dt.datetime(2016, 1, 20, 12, 31, 12, tzinfo=pytz.utc) + assert a.credential_last_changed == dt.datetime(2016, 1, 20, 14, 27, 43, tzinfo=pytz.utc) + assert a.account_first_login == dt.datetime(2016, 1, 20, 14, 26, 7, tzinfo=pytz.utc) + assert a.account_last_login == dt.datetime(2016, 7, 22, 16, 8, 28, tzinfo=pytz.utc) + + +def test_user_account_unix_account_ext_example(): + u = stix2.v21.UNIXAccountExt( + gid=1001, + groups=["wheel"], + home_dir="/home/jdoe", + shell="/bin/bash", + ) + a = stix2.v21.UserAccount( + user_id="1001", + account_login="jdoe", + account_type="unix", + extensions={'unix-account-ext': u}, + ) + assert a.extensions['unix-account-ext'].gid == 1001 + assert a.extensions['unix-account-ext'].groups == ["wheel"] + assert a.extensions['unix-account-ext'].home_dir == "/home/jdoe" + assert a.extensions['unix-account-ext'].shell == "/bin/bash" + + +def test_windows_registry_key_example(): + with pytest.raises(ValueError): + stix2.v21.WindowsRegistryValueType( + name="Foo", + data="qwerty", + data_type="string", + ) + + v = stix2.v21.WindowsRegistryValueType( + name="Foo", + data="qwerty", + data_type="REG_SZ", + ) + w = stix2.v21.WindowsRegistryKey( + key="hkey_local_machine\\system\\bar\\foo", + values=[v], + ) + assert w.key == "hkey_local_machine\\system\\bar\\foo" + assert w.values[0].name == "Foo" + assert w.values[0].data == "qwerty" + assert w.values[0].data_type == "REG_SZ" + + +def test_x509_certificate_example(): + x509 = stix2.v21.X509Certificate( + issuer="C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com", # noqa + validity_not_before="2016-03-12T12:00:00Z", + validity_not_after="2016-08-21T12:00:00Z", + subject="C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org", + ) # noqa + + assert x509.type == "x509-certificate" + assert x509.issuer == "C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com" # noqa + assert x509.subject == "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org" # noqa + + +def test_new_version_with_related_objects(): + data = stix2.v21.ObservedData( + first_observed="2016-03-12T12:00:00Z", + last_observed="2016-03-12T12:00:00Z", + number_observed=1, + objects={ + 'src_ip': { + 'type': 'ipv4-addr', + 'value': '127.0.0.1/32', + }, + 'domain': { + 'type': 'domain-name', + 'value': 'example.com', + 'resolves_to_refs': ['src_ip'], + }, + }, + ) + new_version = data.new_version(last_observed="2017-12-12T12:00:00Z") + assert new_version.last_observed.year == 2017 + assert new_version.objects['domain'].resolves_to_refs[0] == 'src_ip' diff --git a/stix2/test/v21/test_opinion.py b/stix2/test/v21/test_opinion.py new file mode 100644 index 0000000..79e97ca --- /dev/null +++ b/stix2/test/v21/test_opinion.py @@ -0,0 +1,92 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import OPINION_ID + +EXPLANATION = ( + 'This doesn\'t seem like it is feasible. We\'ve seen how ' + 'PandaCat has attacked Spanish infrastructure over the ' + 'last 3 years, so this change in targeting seems too great' + ' to be viable. The methods used are more commonly ' + 'associated with the FlameDragonCrew.' +) + +EXPECTED_OPINION = """{ + "type": "opinion", + "spec_version": "2.1", + "id": "opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "explanation": "%s", + "object_refs": [ + "relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471" + ], + "opinion": "strongly-disagree" +}""" % EXPLANATION + +EXPECTED_OPINION_REPR = "Opinion(" + " ".join(( + """ + type='opinion', + spec_version='2.1', + id='opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7', + created='2016-05-12T08:17:27.000Z', + modified='2016-05-12T08:17:27.000Z', + explanation="%s", + object_refs=['relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471'], + opinion='strongly-disagree'""" % EXPLANATION +).split()) + ")" + + +def test_opinion_with_required_properties(): + now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + + opi = stix2.v21.Opinion( + type='opinion', + id=OPINION_ID, + created=now, + modified=now, + object_refs=['relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471'], + opinion='strongly-disagree', + explanation=EXPLANATION, + ) + + assert str(opi) == EXPECTED_OPINION + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(opi)) + assert rep == EXPECTED_OPINION_REPR + + +@pytest.mark.parametrize( + "data", [ + EXPECTED_OPINION, + { + "type": "opinion", + "spec_version": "2.1", + "id": "opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "explanation": EXPLANATION, + "object_refs": [ + "relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471", + ], + "opinion": "strongly-disagree", + }, + ], +) +def test_parse_opinion(data): + opinion = stix2.parse(data, version="2.1") + + assert opinion.type == 'opinion' + assert opinion.spec_version == '2.1' + assert opinion.id == OPINION_ID + assert opinion.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert opinion.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert opinion.opinion == 'strongly-disagree' + assert opinion.object_refs[0] == 'relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471' + assert opinion.explanation == EXPLANATION + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(opinion)) + assert rep == EXPECTED_OPINION_REPR diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py new file mode 100644 index 0000000..3dc7cde --- /dev/null +++ b/stix2/test/v21/test_pattern_expressions.py @@ -0,0 +1,525 @@ +import datetime + +import pytest + +import stix2 +from stix2.pattern_visitor import create_pattern_object + + +def test_create_comparison_expression(): + exp = stix2.EqualityComparisonExpression( + "file:hashes.'SHA-256'", + stix2.HashConstant("aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", "SHA-256"), + ) # noqa + + assert str(exp) == "file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f'" + + +def test_boolean_expression(): + exp1 = stix2.MatchesComparisonExpression( + "email-message:from_ref.value", + stix2.StringConstant(".+\\@example\\.com$"), + ) + exp2 = stix2.MatchesComparisonExpression( + "email-message:body_multipart[*].body_raw_ref.name", + stix2.StringConstant("^Final Report.+\\.exe$"), + ) + exp = stix2.AndBooleanExpression([exp1, exp2]) + + assert str(exp) == "email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$'" # noqa + + +def test_boolean_expression_with_parentheses(): + exp1 = stix2.MatchesComparisonExpression( + stix2.ObjectPath( + "email-message", + [ + stix2.ReferenceObjectPathComponent("from_ref"), + stix2.BasicObjectPathComponent("value", False), + ], + ), + stix2.StringConstant(".+\\@example\\.com$"), + ) + exp2 = stix2.MatchesComparisonExpression( + "email-message:body_multipart[*].body_raw_ref.name", + stix2.StringConstant("^Final Report.+\\.exe$"), + ) + exp = stix2.ParentheticalExpression(stix2.AndBooleanExpression([exp1, exp2])) + assert str(exp) == "(email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$')" # noqa + + +def test_hash_followed_by_registryKey_expression_python_constant(): + hash_exp = stix2.EqualityComparisonExpression( + "file:hashes.MD5", + stix2.HashConstant("79054025255fb1a26e4bc422aef54eb4", "MD5"), + ) + o_exp1 = stix2.ObservationExpression(hash_exp) + reg_exp = stix2.EqualityComparisonExpression( + stix2.ObjectPath("windows-registry-key", ["key"]), + stix2.StringConstant("HKEY_LOCAL_MACHINE\\foo\\bar"), + ) + o_exp2 = stix2.ObservationExpression(reg_exp) + fb_exp = stix2.FollowedByObservationExpression([o_exp1, o_exp2]) + para_exp = stix2.ParentheticalExpression(fb_exp) + qual_exp = stix2.WithinQualifier(300) + exp = stix2.QualifiedObservationExpression(para_exp, qual_exp) + assert str(exp) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS" # noqa + + +def test_hash_followed_by_registryKey_expression(): + hash_exp = stix2.EqualityComparisonExpression( + "file:hashes.MD5", + stix2.HashConstant("79054025255fb1a26e4bc422aef54eb4", "MD5"), + ) + o_exp1 = stix2.ObservationExpression(hash_exp) + reg_exp = stix2.EqualityComparisonExpression( + stix2.ObjectPath("windows-registry-key", ["key"]), + stix2.StringConstant("HKEY_LOCAL_MACHINE\\foo\\bar"), + ) + o_exp2 = stix2.ObservationExpression(reg_exp) + fb_exp = stix2.FollowedByObservationExpression([o_exp1, o_exp2]) + para_exp = stix2.ParentheticalExpression(fb_exp) + qual_exp = stix2.WithinQualifier(stix2.IntegerConstant(300)) + exp = stix2.QualifiedObservationExpression(para_exp, qual_exp) + assert str(exp) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS" # noqa + + +def test_file_observable_expression(): + exp1 = stix2.EqualityComparisonExpression( + "file:hashes.'SHA-256'", + stix2.HashConstant( + "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", + 'SHA-256', + ), + ) + exp2 = stix2.EqualityComparisonExpression("file:mime_type", stix2.StringConstant("application/x-pdf")) + bool_exp = stix2.ObservationExpression(stix2.AndBooleanExpression([exp1, exp2])) + assert str(bool_exp) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f' AND file:mime_type = 'application/x-pdf']" # noqa + + +@pytest.mark.parametrize( + "observation_class, op", [ + (stix2.AndObservationExpression, 'AND'), + (stix2.OrObservationExpression, 'OR'), + ], +) +def test_multiple_file_observable_expression(observation_class, op): + exp1 = stix2.EqualityComparisonExpression( + "file:hashes.'SHA-256'", + stix2.HashConstant( + "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c", + 'SHA-256', + ), + ) + exp2 = stix2.EqualityComparisonExpression( + "file:hashes.MD5", + stix2.HashConstant("cead3f77f6cda6ec00f57d76c9a6879f", "MD5"), + ) + bool1_exp = stix2.OrBooleanExpression([exp1, exp2]) + exp3 = stix2.EqualityComparisonExpression( + "file:hashes.'SHA-256'", + stix2.HashConstant( + "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", + 'SHA-256', + ), + ) + op1_exp = stix2.ObservationExpression(bool1_exp) + op2_exp = stix2.ObservationExpression(exp3) + exp = observation_class([op1_exp, op2_exp]) + assert str(exp) == "[file:hashes.'SHA-256' = 'bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c' OR file:hashes.MD5 = 'cead3f77f6cda6ec00f57d76c9a6879f'] {} [file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']".format(op) # noqa + + +def test_root_types(): + ast = stix2.ObservationExpression( + stix2.AndBooleanExpression( + [ + stix2.ParentheticalExpression( + stix2.OrBooleanExpression([ + stix2.EqualityComparisonExpression("a:b", stix2.StringConstant("1")), + stix2.EqualityComparisonExpression("b:c", stix2.StringConstant("2")), + ]), + ), + stix2.EqualityComparisonExpression(u"b:d", stix2.StringConstant("3")), + ], + ), + ) + assert str(ast) == "[(a:b = '1' OR b:c = '2') AND b:d = '3']" + + +def test_artifact_payload(): + exp1 = stix2.EqualityComparisonExpression( + "artifact:mime_type", + "application/vnd.tcpdump.pcap", + ) + exp2 = stix2.MatchesComparisonExpression( + "artifact:payload_bin", + stix2.StringConstant("\\xd4\\xc3\\xb2\\xa1\\x02\\x00\\x04\\x00"), + ) + and_exp = stix2.ObservationExpression(stix2.AndBooleanExpression([exp1, exp2])) + assert str(and_exp) == "[artifact:mime_type = 'application/vnd.tcpdump.pcap' AND artifact:payload_bin MATCHES '\\\\xd4\\\\xc3\\\\xb2\\\\xa1\\\\x02\\\\x00\\\\x04\\\\x00']" # noqa + + +def test_greater_than_python_constant(): + exp1 = stix2.GreaterThanComparisonExpression("file:extensions.'windows-pebinary-ext'.sections[*].entropy", 7.0) + exp = stix2.ObservationExpression(exp1) + assert str(exp) == "[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.0]" + + +def test_greater_than(): + exp1 = stix2.GreaterThanComparisonExpression( + "file:extensions.'windows-pebinary-ext'.sections[*].entropy", + stix2.FloatConstant(7.0), + ) + exp = stix2.ObservationExpression(exp1) + assert str(exp) == "[file:extensions.'windows-pebinary-ext'.sections[*].entropy > 7.0]" + + +def test_less_than(): + exp = stix2.LessThanComparisonExpression("file:size", 1024) + assert str(exp) == "file:size < 1024" + + +def test_greater_than_or_equal(): + exp = stix2.GreaterThanEqualComparisonExpression( + "file:size", + 1024, + ) + + assert str(exp) == "file:size >= 1024" + + +def test_less_than_or_equal(): + exp = stix2.LessThanEqualComparisonExpression( + "file:size", + 1024, + ) + assert str(exp) == "file:size <= 1024" + + +def test_not(): + exp = stix2.LessThanComparisonExpression( + "file:size", + 1024, + negated=True, + ) + assert str(exp) == "file:size NOT < 1024" + + +def test_and_observable_expression(): + exp1 = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "user-account:account_type", + "unix", + ), + stix2.EqualityComparisonExpression( + "user-account:user_id", + stix2.StringConstant("1007"), + ), + stix2.EqualityComparisonExpression( + "user-account:account_login", + "Peter", + ), + ]) + exp2 = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "user-account:account_type", + "unix", + ), + stix2.EqualityComparisonExpression( + "user-account:user_id", + stix2.StringConstant("1008"), + ), + stix2.EqualityComparisonExpression( + "user-account:account_login", + "Paul", + ), + ]) + exp3 = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "user-account:account_type", + "unix", + ), + stix2.EqualityComparisonExpression( + "user-account:user_id", + stix2.StringConstant("1009"), + ), + stix2.EqualityComparisonExpression( + "user-account:account_login", + "Mary", + ), + ]) + exp = stix2.AndObservationExpression([ + stix2.ObservationExpression(exp1), + stix2.ObservationExpression(exp2), + stix2.ObservationExpression(exp3), + ]) + assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1009' AND user-account:account_login = 'Mary']" # noqa + + +def test_invalid_and_observable_expression(): + with pytest.raises(ValueError) as excinfo: + stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "user-account:display_name", + "admin", + ), + stix2.EqualityComparisonExpression( + "email-addr:display_name", + stix2.StringConstant("admin"), + ), + ]) + assert "All operands to an 'AND' expression must have the same object type" in str(excinfo) + + +def test_hex(): + exp_and = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "file:mime_type", + "image/bmp", + ), + stix2.EqualityComparisonExpression( + "file:magic_number_hex", + stix2.HexConstant("ffd8"), + ), + ]) + exp = stix2.ObservationExpression(exp_and) + assert str(exp) == "[file:mime_type = 'image/bmp' AND file:magic_number_hex = h'ffd8']" + + +def test_multiple_qualifiers(): + exp_and = stix2.AndBooleanExpression([ + stix2.EqualityComparisonExpression( + "network-traffic:dst_ref.type", + "domain-name", + ), + stix2.EqualityComparisonExpression( + "network-traffic:dst_ref.value", + "example.com", + ), + ]) + exp_ob = stix2.ObservationExpression(exp_and) + qual_rep = stix2.RepeatQualifier(5) + qual_within = stix2.WithinQualifier(stix2.IntegerConstant(1800)) + exp = stix2.QualifiedObservationExpression(stix2.QualifiedObservationExpression(exp_ob, qual_rep), qual_within) + assert str(exp) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS" # noqa + + +def test_set_op(): + exp = stix2.ObservationExpression(stix2.IsSubsetComparisonExpression( + "network-traffic:dst_ref.value", + "2001:0db8:dead:beef:0000:0000:0000:0000/64", + )) + assert str(exp) == "[network-traffic:dst_ref.value ISSUBSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']" + + +def test_timestamp(): + ts = stix2.TimestampConstant('2014-01-13T07:03:17Z') + assert str(ts) == "t'2014-01-13T07:03:17Z'" + + +def test_boolean(): + exp = stix2.EqualityComparisonExpression( + "email-message:is_multipart", + True, + ) + assert str(exp) == "email-message:is_multipart = true" + + +def test_binary(): + const = stix2.BinaryConstant("dGhpcyBpcyBhIHRlc3Q=") + exp = stix2.EqualityComparisonExpression( + "artifact:payload_bin", + const, + ) + assert str(exp) == "artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q='" + + +def test_list(): + exp = stix2.InComparisonExpression( + "process:name", + ['proccy', 'proximus', 'badproc'], + ) + assert str(exp) == "process:name IN ('proccy', 'proximus', 'badproc')" + + +def test_list2(): + # alternate way to construct an "IN" Comparison Expression + exp = stix2.EqualityComparisonExpression( + "process:name", + ['proccy', 'proximus', 'badproc'], + ) + assert str(exp) == "process:name IN ('proccy', 'proximus', 'badproc')" + + +def test_invalid_constant_type(): + with pytest.raises(ValueError) as excinfo: + stix2.EqualityComparisonExpression( + "artifact:payload_bin", + {'foo': 'bar'}, + ) + assert 'Unable to create a constant' in str(excinfo) + + +def test_invalid_integer_constant(): + with pytest.raises(ValueError) as excinfo: + stix2.IntegerConstant('foo') + assert 'must be an integer' in str(excinfo) + + +def test_invalid_timestamp_constant(): + with pytest.raises(ValueError) as excinfo: + stix2.TimestampConstant('foo') + assert 'Must be a datetime object or timestamp string' in str(excinfo) + + +def test_invalid_float_constant(): + with pytest.raises(ValueError) as excinfo: + stix2.FloatConstant('foo') + assert 'must be a float' in str(excinfo) + + +@pytest.mark.parametrize( + "data, result", [ + (True, True), + (False, False), + ('True', True), + ('False', False), + ('true', True), + ('false', False), + ('t', True), + ('f', False), + ('T', True), + ('F', False), + (1, True), + (0, False), + ], +) +def test_boolean_constant(data, result): + boolean = stix2.BooleanConstant(data) + assert boolean.value == result + + +def test_invalid_boolean_constant(): + with pytest.raises(ValueError) as excinfo: + stix2.BooleanConstant('foo') + assert 'must be a boolean' in str(excinfo) + + +@pytest.mark.parametrize( + "hashtype, data", [ + ('MD5', 'zzz'), + ('ssdeep', 'zzz=='), + ], +) +def test_invalid_hash_constant(hashtype, data): + with pytest.raises(ValueError) as excinfo: + stix2.HashConstant(data, hashtype) + assert 'is not a valid {} hash'.format(hashtype) in str(excinfo) + + +def test_invalid_hex_constant(): + with pytest.raises(ValueError) as excinfo: + stix2.HexConstant('mm') + assert "must contain an even number of hexadecimal characters" in str(excinfo) + + +def test_invalid_binary_constant(): + with pytest.raises(ValueError) as excinfo: + stix2.BinaryConstant('foo') + assert 'must contain a base64' in str(excinfo) + + +def test_escape_quotes_and_backslashes(): + exp = stix2.MatchesComparisonExpression( + "file:name", + "^Final Report.+\\.exe$", + ) + assert str(exp) == "file:name MATCHES '^Final Report.+\\\\.exe$'" + + +def test_like(): + exp = stix2.LikeComparisonExpression( + "directory:path", + "C:\\Windows\\%\\foo", + ) + assert str(exp) == "directory:path LIKE 'C:\\\\Windows\\\\%\\\\foo'" + + +def test_issuperset(): + exp = stix2.IsSupersetComparisonExpression( + "ipv4-addr:value", + "198.51.100.0/24", + ) + assert str(exp) == "ipv4-addr:value ISSUPERSET '198.51.100.0/24'" + + +def test_repeat_qualifier(): + qual = stix2.RepeatQualifier(stix2.IntegerConstant(5)) + assert str(qual) == 'REPEATS 5 TIMES' + + +def test_invalid_repeat_qualifier(): + with pytest.raises(ValueError) as excinfo: + stix2.RepeatQualifier('foo') + assert 'is not a valid argument for a Repeat Qualifier' in str(excinfo) + + +def test_invalid_within_qualifier(): + with pytest.raises(ValueError) as excinfo: + stix2.WithinQualifier('foo') + assert 'is not a valid argument for a Within Qualifier' in str(excinfo) + + +def test_startstop_qualifier(): + qual = stix2.StartStopQualifier( + stix2.TimestampConstant('2016-06-01T00:00:00Z'), + datetime.datetime(2017, 3, 12, 8, 30, 0), + ) + assert str(qual) == "START t'2016-06-01T00:00:00Z' STOP t'2017-03-12T08:30:00Z'" + + qual2 = stix2.StartStopQualifier( + datetime.date(2016, 6, 1), + stix2.TimestampConstant('2016-07-01T00:00:00Z'), + ) + assert str(qual2) == "START t'2016-06-01T00:00:00Z' STOP t'2016-07-01T00:00:00Z'" + + +def test_invalid_startstop_qualifier(): + with pytest.raises(ValueError) as excinfo: + stix2.StartStopQualifier( + 'foo', + stix2.TimestampConstant('2016-06-01T00:00:00Z'), + ) + assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo) + + with pytest.raises(ValueError) as excinfo: + stix2.StartStopQualifier( + datetime.date(2016, 6, 1), + 'foo', + ) + assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo) + + +def test_make_constant_already_a_constant(): + str_const = stix2.StringConstant('Foo') + result = stix2.patterns.make_constant(str_const) + assert result is str_const + + +def test_parsing_comparison_expression(): + patt_obj = create_pattern_object("[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']") + assert str(patt_obj) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']" + + +def test_parsing_qualified_expression(): + patt_obj = create_pattern_object( + "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS", + ) + assert str( + patt_obj, + ) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS" + + +def test_list_constant(): + patt_obj = create_pattern_object("[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]") + assert str(patt_obj) == "[network-traffic:src_ref.value IN ('10.0.0.0', '10.0.0.1', '10.0.0.2')]" diff --git a/stix2/test/v21/test_pickle.py b/stix2/test/v21/test_pickle.py new file mode 100644 index 0000000..0dc1c4c --- /dev/null +++ b/stix2/test/v21/test_pickle.py @@ -0,0 +1,17 @@ +import pickle + +import stix2 + + +def test_pickling(): + """ + Ensure a pickle/unpickle cycle works okay. + """ + identity = stix2.v21.Identity( + id="identity--d66cb89d-5228-4983-958c-fa84ef75c88c", + name="alice", + description="this is a pickle test", + identity_class="some_class", + ) + + pickle.loads(pickle.dumps(identity)) diff --git a/stix2/test/v21/test_properties.py b/stix2/test/v21/test_properties.py new file mode 100644 index 0000000..611ec5e --- /dev/null +++ b/stix2/test/v21/test_properties.py @@ -0,0 +1,507 @@ +import uuid + +import pytest + +import stix2 +from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError +from stix2.properties import ( + ERROR_INVALID_ID, BinaryProperty, BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, + HashesProperty, HexProperty, IDProperty, IntegerProperty, ListProperty, + Property, ReferenceProperty, StringProperty, TimestampProperty, + TypeProperty, +) +from stix2.v21.common import MarkingProperty + +from . import constants + + +def test_property(): + p = Property() + + assert p.required is False + assert p.clean('foo') == 'foo' + assert p.clean(3) == 3 + + +def test_basic_clean(): + class Prop(Property): + + def clean(self, value): + if value == 42: + return value + else: + raise ValueError("Must be 42") + + p = Prop() + + assert p.clean(42) == 42 + with pytest.raises(ValueError): + p.clean(41) + + +def test_property_default(): + class Prop(Property): + + def default(self): + return 77 + + p = Prop() + + assert p.default() == 77 + + +def test_fixed_property(): + p = Property(fixed="2.0") + + assert p.clean("2.0") + with pytest.raises(ValueError): + assert p.clean("x") is False + with pytest.raises(ValueError): + assert p.clean(2.0) is False + + assert p.default() == "2.0" + assert p.clean(p.default()) + + +def test_list_property(): + p = ListProperty(StringProperty) + + assert p.clean(['abc', 'xyz']) + with pytest.raises(ValueError): + p.clean([]) + + +def test_string_property(): + prop = StringProperty() + + assert prop.clean('foobar') + assert prop.clean(1) + assert prop.clean([1, 2, 3]) + + +def test_type_property(): + prop = TypeProperty('my-type') + + assert prop.clean('my-type') + with pytest.raises(ValueError): + prop.clean('not-my-type') + assert prop.clean(prop.default()) + + +ID_PROP = IDProperty('my-type') +MY_ID = 'my-type--232c9d3f-49fc-4440-bb01-607f638778e7' + + +@pytest.mark.parametrize( + "value", [ + MY_ID, + 'my-type--00000000-0000-4000-8000-000000000000', + ], +) +def test_id_property_valid(value): + assert ID_PROP.clean(value) == value + + +CONSTANT_IDS = [ + constants.ATTACK_PATTERN_ID, + constants.CAMPAIGN_ID, + constants.COURSE_OF_ACTION_ID, + constants.IDENTITY_ID, + constants.INDICATOR_ID, + constants.INTRUSION_SET_ID, + constants.MALWARE_ID, + constants.MARKING_DEFINITION_ID, + constants.OBSERVED_DATA_ID, + constants.RELATIONSHIP_ID, + constants.REPORT_ID, + constants.SIGHTING_ID, + constants.THREAT_ACTOR_ID, + constants.TOOL_ID, + constants.VULNERABILITY_ID, +] +CONSTANT_IDS.extend(constants.MARKING_IDS) +CONSTANT_IDS.extend(constants.RELATIONSHIP_IDS) + + +@pytest.mark.parametrize("value", CONSTANT_IDS) +def test_id_property_valid_for_type(value): + type = value.split('--', 1)[0] + assert IDProperty(type=type).clean(value) == value + + +def test_id_property_wrong_type(): + with pytest.raises(ValueError) as excinfo: + ID_PROP.clean('not-my-type--232c9d3f-49fc-4440-bb01-607f638778e7') + assert str(excinfo.value) == "must start with 'my-type--'." + + +@pytest.mark.parametrize( + "value", [ + 'my-type--foo', + # Not a v4 UUID + 'my-type--00000000-0000-0000-0000-000000000000', + 'my-type--' + str(uuid.uuid1()), + 'my-type--' + str(uuid.uuid3(uuid.NAMESPACE_DNS, "example.org")), + 'my-type--' + str(uuid.uuid5(uuid.NAMESPACE_DNS, "example.org")), + ], +) +def test_id_property_not_a_valid_hex_uuid(value): + with pytest.raises(ValueError) as excinfo: + ID_PROP.clean(value) + assert str(excinfo.value) == ERROR_INVALID_ID + + +def test_id_property_default(): + default = ID_PROP.default() + assert ID_PROP.clean(default) == default + + +@pytest.mark.parametrize( + "value", [ + 2, + -1, + 3.14, + False, + ], +) +def test_integer_property_valid(value): + int_prop = IntegerProperty() + assert int_prop.clean(value) is not None + + +@pytest.mark.parametrize( + "value", [ + -1, + -100, + -50 * 6, + ], +) +def test_integer_property_invalid_min_with_constraints(value): + int_prop = IntegerProperty(min=0, max=180) + with pytest.raises(ValueError) as excinfo: + int_prop.clean(value) + assert "minimum value is" in str(excinfo.value) + + +@pytest.mark.parametrize( + "value", [ + 181, + 200, + 50 * 6, + ], +) +def test_integer_property_invalid_max_with_constraints(value): + int_prop = IntegerProperty(min=0, max=180) + with pytest.raises(ValueError) as excinfo: + int_prop.clean(value) + assert "maximum value is" in str(excinfo.value) + + +@pytest.mark.parametrize( + "value", [ + "something", + StringProperty(), + ], +) +def test_integer_property_invalid(value): + int_prop = IntegerProperty() + with pytest.raises(ValueError): + int_prop.clean(value) + + +@pytest.mark.parametrize( + "value", [ + 2, + -1, + 3.14, + False, + ], +) +def test_float_property_valid(value): + int_prop = FloatProperty() + assert int_prop.clean(value) is not None + + +@pytest.mark.parametrize( + "value", [ + "something", + StringProperty(), + ], +) +def test_float_property_invalid(value): + int_prop = FloatProperty() + with pytest.raises(ValueError): + int_prop.clean(value) + + +@pytest.mark.parametrize( + "value", [ + True, + False, + 'True', + 'False', + 'true', + 'false', + 'TRUE', + 'FALSE', + 'T', + 'F', + 't', + 'f', + 1, + 0, + ], +) +def test_boolean_property_valid(value): + bool_prop = BooleanProperty() + + assert bool_prop.clean(value) is not None + + +@pytest.mark.parametrize( + "value", [ + 'abc', + ['false'], + {'true': 'true'}, + 2, + -1, + ], +) +def test_boolean_property_invalid(value): + bool_prop = BooleanProperty() + with pytest.raises(ValueError): + bool_prop.clean(value) + + +def test_reference_property(): + ref_prop = ReferenceProperty() + + assert ref_prop.clean("my-type--00000000-0000-4000-8000-000000000000") + with pytest.raises(ValueError): + ref_prop.clean("foo") + + # This is not a valid V4 UUID + with pytest.raises(ValueError): + ref_prop.clean("my-type--00000000-0000-0000-0000-000000000000") + + +@pytest.mark.parametrize( + "value", [ + '2017-01-01T12:34:56Z', + '2017-01-01 12:34:56', + 'Jan 1 2017 12:34:56', + ], +) +def test_timestamp_property_valid(value): + ts_prop = TimestampProperty() + assert ts_prop.clean(value) == constants.FAKE_TIME + + +def test_timestamp_property_invalid(): + ts_prop = TimestampProperty() + with pytest.raises(ValueError): + ts_prop.clean(1) + with pytest.raises(ValueError): + ts_prop.clean("someday sometime") + + +def test_binary_property(): + bin_prop = BinaryProperty() + + assert bin_prop.clean("TG9yZW0gSXBzdW0=") + with pytest.raises(ValueError): + bin_prop.clean("foobar") + + +def test_hex_property(): + hex_prop = HexProperty() + + assert hex_prop.clean("4c6f72656d20497073756d") + with pytest.raises(ValueError): + hex_prop.clean("foobar") + + +@pytest.mark.parametrize( + "d", [ + {'description': 'something'}, + [('abc', 1), ('bcd', 2), ('cde', 3)], + ], +) +def test_dictionary_property_valid(d): + dict_prop = DictionaryProperty(spec_version='2.1') + assert dict_prop.clean(d) + + +@pytest.mark.parametrize( + "d", [ + [{'a': 'something'}, "Invalid dictionary key a: (shorter than 3 characters)."], + ], +) +def test_dictionary_no_longer_raises(d): + dict_prop = DictionaryProperty(spec_version='2.1') + dict_prop.clean(d[0]) + + +@pytest.mark.parametrize( + "d", [ + [ + {'a'*300: 'something'}, "Invalid dictionary key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaa: (longer than 250 characters).", + ], + [ + {'Hey!': 'something'}, "Invalid dictionary key Hey!: (contains characters other than lowercase a-z, " + "uppercase A-Z, numerals 0-9, hyphen (-), or underscore (_)).", + ], + ], +) +def test_dictionary_property_invalid_key(d): + dict_prop = DictionaryProperty(spec_version='2.1') + + with pytest.raises(DictionaryKeyError) as excinfo: + dict_prop.clean(d[0]) + + assert str(excinfo.value) == d[1] + + +@pytest.mark.parametrize( + "d", [ + ({}, "The dictionary property must contain a non-empty dictionary"), + # TODO: This error message could be made more helpful. The error is caused + # because `json.loads()` doesn't like the *single* quotes around the key + # name, even though they are valid in a Python dictionary. While technically + # accurate (a string is not a dictionary), if we want to be able to load + # string-encoded "dictionaries" that are, we need a better error message + # or an alternative to `json.loads()` ... and preferably *not* `eval()`. :-) + # Changing the following to `'{"description": "something"}'` does not cause + # any ValueError to be raised. + ("{'description': 'something'}", "The dictionary property must contain a dictionary"), + ], +) +def test_dictionary_property_invalid(d): + dict_prop = DictionaryProperty(spec_version='2.1') + + with pytest.raises(ValueError) as excinfo: + dict_prop.clean(d[0]) + assert str(excinfo.value) == d[1] + + +def test_property_list_of_dictionary(): + @stix2.v21.CustomObject( + 'x-new-obj', [ + ('property1', ListProperty(DictionaryProperty(spec_version='2.1'), required=True)), + ], + ) + class NewObj(): + pass + + test_obj = NewObj(property1=[{'foo': 'bar'}]) + assert test_obj.property1[0]['foo'] == 'bar' + + +@pytest.mark.parametrize( + "value", [ + {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, + [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], + ], +) +def test_hashes_property_valid(value): + hash_prop = HashesProperty() + assert hash_prop.clean(value) + + +@pytest.mark.parametrize( + "value", [ + {"MD5": "a"}, + {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, + ], +) +def test_hashes_property_invalid(value): + hash_prop = HashesProperty() + + with pytest.raises(ValueError): + hash_prop.clean(value) + + +def test_embedded_property(): + emb_prop = EmbeddedObjectProperty(type=stix2.v21.EmailMIMEComponent) + mime = stix2.v21.EmailMIMEComponent( + content_type="text/plain; charset=utf-8", + content_disposition="inline", + body="Cats are funny!", + ) + assert emb_prop.clean(mime) + + with pytest.raises(ValueError): + emb_prop.clean("string") + + +@pytest.mark.parametrize( + "value", [ + ['a', 'b', 'c'], + ('a', 'b', 'c'), + 'b', + ], +) +def test_enum_property_valid(value): + enum_prop = EnumProperty(value) + assert enum_prop.clean('b') + + +def test_enum_property_invalid(): + enum_prop = EnumProperty(['a', 'b', 'c']) + with pytest.raises(ValueError): + enum_prop.clean('z') + + +def test_extension_property_valid(): + ext_prop = ExtensionsProperty(spec_version='2.1', enclosing_type='file') + assert ext_prop({ + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }) + + +@pytest.mark.parametrize( + "data", [ + 1, + {'foobar-ext': { + 'pe_type': 'exe', + }}, + ], +) +def test_extension_property_invalid(data): + ext_prop = ExtensionsProperty(spec_version='2.1', enclosing_type='file') + with pytest.raises(ValueError): + ext_prop.clean(data) + + +def test_extension_property_invalid_type(): + ext_prop = ExtensionsProperty(spec_version='2.1', enclosing_type='indicator') + with pytest.raises(ValueError) as excinfo: + ext_prop.clean( + { + 'windows-pebinary-ext': { + 'pe_type': 'exe', + }, + }, + ) + assert "Can't parse unknown extension" in str(excinfo.value) + + +def test_extension_at_least_one_property_constraint(): + with pytest.raises(AtLeastOnePropertyError): + stix2.v21.TCPExt() + + +def test_marking_property_error(): + mark_prop = MarkingProperty() + + with pytest.raises(ValueError) as excinfo: + mark_prop.clean('my-marking') + + assert str(excinfo.value) == "must be a Statement, TLP Marking or a registered marking." diff --git a/stix2/test/v21/test_relationship.py b/stix2/test/v21/test_relationship.py new file mode 100644 index 0000000..0ec3e08 --- /dev/null +++ b/stix2/test/v21/test_relationship.py @@ -0,0 +1,208 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import ( + FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID, RELATIONSHIP_KWARGS, +) + +EXPECTED_RELATIONSHIP = """{ + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--df7c87eb-75d2-4948-af81-9d49d246f301", + "created": "2016-04-06T20:06:37.000Z", + "modified": "2016-04-06T20:06:37.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e" +}""" + + +def test_relationship_all_required_properties(): + now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + + rel = stix2.v21.Relationship( + type='relationship', + id=RELATIONSHIP_ID, + created=now, + modified=now, + relationship_type='indicates', + source_ref=INDICATOR_ID, + target_ref=MALWARE_ID, + ) + assert str(rel) == EXPECTED_RELATIONSHIP + + +def test_relationship_autogenerated_properties(relationship): + assert relationship.type == 'relationship' + assert relationship.spec_version == '2.1' + assert relationship.id == 'relationship--00000000-0000-4000-8000-000000000001' + assert relationship.created == FAKE_TIME + assert relationship.modified == FAKE_TIME + assert relationship.relationship_type == 'indicates' + assert relationship.source_ref == INDICATOR_ID + assert relationship.target_ref == MALWARE_ID + + assert relationship['type'] == 'relationship' + assert relationship['spec_version'] == '2.1' + assert relationship['id'] == 'relationship--00000000-0000-4000-8000-000000000001' + assert relationship['created'] == FAKE_TIME + assert relationship['modified'] == FAKE_TIME + assert relationship['relationship_type'] == 'indicates' + assert relationship['source_ref'] == INDICATOR_ID + assert relationship['target_ref'] == MALWARE_ID + + +def test_relationship_type_must_be_relationship(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Relationship(type='xxx', **RELATIONSHIP_KWARGS) + + assert excinfo.value.cls == stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Relationship(id='my-prefix--', **RELATIONSHIP_KWARGS) + + assert excinfo.value.cls == stix2.v21.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_property_relationship_type(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.Relationship() + assert excinfo.value.cls == stix2.v21.Relationship + assert excinfo.value.properties == ["relationship_type", "source_ref", "target_ref"] + + +def test_relationship_missing_some_required_properties(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.Relationship(relationship_type='indicates') + + assert excinfo.value.cls == stix2.v21.Relationship + assert excinfo.value.properties == ["source_ref", "target_ref"] + + +def test_relationship_required_properties_target_ref(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.v21.Relationship( + relationship_type='indicates', + source_ref=INDICATOR_ID, + ) + + assert excinfo.value.cls == stix2.v21.Relationship + assert excinfo.value.properties == ["target_ref"] + + +def test_cannot_assign_to_relationship_attributes(relationship): + with pytest.raises(stix2.exceptions.ImmutableError) as excinfo: + relationship.relationship_type = "derived-from" + + assert str(excinfo.value) == "Cannot modify 'relationship_type' property in 'Relationship' after creation." + + +def test_invalid_kwarg_to_relationship(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.v21.Relationship(my_custom_property="foo", **RELATIONSHIP_KWARGS) + + assert excinfo.value.cls == stix2.v21.Relationship + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Relationship: (my_custom_property)." + + +def test_create_relationship_from_objects_rather_than_ids1(indicator, malware): + rel = stix2.v21.Relationship( + relationship_type="indicates", + source_ref=indicator, + target_ref=malware, + stop_time="2016-04-06T20:03:48Z", + ) + + assert rel.relationship_type == 'indicates' + assert rel.source_ref == 'indicator--00000000-0000-4000-8000-000000000001' + assert rel.target_ref == 'malware--00000000-0000-4000-8000-000000000003' + assert rel.id == 'relationship--00000000-0000-4000-8000-000000000005' + assert rel.stop_time == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + + +def test_create_relationship_from_objects_rather_than_ids2(indicator, malware): + rel = stix2.v21.Relationship( + relationship_type="indicates", + source_ref=indicator, + target_ref=malware, + start_time="2016-04-06T20:03:48Z", + ) + + assert rel.relationship_type == 'indicates' + assert rel.source_ref == 'indicator--00000000-0000-4000-8000-000000000001' + assert rel.target_ref == 'malware--00000000-0000-4000-8000-000000000003' + assert rel.id == 'relationship--00000000-0000-4000-8000-000000000005' + assert rel.start_time == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + + +def test_create_relationship_with_positional_args(indicator, malware): + rel = stix2.v21.Relationship(indicator, 'indicates', malware) + + assert rel.relationship_type == 'indicates' + assert rel.source_ref == 'indicator--00000000-0000-4000-8000-000000000001' + assert rel.target_ref == 'malware--00000000-0000-4000-8000-000000000003' + assert rel.id == 'relationship--00000000-0000-4000-8000-000000000005' + + +@pytest.mark.parametrize( + "data", [ + EXPECTED_RELATIONSHIP, + { + "created": "2016-04-06T20:06:37Z", + "id": "relationship--df7c87eb-75d2-4948-af81-9d49d246f301", + "modified": "2016-04-06T20:06:37Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + "spec_version": "2.1", + "type": "relationship", + }, + ], +) +def test_parse_relationship(data): + rel = stix2.parse(data, version="2.1") + + assert rel.type == 'relationship' + assert rel.spec_version == '2.1' + assert rel.id == RELATIONSHIP_ID + assert rel.created == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert rel.modified == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert rel.relationship_type == "indicates" + assert rel.source_ref == "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7" + assert rel.target_ref == "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e" + + +@pytest.mark.parametrize( + "data", [ + { + "created": "2016-04-06T20:06:37Z", + "id": "relationship--df7c87eb-75d2-4948-af81-9d49d246f301", + "modified": "2016-04-06T20:06:37Z", + "relationship_type": "indicates", + "source_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "target_ref": "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e", + "start_time": "2018-04-06T20:06:37Z", + "stop_time": "2016-04-06T20:06:37Z", + "spec_version": "2.1", + "type": "relationship", + }, + ], +) +def test_parse_relationship_with_wrong_start_and_stop_time(data): + with pytest.raises(ValueError) as excinfo: + stix2.parse(data) + + assert str(excinfo.value) == "{id} 'stop_time' must be later than 'start_time'".format(**data) diff --git a/stix2/test/v21/test_report.py b/stix2/test/v21/test_report.py new file mode 100644 index 0000000..c9d790e --- /dev/null +++ b/stix2/test/v21/test_report.py @@ -0,0 +1,137 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import INDICATOR_KWARGS, REPORT_ID + +EXPECTED = """{ + "type": "report", + "spec_version": "2.1", + "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", + "report_types": [ + "campaign" + ], + "published": "2016-01-20T17:00:00Z", + "object_refs": [ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ] +}""" + + +def test_report_example(): + report = stix2.v21.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-20T17:00:00Z", + report_types=["campaign"], + object_refs=[ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", + ], + ) + + assert str(report) == EXPECTED + + +def test_report_example_objects_in_object_refs(): + report = stix2.v21.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-20T17:00:00Z", + report_types=["campaign"], + object_refs=[ + stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.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-20T17:00:00Z", + report_types=["campaign"], + object_refs=[ + stix2.v21.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 excinfo.value.cls == stix2.v21.Report + assert excinfo.value.prop_name == "object_refs" + assert excinfo.value.reason == stix2.properties.ERROR_INVALID_ID + assert str(excinfo.value) == "Invalid value for Report 'object_refs': " + stix2.properties.ERROR_INVALID_ID + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2015-12-21T19:59:11.000Z", + "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", + "description": "A simple report with an indicator and campaign", + "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", + "report_types": [ + "campaign", + ], + "modified": "2015-12-21T19:59:11.000Z", + "name": "The Black Vine Cyberespionage Group", + "object_refs": [ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", + ], + "published": "2016-01-20T17:00:00Z", + "spec_version": "2.1", + "type": "report", + }, + ], +) +def test_parse_report(data): + rept = stix2.parse(data, version="2.1") + + assert rept.type == 'report' + assert rept.spec_version == '2.1' + assert rept.id == REPORT_ID + assert rept.created == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert rept.modified == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert rept.created_by_ref == "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283" + assert rept.object_refs == [ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a", + ] + assert rept.description == "A simple report with an indicator and campaign" + assert rept.report_types == ["campaign"] + assert rept.name == "The Black Vine Cyberespionage Group" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_sighting.py b/stix2/test/v21/test_sighting.py new file mode 100644 index 0000000..8fcbb6d --- /dev/null +++ b/stix2/test/v21/test_sighting.py @@ -0,0 +1,119 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import INDICATOR_ID, SIGHTING_ID, SIGHTING_KWARGS + +EXPECTED_SIGHTING = """{ + "type": "sighting", + "spec_version": "2.1", + "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", + "created": "2016-04-06T20:06:37.000Z", + "modified": "2016-04-06T20:06:37.000Z", + "sighting_of_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "where_sighted_refs": [ + "identity--8cc7afd6-5455-4d2b-a736-e614ee631d99" + ] +}""" + +BAD_SIGHTING = """{ + "created": "2016-04-06T20:06:37.000Z", + "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", + "modified": "2016-04-06T20:06:37.000Z", + "sighting_of_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "spec_version": "2.1", + "type": "sighting", + "where_sighted_refs": [ + "malware--8cc7afd6-5455-4d2b-a736-e614ee631d99" + ] +}""" + + +def test_sighting_all_required_properties(): + now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + + s = stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.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 excinfo.value.cls == stix2.v21.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(stix2.exceptions.InvalidValueError) as excinfo: + stix2.v21.Sighting(type='xxx', **SIGHTING_KWARGS) + + assert excinfo.value.cls == stix2.v21.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(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.v21.Sighting(my_custom_property="foo", **SIGHTING_KWARGS) + + assert excinfo.value.cls == stix2.v21.Sighting + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Sighting: (my_custom_property)." + + +def test_create_sighting_from_objects_rather_than_ids(malware): # noqa: F811 + rel = stix2.v21.Sighting(sighting_of_ref=malware) + + assert rel.sighting_of_ref == 'malware--00000000-0000-4000-8000-000000000001' + assert rel.id == 'sighting--00000000-0000-4000-8000-000000000003' + + +@pytest.mark.parametrize( + "data", [ + 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--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", + "spec_version": "2.1", + "type": "sighting", + "where_sighted_refs": [ + "identity--8cc7afd6-5455-4d2b-a736-e614ee631d99", + ], + }, + ], +) +def test_parse_sighting(data): + sighting = stix2.parse(data, version="2.1") + + assert sighting.type == 'sighting' + assert sighting.spec_version == '2.1' + assert sighting.id == SIGHTING_ID + assert sighting.created == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert sighting.modified == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert sighting.sighting_of_ref == "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7" + assert sighting.where_sighted_refs == ["identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"] diff --git a/stix2/test/v21/test_threat_actor.py b/stix2/test/v21/test_threat_actor.py new file mode 100644 index 0000000..a7a29f8 --- /dev/null +++ b/stix2/test/v21/test_threat_actor.py @@ -0,0 +1,70 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import THREAT_ACTOR_ID + +EXPECTED = """{ + "type": "threat-actor", + "spec_version": "2.1", + "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Evil Org", + "description": "The Evil Org threat actor group", + "threat_actor_types": [ + "crime-syndicate" + ] +}""" + + +def test_threat_actor_example(): + threat_actor = stix2.v21.ThreatActor( + id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", + name="Evil Org", + description="The Evil Org threat actor group", + threat_actor_types=["crime-syndicate"], + ) + + assert str(threat_actor) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "The Evil Org threat actor group", + "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "threat_actor_types": [ + "crime-syndicate", + ], + "modified": "2016-04-06T20:03:48.000Z", + "name": "Evil Org", + "spec_version": "2.1", + "type": "threat-actor", + }, + ], +) +def test_parse_threat_actor(data): + actor = stix2.parse(data, version="2.1") + + assert actor.type == 'threat-actor' + assert actor.spec_version == '2.1' + assert actor.id == THREAT_ACTOR_ID + assert actor.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert actor.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert actor.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert actor.description == "The Evil Org threat actor group" + assert actor.name == "Evil Org" + assert actor.threat_actor_types == ["crime-syndicate"] + +# TODO: Add other examples diff --git a/stix2/test/v21/test_tool.py b/stix2/test/v21/test_tool.py new file mode 100644 index 0000000..9258a23 --- /dev/null +++ b/stix2/test/v21/test_tool.py @@ -0,0 +1,100 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import TOOL_ID + +EXPECTED = """{ + "type": "tool", + "spec_version": "2.1", + "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "name": "VNC", + "tool_types": [ + "remote-access" + ] +}""" + +EXPECTED_WITH_REVOKED = """{ + "type": "tool", + "spec_version": "2.1", + "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "name": "VNC", + "tool_types": [ + "remote-access" + ], + "revoked": false +}""" + + +def test_tool_example(): + tool = stix2.v21.Tool( + id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", + name="VNC", + tool_types=["remote-access"], + ) + + assert str(tool) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "tool_types": [ + "remote-access", + ], + "modified": "2016-04-06T20:03:48Z", + "name": "VNC", + "spec_version": "2.1", + "type": "tool", + }, + ], +) +def test_parse_tool(data): + tool = stix2.parse(data, version="2.1") + + assert tool.type == 'tool' + assert tool.spec_version == '2.1' + assert tool.id == TOOL_ID + assert tool.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert tool.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert tool.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert tool.tool_types == ["remote-access"] + assert tool.name == "VNC" + + +def test_tool_no_workbench_wrappers(): + tool = stix2.v21.Tool(name='VNC', tool_types=['remote-access']) + with pytest.raises(AttributeError): + tool.created_by() + + +def test_tool_serialize_with_defaults(): + tool = stix2.v21.Tool( + id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", + name="VNC", + tool_types=["remote-access"], + ) + + assert tool.serialize(pretty=True, include_optional_defaults=True) == EXPECTED_WITH_REVOKED + + +# TODO: Add other examples diff --git a/stix2/test/v21/test_utils.py b/stix2/test/v21/test_utils.py new file mode 100644 index 0000000..96a06d3 --- /dev/null +++ b/stix2/test/v21/test_utils.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- + +import datetime as dt +from io import StringIO + +import pytest +import pytz + +import stix2.utils + +amsterdam = pytz.timezone('Europe/Amsterdam') +eastern = pytz.timezone('US/Eastern') + + +@pytest.mark.parametrize( + 'dttm, timestamp', [ + (dt.datetime(2017, 1, 1, tzinfo=pytz.utc), '2017-01-01T00:00:00Z'), + (amsterdam.localize(dt.datetime(2017, 1, 1)), '2016-12-31T23:00:00Z'), + (eastern.localize(dt.datetime(2017, 1, 1, 12, 34, 56)), '2017-01-01T17:34:56Z'), + (eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'), + (dt.datetime(2017, 7, 1), '2017-07-01T00:00:00Z'), + (dt.datetime(2017, 7, 1, 0, 0, 0, 1), '2017-07-01T00:00:00.000001Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='millisecond'), '2017-07-01T00:00:00.000Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='second'), '2017-07-01T00:00:00Z'), + ], +) +def test_timestamp_formatting(dttm, timestamp): + assert stix2.utils.format_datetime(dttm) == timestamp + + +@pytest.mark.parametrize( + 'timestamp, dttm', [ + (dt.datetime(2017, 1, 1, 0, tzinfo=pytz.utc), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + (dt.date(2017, 1, 1), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T00:00:00Z', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T02:00:00+2:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T00:00:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ], +) +def test_parse_datetime(timestamp, dttm): + assert stix2.utils.parse_into_datetime(timestamp) == dttm + + +@pytest.mark.parametrize( + 'timestamp, dttm, precision', [ + ('2017-01-01T01:02:03.000001', dt.datetime(2017, 1, 1, 1, 2, 3, 0, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.001', dt.datetime(2017, 1, 1, 1, 2, 3, 1000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.1', dt.datetime(2017, 1, 1, 1, 2, 3, 100000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, 450000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, tzinfo=pytz.utc), 'second'), + ], +) +def test_parse_datetime_precision(timestamp, dttm, precision): + assert stix2.utils.parse_into_datetime(timestamp, precision) == dttm + + +@pytest.mark.parametrize( + 'ts', [ + 'foobar', + 1, + ], +) +def test_parse_datetime_invalid(ts): + with pytest.raises(ValueError): + stix2.utils.parse_into_datetime('foobar') + + +@pytest.mark.parametrize( + 'data', [ + {"a": 1}, + '{"a": 1}', + StringIO(u'{"a": 1}'), + [("a", 1,)], + ], +) +def test_get_dict(data): + assert stix2.utils._get_dict(data) + + +@pytest.mark.parametrize( + 'data', [ + 1, + [1], + ['a', 1], + "foobar", + ], +) +def test_get_dict_invalid(data): + with pytest.raises(ValueError): + stix2.utils._get_dict(data) + + +@pytest.mark.parametrize( + 'stix_id, type', [ + ('malware--d69c8146-ab35-4d50-8382-6fc80e641d43', 'malware'), + ('intrusion-set--899ce53f-13a0-479b-a0e4-67d46e241542', 'intrusion-set'), + ], +) +def test_get_type_from_id(stix_id, type): + assert stix2.utils.get_type_from_id(stix_id) == type + + +def test_deduplicate(stix_objs1): + unique = stix2.utils.deduplicate(stix_objs1) + + # Only 3 objects are unique + # 2 id's vary + # 2 modified times vary for a particular id + + assert len(unique) == 3 + + ids = [obj['id'] for obj in unique] + mods = [obj['modified'] for obj in unique] + + assert "indicator--00000000-0000-4000-8000-000000000001" in ids + assert "indicator--00000000-0000-4000-8000-000000000001" in ids + assert "2017-01-27T13:49:53.935Z" in mods + assert "2017-01-27T13:49:53.936Z" in mods + + +@pytest.mark.parametrize( + 'object, tuple_to_find, expected_index', [ + ( + stix2.v21.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file", + }, + "1": { + "type": "ipv4-addr", + "value": "198.51.100.3", + }, + "2": { + "type": "network-traffic", + "src_ref": "1", + "protocols": [ + "tcp", + "http", + ], + "extensions": { + "http-request-ext": { + "request_method": "get", + "request_value": "/download.html", + "request_version": "http/1.1", + "request_header": { + "Accept-Encoding": "gzip,deflate", + "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", + "Host": "www.example.com", + }, + }, + }, + }, + }, + ), ('1', {"type": "ipv4-addr", "value": "198.51.100.3"}), 1, + ), + ( + { + "type": "x-example", + "id": "x-example--d5413db2-c26c-42e0-b0e0-ec800a310bfb", + "created": "2018-06-11T01:25:22.063Z", + "modified": "2018-06-11T01:25:22.063Z", + "dictionary": { + "key": { + "key_one": "value", + "key_two": "value", + }, + }, + }, ('key', {'key_one': 'value', 'key_two': 'value'}), 0, + ), + ( + { + "type": "language-content", + "spec_version": "2.1", + "id": "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d", + "created": "2017-02-08T21:31:22.007Z", + "modified": "2017-02-08T21:31:22.007Z", + "object_ref": "campaign--12a111f0-b824-4baf-a224-83b80237a094", + "object_modified": "2017-02-08T21:31:22.007Z", + "contents": { + "de": { + "name": "Bank Angriff 1", + "description": "Weitere Informationen über Banküberfall", + }, + "fr": { + "name": "Attaque Bank 1", + "description": "Plus d'informations sur la crise bancaire", + }, + }, + }, ('fr', {"name": "Attaque Bank 1", "description": "Plus d'informations sur la crise bancaire"}), 1, + ), + ], +) +def test_find_property_index(object, tuple_to_find, expected_index): + assert stix2.utils.find_property_index( + object, + *tuple_to_find + ) == expected_index + + +@pytest.mark.parametrize( + 'dict_value, tuple_to_find, expected_index', [ + ( + { + "contents": { + "de": { + "name": "Bank Angriff 1", + "description": "Weitere Informationen über Banküberfall", + }, + "fr": { + "name": "Attaque Bank 1", + "description": "Plus d'informations sur la crise bancaire", + }, + "es": { + "name": "Ataque al Banco", + "description": "Mas informacion sobre el ataque al banco", + }, + }, + }, ('es', {"name": "Ataque al Banco", "description": "Mas informacion sobre el ataque al banco"}), 1, + ), # Sorted alphabetically + ( + { + 'my_list': [ + {"key_one": 1}, + {"key_two": 2}, + ], + }, ('key_one', 1), 0, + ), + ], +) +def test_iterate_over_values(dict_value, tuple_to_find, expected_index): + assert stix2.utils._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index diff --git a/stix2/test/v21/test_versioning.py b/stix2/test/v21/test_versioning.py new file mode 100644 index 0000000..a7f4a2f --- /dev/null +++ b/stix2/test/v21/test_versioning.py @@ -0,0 +1,265 @@ +import pytest + +import stix2 + +from .constants import CAMPAIGN_MORE_KWARGS + + +def test_making_new_version(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + + campaign_v2 = campaign_v1.new_version(name="fred") + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.spec_version == campaign_v2.spec_version + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name != campaign_v2.name + assert campaign_v2.name == "fred" + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + + +def test_making_new_version_with_unset(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + + campaign_v2 = campaign_v1.new_version(description=None) + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.spec_version == campaign_v2.spec_version + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name == campaign_v2.name + with pytest.raises(AttributeError): + assert campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + + +def test_making_new_version_with_embedded_object(): + campaign_v1 = stix2.v21.Campaign( + external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-163", + }], + **CAMPAIGN_MORE_KWARGS + ) + + campaign_v2 = campaign_v1.new_version(external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-164", + }]) + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.spec_version == campaign_v2.spec_version + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name == campaign_v2.name + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + assert campaign_v1.external_references[0].external_id != campaign_v2.external_references[0].external_id + + +def test_revoke(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + + campaign_v2 = campaign_v1.revoke() + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.spec_version == campaign_v2.spec_version + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name == campaign_v2.name + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + + assert campaign_v2.revoked + + +def test_versioning_error_invalid_property(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + + with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as excinfo: + campaign_v1.new_version(type="threat-actor") + + assert str(excinfo.value) == "These properties cannot be changed when making a new version: type." + + +def test_versioning_error_bad_modified_value(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + campaign_v1.new_version(modified="2015-04-06T20:03:00.000Z") + + assert excinfo.value.cls == stix2.v21.Campaign + assert excinfo.value.prop_name == "modified" + assert excinfo.value.reason == ( + "The new modified datetime cannot be before than or equal to the current modified datetime." + "It cannot be equal, as according to STIX 2 specification, objects that are different " + "but have the same id and modified timestamp do not have defined consumer behavior." + ) + + msg = "Invalid value for {0} '{1}': {2}" + msg = msg.format( + stix2.v21.Campaign.__name__, "modified", + "The new modified datetime cannot be before than or equal to the current modified datetime." + "It cannot be equal, as according to STIX 2 specification, objects that are different " + "but have the same id and modified timestamp do not have defined consumer behavior.", + ) + assert str(excinfo.value) == msg + + +def test_versioning_error_usetting_required_property(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + campaign_v1.new_version(name=None) + + assert excinfo.value.cls == stix2.v21.Campaign + assert excinfo.value.properties == ["name"] + + msg = "No values for required properties for {0}: ({1})." + msg = msg.format(stix2.v21.Campaign.__name__, "name") + assert str(excinfo.value) == msg + + +def test_versioning_error_new_version_of_revoked(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v2 = campaign_v1.revoke() + + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: + campaign_v2.new_version(name="barney") + assert str(excinfo.value) == "Cannot create a new version of a revoked object." + + assert excinfo.value.called_by == "new_version" + assert str(excinfo.value) == "Cannot create a new version of a revoked object." + + +def test_versioning_error_revoke_of_revoked(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v2 = campaign_v1.revoke() + + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: + campaign_v2.revoke() + assert str(excinfo.value) == "Cannot revoke an already revoked object." + + assert excinfo.value.called_by == "revoke" + assert str(excinfo.value) == "Cannot revoke an already revoked object." + + +def test_making_new_version_dict(): + campaign_v1 = CAMPAIGN_MORE_KWARGS + campaign_v2 = stix2.utils.new_version(CAMPAIGN_MORE_KWARGS, name="fred") + + assert campaign_v1['id'] == campaign_v2['id'] + assert campaign_v1['spec_version'] == campaign_v2['spec_version'] + assert campaign_v1['created_by_ref'] == campaign_v2['created_by_ref'] + assert campaign_v1['created'] == campaign_v2['created'] + assert campaign_v1['name'] != campaign_v2['name'] + assert campaign_v2['name'] == "fred" + assert campaign_v1['description'] == campaign_v2['description'] + assert stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified'] + + +def test_versioning_error_dict_bad_modified_value(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.utils.new_version(CAMPAIGN_MORE_KWARGS, modified="2015-04-06T20:03:00.000Z") + + assert excinfo.value.cls == dict + assert excinfo.value.prop_name == "modified" + assert excinfo.value.reason == "The new modified datetime cannot be before than or equal to the current modified datetime." \ + "It cannot be equal, as according to STIX 2 specification, objects that are different " \ + "but have the same id and modified timestamp do not have defined consumer behavior." + + +def test_versioning_error_dict_no_modified_value(): + campaign_v1 = { + 'type': 'campaign', + 'id': "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + 'created': "2016-04-06T20:03:00.000Z", + 'name': "Green Group Attacks Against Finance", + } + campaign_v2 = stix2.utils.new_version(campaign_v1, modified="2017-04-06T20:03:00.000Z") + + assert str(campaign_v2['modified']) == "2017-04-06T20:03:00.000Z" + + +def test_making_new_version_invalid_cls(): + campaign_v1 = "This is a campaign." + with pytest.raises(ValueError) as excinfo: + stix2.utils.new_version(campaign_v1, name="fred") + + assert 'cannot create new version of object of this type' in str(excinfo.value) + + +def test_revoke_dict(): + campaign_v1 = CAMPAIGN_MORE_KWARGS + campaign_v2 = stix2.utils.revoke(campaign_v1) + + assert campaign_v1['id'] == campaign_v2['id'] + assert campaign_v1['spec_version'] == campaign_v2['spec_version'] + assert campaign_v1['created_by_ref'] == campaign_v2['created_by_ref'] + assert campaign_v1['created'] == campaign_v2['created'] + assert campaign_v1['name'] == campaign_v2['name'] + assert campaign_v1['description'] == campaign_v2['description'] + assert stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified'] + + assert campaign_v2['revoked'] + + +def test_versioning_error_revoke_of_revoked_dict(): + campaign_v1 = CAMPAIGN_MORE_KWARGS + campaign_v2 = stix2.utils.revoke(campaign_v1) + + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: + stix2.utils.revoke(campaign_v2) + + assert excinfo.value.called_by == "revoke" + + +def test_revoke_invalid_cls(): + campaign_v1 = "This is a campaign." + with pytest.raises(ValueError) as excinfo: + stix2.utils.revoke(campaign_v1) + + assert 'cannot revoke object of this type' in str(excinfo.value) + + +def test_remove_custom_stix_property(): + mal = stix2.v21.Malware( + name="ColePowers", + malware_types=["rootkit"], + x_custom="armada", + allow_custom=True, + ) + + mal_nc = stix2.utils.remove_custom_stix(mal) + + assert "x_custom" not in mal_nc + assert (stix2.utils.parse_into_datetime(mal["modified"], precision="millisecond") < + stix2.utils.parse_into_datetime(mal_nc["modified"], precision="millisecond")) + + +def test_remove_custom_stix_object(): + @stix2.v21.CustomObject( + "x-animal", [ + ("species", stix2.properties.StringProperty(required=True)), + ("animal_class", stix2.properties.StringProperty()), + ], + ) + class Animal(object): + pass + + animal = Animal(species="lion", animal_class="mammal") + + nc = stix2.utils.remove_custom_stix(animal) + + assert nc is None + + +def test_remove_custom_stix_no_custom(): + campaign_v1 = stix2.v21.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v2 = stix2.utils.remove_custom_stix(campaign_v1) + + assert len(campaign_v1.keys()) == len(campaign_v2.keys()) + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.description == campaign_v2.description diff --git a/stix2/test/v21/test_vulnerability.py b/stix2/test/v21/test_vulnerability.py new file mode 100644 index 0000000..9c618e5 --- /dev/null +++ b/stix2/test/v21/test_vulnerability.py @@ -0,0 +1,74 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import VULNERABILITY_ID + +EXPECTED = """{ + "type": "vulnerability", + "spec_version": "2.1", + "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "name": "CVE-2016-1234", + "external_references": [ + { + "source_name": "cve", + "external_id": "CVE-2016-1234" + } + ] +}""" + + +def test_vulnerability_example(): + vulnerability = stix2.v21.Vulnerability( + id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + created="2016-05-12T08:17:27.000Z", + modified="2016-05-12T08:17:27.000Z", + name="CVE-2016-1234", + external_references=[ + stix2.ExternalReference( + source_name='cve', + external_id="CVE-2016-1234", + ), + ], + ) + + assert str(vulnerability) == EXPECTED + + +@pytest.mark.parametrize( + "data", [ + EXPECTED, + { + "created": "2016-05-12T08:17:27Z", + "external_references": [ + { + "external_id": "CVE-2016-1234", + "source_name": "cve", + }, + ], + "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "modified": "2016-05-12T08:17:27Z", + "name": "CVE-2016-1234", + "spec_version": "2.1", + "type": "vulnerability", + }, + ], +) +def test_parse_vulnerability(data): + vuln = stix2.parse(data, version="2.1") + + assert vuln.type == 'vulnerability' + assert vuln.spec_version == '2.1' + assert vuln.id == VULNERABILITY_ID + assert vuln.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert vuln.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert vuln.name == "CVE-2016-1234" + assert vuln.external_references[0].external_id == "CVE-2016-1234" + assert vuln.external_references[0].source_name == "cve" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_workbench.py b/stix2/test/v21/test_workbench.py new file mode 100644 index 0000000..0a976d7 --- /dev/null +++ b/stix2/test/v21/test_workbench.py @@ -0,0 +1,331 @@ +import os + +import pytest + +import stix2 +from stix2.workbench import ( + AttackPattern, Campaign, CourseOfAction, ExternalReference, + FileSystemSource, Filter, Identity, Indicator, IntrusionSet, Malware, + MarkingDefinition, ObservedData, Relationship, Report, StatementMarking, + ThreatActor, Tool, Vulnerability, add_data_source, all_versions, + attack_patterns, campaigns, courses_of_action, create, get, identities, + indicators, intrusion_sets, malware, observed_data, query, reports, save, + set_default_created, set_default_creator, set_default_external_refs, + set_default_object_marking_refs, threat_actors, tools, vulnerabilities, +) + +from .constants import ( + ATTACK_PATTERN_ID, ATTACK_PATTERN_KWARGS, CAMPAIGN_ID, CAMPAIGN_KWARGS, + COURSE_OF_ACTION_ID, COURSE_OF_ACTION_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, + INDICATOR_ID, INDICATOR_KWARGS, INTRUSION_SET_ID, INTRUSION_SET_KWARGS, + MALWARE_ID, MALWARE_KWARGS, OBSERVED_DATA_ID, OBSERVED_DATA_KWARGS, + REPORT_ID, REPORT_KWARGS, THREAT_ACTOR_ID, THREAT_ACTOR_KWARGS, TOOL_ID, + TOOL_KWARGS, VULNERABILITY_ID, VULNERABILITY_KWARGS, +) + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_environment(): + + # Create a STIX object + ind = create(Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + save(ind) + + resp = get(INDICATOR_ID) + assert resp['indicator_types'][0] == 'malicious-activity' + + resp = all_versions(INDICATOR_ID) + assert len(resp) == 1 + + # Search on something other than id + q = [Filter('type', '=', 'vulnerability')] + resp = query(q) + assert len(resp) == 0 + + +def test_workbench_get_all_attack_patterns(): + mal = AttackPattern(id=ATTACK_PATTERN_ID, **ATTACK_PATTERN_KWARGS) + save(mal) + + resp = attack_patterns() + assert len(resp) == 1 + assert resp[0].id == ATTACK_PATTERN_ID + + +def test_workbench_get_all_campaigns(): + cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + save(cam) + + resp = campaigns() + assert len(resp) == 1 + assert resp[0].id == CAMPAIGN_ID + + +def test_workbench_get_all_courses_of_action(): + coa = CourseOfAction(id=COURSE_OF_ACTION_ID, **COURSE_OF_ACTION_KWARGS) + save(coa) + + resp = courses_of_action() + assert len(resp) == 1 + assert resp[0].id == COURSE_OF_ACTION_ID + + +def test_workbench_get_all_identities(): + idty = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + save(idty) + + resp = identities() + assert len(resp) == 1 + assert resp[0].id == IDENTITY_ID + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_get_all_indicators(): + resp = indicators() + assert len(resp) == 1 + assert resp[0].id == INDICATOR_ID + + +def test_workbench_get_all_intrusion_sets(): + ins = IntrusionSet(id=INTRUSION_SET_ID, **INTRUSION_SET_KWARGS) + save(ins) + + resp = intrusion_sets() + assert len(resp) == 1 + assert resp[0].id == INTRUSION_SET_ID + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_get_all_malware(): + mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) + save(mal) + + resp = malware() + assert len(resp) == 1 + assert resp[0].id == MALWARE_ID + + +def test_workbench_get_all_observed_data(): + od = ObservedData(id=OBSERVED_DATA_ID, **OBSERVED_DATA_KWARGS) + save(od) + + resp = observed_data() + assert len(resp) == 1 + assert resp[0].id == OBSERVED_DATA_ID + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_get_all_reports(): + rep = Report(id=REPORT_ID, **REPORT_KWARGS) + save(rep) + + resp = reports() + assert len(resp) == 1 + assert resp[0].id == REPORT_ID + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_get_all_threat_actors(): + thr = ThreatActor(id=THREAT_ACTOR_ID, **THREAT_ACTOR_KWARGS) + save(thr) + + resp = threat_actors() + assert len(resp) == 1 + assert resp[0].id == THREAT_ACTOR_ID + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_get_all_tools(): + tool = Tool(id=TOOL_ID, **TOOL_KWARGS) + save(tool) + + resp = tools() + assert len(resp) == 1 + assert resp[0].id == TOOL_ID + + +def test_workbench_get_all_vulnerabilities(): + vuln = Vulnerability(id=VULNERABILITY_ID, **VULNERABILITY_KWARGS) + save(vuln) + + resp = vulnerabilities() + assert len(resp) == 1 + assert resp[0].id == VULNERABILITY_ID + + +def test_workbench_add_to_bundle(): + vuln = Vulnerability(**VULNERABILITY_KWARGS) + bundle = stix2.v21.Bundle(vuln) + assert bundle.objects[0].name == 'Heartbleed' + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_relationships(): + rel = Relationship(INDICATOR_ID, 'indicates', MALWARE_ID) + save(rel) + + ind = get(INDICATOR_ID) + resp = ind.relationships() + assert len(resp) == 1 + assert resp[0].relationship_type == 'indicates' + assert resp[0].source_ref == INDICATOR_ID + assert resp[0].target_ref == MALWARE_ID + + +def test_workbench_created_by(): + intset = IntrusionSet(name="Breach 123", created_by_ref=IDENTITY_ID) + save(intset) + creator = intset.created_by() + assert creator.id == IDENTITY_ID + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_related(): + rel1 = Relationship(MALWARE_ID, 'targets', IDENTITY_ID) + rel2 = Relationship(CAMPAIGN_ID, 'uses', MALWARE_ID) + save([rel1, rel2]) + + resp = get(MALWARE_ID).related() + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + resp = get(MALWARE_ID).related(relationship_type='indicates') + assert len(resp) == 1 + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_workbench_related_with_filters(): + malware = Malware( + malware_types=["ransomware"], name="CryptorBit", + created_by_ref=IDENTITY_ID, + ) + rel = Relationship(malware.id, 'variant-of', MALWARE_ID) + save([malware, rel]) + + filters = [Filter('created_by_ref', '=', IDENTITY_ID)] + resp = get(MALWARE_ID).related(filters=filters) + + assert len(resp) == 1 + assert resp[0].name == malware.name + assert resp[0].created_by_ref == IDENTITY_ID + + # filters arg can also be single filter + resp = get(MALWARE_ID).related(filters=filters[0]) + assert len(resp) == 1 + + +@pytest.mark.xfail(reason='The workbench is not working correctly for 2.1') +def test_add_data_source(): + fs_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") + fs = FileSystemSource(fs_path) + add_data_source(fs) + + resp = tools() + assert len(resp) == 3 + resp_ids = [tool.id for tool in resp] + assert TOOL_ID in resp_ids + assert 'tool--03342581-f790-4f03-ba41-e82e67392e23' in resp_ids + assert 'tool--242f3da3-4425-4d11-8f5c-b842886da966' in resp_ids + + +def test_additional_filter(): + resp = tools(Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5')) + assert len(resp) == 2 + + +def test_additional_filters_list(): + resp = tools([ + Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5'), + Filter('name', '=', 'Windows Credential Editor'), + ]) + assert len(resp) == 1 + + +def test_default_creator(): + set_default_creator(IDENTITY_ID) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert 'created_by_ref' not in CAMPAIGN_KWARGS + assert campaign.created_by_ref == IDENTITY_ID + + +def test_default_created_timestamp(): + timestamp = "2018-03-19T01:02:03.000Z" + set_default_created(timestamp) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert 'created' not in CAMPAIGN_KWARGS + assert stix2.utils.format_datetime(campaign.created) == timestamp + assert stix2.utils.format_datetime(campaign.modified) == timestamp + + +def test_default_external_refs(): + ext_ref = ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + ) + set_default_external_refs(ext_ref) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert campaign.external_references[0].source_name == "ACME Threat Intel" + assert campaign.external_references[0].description == "Threat report" + + +def test_default_object_marking_refs(): + stmt_marking = StatementMarking("Copyright 2016, Example Corp") + mark_def = MarkingDefinition( + definition_type="statement", + definition=stmt_marking, + ) + set_default_object_marking_refs(mark_def) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert campaign.object_marking_refs[0] == mark_def.id + + +def test_workbench_custom_property_object_in_observable_extension(): + ntfs = stix2.v21.NTFSExt( + allow_custom=True, + sid=1, + x_foo='bar', + ) + artifact = stix2.v21.File( + name='test', + extensions={'ntfs-ext': ntfs}, + ) + observed_data = ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=1, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_workbench_custom_property_dict_in_observable_extension(): + artifact = stix2.v21.File( + allow_custom=True, + name='test', + extensions={ + 'ntfs-ext': { + 'allow_custom': True, + 'sid': 1, + 'x_foo': 'bar', + }, + }, + ) + observed_data = ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=1, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) diff --git a/stix2/utils.py b/stix2/utils.py index 63b3d45..ffabed0 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,4 +1,5 @@ -"""Utility functions and classes for the stix2 library.""" +"""Utility functions and classes for the STIX2 library.""" + from collections import Mapping import copy import datetime as dt @@ -9,8 +10,9 @@ import pytz import stix2.base -from .exceptions import (InvalidValueError, RevokeError, - UnmodifiablePropertyError) +from .exceptions import ( + InvalidValueError, RevokeError, UnmodifiablePropertyError, +) # Sentinel value for properties that should be set to the current time. # We can't use the standard 'default' approach, since if there are multiple @@ -18,7 +20,7 @@ from .exceptions import (InvalidValueError, RevokeError, NOW = object() # STIX object properties that cannot be modified -STIX_UNMOD_PROPERTIES = ["created", "created_by_ref", "id", "type"] +STIX_UNMOD_PROPERTIES = ['created', 'created_by_ref', 'id', 'type'] TYPE_REGEX = r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-?$' @@ -28,8 +30,10 @@ class STIXdatetime(dt.datetime): precision = kwargs.pop('precision', None) if isinstance(args[0], dt.datetime): # Allow passing in a datetime object dttm = args[0] - args = (dttm.year, dttm.month, dttm.day, dttm.hour, dttm.minute, - dttm.second, dttm.microsecond, dttm.tzinfo) + args = ( + dttm.year, dttm.month, dttm.day, dttm.hour, dttm.minute, + dttm.second, dttm.microsecond, dttm.tzinfo, + ) # self will be an instance of STIXdatetime, not dt.datetime self = dt.datetime.__new__(cls, *args, **kwargs) self.precision = precision @@ -92,16 +96,16 @@ def format_datetime(dttm): zoned = pytz.utc.localize(dttm) else: zoned = dttm.astimezone(pytz.utc) - ts = zoned.strftime("%Y-%m-%dT%H:%M:%S") - ms = zoned.strftime("%f") - precision = getattr(dttm, "precision", None) + ts = zoned.strftime('%Y-%m-%dT%H:%M:%S') + ms = zoned.strftime('%f') + precision = getattr(dttm, 'precision', None) if precision == 'second': - pass # Alredy precise to the second - elif precision == "millisecond": + pass # Already precise to the second + elif precision == 'millisecond': ts = ts + '.' + ms[:3] elif zoned.microsecond > 0: - ts = ts + '.' + ms.rstrip("0") - return ts + "Z" + ts = ts + '.' + ms.rstrip('0') + return ts + 'Z' def parse_into_datetime(value, precision=None): @@ -119,8 +123,10 @@ def parse_into_datetime(value, precision=None): parsed = parser.parse(value) except (TypeError, ValueError): # Unknown format - raise ValueError("must be a datetime object, date object, or " - "timestamp string in a recognizable format.") + raise ValueError( + "must be a datetime object, date object, or " + "timestamp string in a recognizable format.", + ) if parsed.tzinfo: ts = parsed.astimezone(pytz.utc) else: @@ -172,9 +178,12 @@ def _find(seq, val): Search sequence 'seq' for val. This behaves like str.find(): if not found, -1 is returned instead of throwing an exception. - :param seq: The sequence to search - :param val: The value to search for - :return: The index of the value if found, or -1 if not found + Args: + seq: The sequence to search + val: The value to search for + + Returns: + int: The index of the value if found, or -1 if not found """ try: return seq.index(val) @@ -187,10 +196,13 @@ def _find_property_in_seq(seq, search_key, search_value): Helper for find_property_index(): search for the property in all elements of the given sequence. - :param seq: The sequence - :param search_key: Property name to find - :param search_value: Property value to find - :return: A property index, or -1 if the property was not found + Args: + seq: The sequence + search_key: Property name to find + search_value: Property value to find + + Returns: + int: A property index, or -1 if the property was not found """ idx = -1 for elem in seq: @@ -206,10 +218,13 @@ def find_property_index(obj, search_key, search_value): Search (recursively) for the given key and value in the given object. Return an index for the key, relative to whatever object it's found in. - :param obj: The object to search (list, dict, or stix object) - :param search_key: A search key - :param search_value: A search value - :return: An index; -1 if the key and value aren't found + Args: + obj: The object to search (list, dict, or stix object) + search_key: A search key + search_value: A search value + + Returns: + int: An index; -1 if the key and value aren't found """ from .base import _STIXBase @@ -243,11 +258,13 @@ def new_version(data, **kwargs): """ if not isinstance(data, Mapping): - raise ValueError('cannot create new version of object of this type! ' - 'Try a dictionary or instance of an SDO or SRO class.') + raise ValueError( + "cannot create new version of object of this type! " + "Try a dictionary or instance of an SDO or SRO class.", + ) unchangable_properties = [] - if data.get("revoked"): + if data.get('revoked'): raise RevokeError("new_version") try: new_obj_inner = copy.deepcopy(data._inner) @@ -269,10 +286,12 @@ def new_version(data, **kwargs): old_modified_property = parse_into_datetime(data.get('modified'), precision='millisecond') new_modified_property = parse_into_datetime(kwargs['modified'], precision='millisecond') if new_modified_property <= old_modified_property: - raise InvalidValueError(cls, 'modified', - "The new modified datetime cannot be before than or equal to the current modified datetime." - "It cannot be equal, as according to STIX 2 specification, objects that are different " - "but have the same id and modified timestamp do not have defined consumer behavior.") + raise InvalidValueError( + cls, 'modified', + "The new modified datetime cannot be before than or equal to the current modified datetime." + "It cannot be equal, as according to STIX 2 specification, objects that are different " + "but have the same id and modified timestamp do not have defined consumer behavior.", + ) new_obj_inner.update(kwargs) # Exclude properties with a value of 'None' in case data is not an instance of a _STIXBase subclass return cls(**{k: v for k, v in new_obj_inner.items() if v is not None}) @@ -285,10 +304,12 @@ def revoke(data): A new version of the object with ``revoked`` set to ``True``. """ if not isinstance(data, Mapping): - raise ValueError('cannot revoke object of this type! Try a dictionary ' - 'or instance of an SDO or SRO class.') + raise ValueError( + "cannot revoke object of this type! Try a dictionary " + "or instance of an SDO or SRO class.", + ) - if data.get("revoked"): + if data.get('revoked'): raise RevokeError("revoke") return new_version(data, revoked=True, allow_custom=True) @@ -304,14 +325,14 @@ def get_class_hierarchy_names(obj): def remove_custom_stix(stix_obj): """Remove any custom STIX objects or properties. - Warning: This function is a best effort utility, in that - it will remove custom objects and properties based on the - type names; i.e. if "x-" prefixes object types, and "x\\_" - prefixes property types. According to the STIX2 spec, - those naming conventions are a SHOULDs not MUSTs, meaning - that valid custom STIX content may ignore those conventions - and in effect render this utility function invalid when used - on that STIX content. + Warnings: + This function is a best effort utility, in that it will remove custom + objects and properties based on the type names; i.e. if "x-" prefixes + object types, and "x\\_" prefixes property types. According to the + STIX2 spec, those naming conventions are a SHOULDs not MUSTs, meaning + that valid custom STIX content may ignore those conventions and in + effect render this utility function invalid when used on that STIX + content. Args: stix_obj (dict OR python-stix obj): a single python-stix object @@ -321,13 +342,13 @@ def remove_custom_stix(stix_obj): A new version of the object with any custom content removed """ - if stix_obj["type"].startswith("x-"): + if stix_obj['type'].startswith('x-'): # if entire object is custom, discard return None custom_props = [] for prop in stix_obj.items(): - if prop[0].startswith("x_"): + if prop[0].startswith('x_'): # for every custom property, record it and set value to None # (so we can pass it to new_version() and it will be dropped) custom_props.append((prop[0], None)) @@ -344,7 +365,7 @@ def remove_custom_stix(stix_obj): # existing STIX object) and the "modified" property. We dont supply the # "modified" property so that new_version() creates a new datetime # value for this property - non_supplied_props = STIX_UNMOD_PROPERTIES + ["modified"] + non_supplied_props = STIX_UNMOD_PROPERTIES + ['modified'] props = [(prop, stix_obj[prop]) for prop in stix_obj if prop not in non_supplied_props] @@ -353,7 +374,7 @@ def remove_custom_stix(stix_obj): new_obj = new_version(stix_obj, **(dict(props))) - while parse_into_datetime(new_obj["modified"]) == parse_into_datetime(stix_obj["modified"]): + while parse_into_datetime(new_obj['modified']) == parse_into_datetime(stix_obj['modified']): # Prevents bug when fast computation allows multiple STIX object # versions to be created in single unit of time new_obj = new_version(stix_obj, **(dict(props))) diff --git a/stix2/v20/__init__.py b/stix2/v20/__init__.py index 9d7efcc..bef7d66 100644 --- a/stix2/v20/__init__.py +++ b/stix2/v20/__init__.py @@ -1,27 +1,29 @@ +"""STIX 2.0 API Objects.""" # flake8: noqa -from ..core import Bundle -from .common import (TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking, - ExternalReference, GranularMarking, KillChainPhase, - MarkingDefinition, StatementMarking, TLPMarking) -from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact, - AutonomousSystem, CustomExtension, CustomObservable, - Directory, DomainName, EmailAddress, EmailMessage, - EmailMIMEComponent, ExtensionsProperty, File, - HTTPRequestExt, ICMPExt, IPv4Address, IPv6Address, - MACAddress, Mutex, NetworkTraffic, NTFSExt, - ObservableProperty, PDFExt, Process, RasterImageExt, - SocketExt, Software, TCPExt, UNIXAccountExt, - UserAccount, WindowsPEBinaryExt, - WindowsPEOptionalHeaderType, WindowsPESection, - WindowsProcessExt, WindowsRegistryKey, - WindowsRegistryValueType, WindowsServiceExt, - X509Certificate, X509V3ExtenstionsType, - parse_observable) -from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject, - Identity, Indicator, IntrusionSet, Malware, ObservedData, - Report, ThreatActor, Tool, Vulnerability) +from .bundle import Bundle +from .common import ( + TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking, ExternalReference, + GranularMarking, KillChainPhase, MarkingDefinition, StatementMarking, + TLPMarking, +) +from .observables import ( + URL, AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, + CustomExtension, CustomObservable, Directory, DomainName, EmailAddress, + EmailMessage, EmailMIMEComponent, File, HTTPRequestExt, ICMPExt, + IPv4Address, IPv6Address, MACAddress, Mutex, NetworkTraffic, NTFSExt, + PDFExt, Process, RasterImageExt, SocketExt, Software, TCPExt, + UNIXAccountExt, UserAccount, WindowsPEBinaryExt, + WindowsPEOptionalHeaderType, WindowsPESection, WindowsProcessExt, + WindowsRegistryKey, WindowsRegistryValueType, WindowsServiceExt, + X509Certificate, X509V3ExtenstionsType, +) +from .sdo import ( + AttackPattern, Campaign, CourseOfAction, CustomObject, Identity, Indicator, + IntrusionSet, Malware, ObservedData, Report, ThreatActor, Tool, + Vulnerability, +) from .sro import Relationship, Sighting OBJ_MAP = { @@ -42,3 +44,47 @@ OBJ_MAP = { 'sighting': Sighting, 'vulnerability': Vulnerability, } + +OBJ_MAP_OBSERVABLE = { + 'artifact': Artifact, + 'autonomous-system': AutonomousSystem, + 'directory': Directory, + 'domain-name': DomainName, + 'email-addr': EmailAddress, + 'email-message': EmailMessage, + 'file': File, + 'ipv4-addr': IPv4Address, + 'ipv6-addr': IPv6Address, + 'mac-addr': MACAddress, + 'mutex': Mutex, + 'network-traffic': NetworkTraffic, + 'process': Process, + 'software': Software, + 'url': URL, + 'user-account': UserAccount, + 'windows-registry-key': WindowsRegistryKey, + 'x509-certificate': X509Certificate, +} + +EXT_MAP = { + 'file': { + 'archive-ext': ArchiveExt, + 'ntfs-ext': NTFSExt, + 'pdf-ext': PDFExt, + 'raster-image-ext': RasterImageExt, + 'windows-pebinary-ext': WindowsPEBinaryExt, + }, + 'network-traffic': { + 'http-request-ext': HTTPRequestExt, + 'icmp-ext': ICMPExt, + 'socket-ext': SocketExt, + 'tcp-ext': TCPExt, + }, + 'process': { + 'windows-process-ext': WindowsProcessExt, + 'windows-service-ext': WindowsServiceExt, + }, + 'user-account': { + 'unix-account-ext': UNIXAccountExt, + }, +} diff --git a/stix2/v20/bundle.py b/stix2/v20/bundle.py new file mode 100644 index 0000000..76386ef --- /dev/null +++ b/stix2/v20/bundle.py @@ -0,0 +1,37 @@ +"""STIX 2.0 Bundle Representation.""" + +from collections import OrderedDict + +from ..base import _STIXBase +from ..properties import ( + IDProperty, ListProperty, STIXObjectProperty, StringProperty, TypeProperty, +) + + +class Bundle(_STIXBase): + """For more detailed information on this object's properties, see + `the STIX 2.0 specification `__. + """ + + _type = 'bundle' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('id', IDProperty(_type)), + # Not technically correct: STIX 2.0 spec doesn't say spec_version must + # have this value, but it's all we support for now. + ('spec_version', StringProperty(fixed='2.0')), + ('objects', ListProperty(STIXObjectProperty)), + ]) + + def __init__(self, *args, **kwargs): + # Add any positional arguments to the 'objects' kwarg. + if args: + if isinstance(args[0], list): + kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', []) + else: + kwargs['objects'] = list(args) + kwargs.get('objects', []) + + self.__allow_custom = kwargs.get('allow_custom', False) + self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False) + + super(Bundle, self).__init__(**kwargs) diff --git a/stix2/v20/common.py b/stix2/v20/common.py index d574c92..afd7812 100644 --- a/stix2/v20/common.py +++ b/stix2/v20/common.py @@ -1,13 +1,15 @@ -"""STIX 2 Common Data Types and Properties.""" +"""STIX 2.0 Common Data Types and Properties.""" from collections import OrderedDict import copy -from ..base import _cls_init, _STIXBase +from ..base import _STIXBase +from ..custom import _custom_marking_builder from ..markings import _MarkingsMixin -from ..properties import (HashesProperty, IDProperty, ListProperty, Property, - ReferenceProperty, SelectorProperty, StringProperty, - TimestampProperty, TypeProperty) +from ..properties import ( + HashesProperty, IDProperty, ListProperty, Property, ReferenceProperty, + SelectorProperty, StringProperty, TimestampProperty, TypeProperty, +) from ..utils import NOW, _get_dict @@ -16,8 +18,7 @@ class ExternalReference(_STIXBase): `the STIX 2.0 specification `__. """ - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('source_name', StringProperty(required=True)), ('description', StringProperty()), ('url', StringProperty()), @@ -27,7 +28,7 @@ class ExternalReference(_STIXBase): def _check_object_constraints(self): super(ExternalReference, self)._check_object_constraints() - self._check_at_least_one_property(["description", "external_id", "url"]) + self._check_at_least_one_property(['description', 'external_id', 'url']) class KillChainPhase(_STIXBase): @@ -35,8 +36,7 @@ class KillChainPhase(_STIXBase): `the STIX 2.0 specification `__. """ - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('kill_chain_name', StringProperty(required=True)), ('phase_name', StringProperty(required=True)), ]) @@ -47,9 +47,8 @@ class GranularMarking(_STIXBase): `the STIX 2.0 specification `__. """ - _properties = OrderedDict() - _properties.update([ - ('marking_ref', ReferenceProperty(required=True, type="marking-definition")), + _properties = OrderedDict([ + ('marking_ref', ReferenceProperty(required=True, type='marking-definition')), ('selectors', ListProperty(SelectorProperty, required=True)), ]) @@ -61,9 +60,8 @@ class TLPMarking(_STIXBase): # TODO: don't allow the creation of any other TLPMarkings than the ones below _type = 'tlp' - _properties = OrderedDict() - _properties.update([ - ('tlp', StringProperty(required=True)) + _properties = OrderedDict([ + ('tlp', StringProperty(required=True)), ]) @@ -73,9 +71,8 @@ class StatementMarking(_STIXBase): """ _type = 'statement' - _properties = OrderedDict() - _properties.update([ - ('statement', StringProperty(required=True)) + _properties = OrderedDict([ + ('statement', StringProperty(required=True)), ]) def __init__(self, statement=None, **kwargs): @@ -104,14 +101,13 @@ class MarkingDefinition(_STIXBase, _MarkingsMixin): """ _type = 'marking-definition' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ('definition_type', StringProperty(required=True)), ('definition', MarkingProperty(required=True)), @@ -145,17 +141,12 @@ OBJ_MAP_MARKING = { } -def _register_marking(cls): - """Register a custom STIX Marking Definition type. - """ - OBJ_MAP_MARKING[cls._type] = cls - return cls - - def CustomMarking(type='x-custom-marking', properties=None): """Custom STIX Marking decorator. Example: + >>> from stix2 import CustomMarking + >>> from stix2.properties import IntegerProperty, StringProperty >>> @CustomMarking('x-custom-marking', [ ... ('property1', StringProperty(required=True)), ... ('property2', IntegerProperty()), @@ -164,51 +155,37 @@ def CustomMarking(type='x-custom-marking', properties=None): ... pass """ - def custom_builder(cls): + def wrapper(cls): + return _custom_marking_builder(cls, type, properties, '2.0') + return wrapper - class _Custom(cls, _STIXBase): - _type = type - _properties = OrderedDict() - - if not properties or not isinstance(properties, list): - raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - - _properties.update(properties) - - def __init__(self, **kwargs): - _STIXBase.__init__(self, **kwargs) - _cls_init(cls, self, kwargs) - - _register_marking(_Custom) - return _Custom - - return custom_builder +# TODO: don't allow the creation of any other TLPMarkings than the ones below 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") + 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") + 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") + 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") + 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/v20/observables.py b/stix2/v20/observables.py index faa5868..0e7c4a0 100644 --- a/stix2/v20/observables.py +++ b/stix2/v20/observables.py @@ -6,91 +6,17 @@ Observable and do not have a ``_type`` attribute. """ from collections import OrderedDict -import copy -import re +import itertools -from ..base import _cls_init, _Extension, _Observable, _STIXBase -from ..exceptions import (AtLeastOnePropertyError, CustomContentError, - DependentPropertiesError, ParseError) -from ..properties import (BinaryProperty, BooleanProperty, DictionaryProperty, - EmbeddedObjectProperty, EnumProperty, FloatProperty, - HashesProperty, HexProperty, IntegerProperty, - ListProperty, ObjectReferenceProperty, Property, - StringProperty, TimestampProperty, TypeProperty) -from ..utils import TYPE_REGEX, _get_dict - - -class ObservableProperty(Property): - """Property for holding Cyber Observable Objects. - """ - - def __init__(self, allow_custom=False, *args, **kwargs): - self.allow_custom = allow_custom - super(ObservableProperty, self).__init__(*args, **kwargs) - - def clean(self, value): - try: - dictified = _get_dict(value) - # get deep copy since we are going modify the dict and might - # modify the original dict as _get_dict() does not return new - # dict when passed a dict - dictified = copy.deepcopy(dictified) - except ValueError: - raise ValueError("The observable property must contain a dictionary") - if dictified == {}: - raise ValueError("The observable property must contain a non-empty dictionary") - - valid_refs = dict((k, v['type']) for (k, v) in dictified.items()) - - for key, obj in dictified.items(): - if self.allow_custom: - parsed_obj = parse_observable(obj, valid_refs, allow_custom=True) - else: - parsed_obj = parse_observable(obj, valid_refs) - dictified[key] = parsed_obj - - return dictified - - -class ExtensionsProperty(DictionaryProperty): - """Property for representing extensions on Observable objects. - """ - - def __init__(self, allow_custom=False, enclosing_type=None, required=False): - self.allow_custom = allow_custom - self.enclosing_type = enclosing_type - super(ExtensionsProperty, self).__init__(required) - - def clean(self, value): - try: - dictified = _get_dict(value) - # get deep copy since we are going modify the dict and might - # modify the original dict as _get_dict() does not return new - # dict when passed a dict - dictified = copy.deepcopy(dictified) - except ValueError: - raise ValueError("The extensions property must contain a dictionary") - if dictified == {}: - raise ValueError("The extensions property must contain a non-empty dictionary") - - specific_type_map = EXT_MAP.get(self.enclosing_type, {}) - for key, subvalue in dictified.items(): - if key in specific_type_map: - cls = specific_type_map[key] - if type(subvalue) is dict: - if self.allow_custom: - subvalue['allow_custom'] = True - dictified[key] = cls(**subvalue) - else: - dictified[key] = cls(**subvalue) - elif type(subvalue) is cls: - # If already an instance of an _Extension class, assume it's valid - dictified[key] = subvalue - else: - raise ValueError("Cannot determine extension type.") - else: - raise CustomContentError("Can't parse unknown extension type: {}".format(key)) - return dictified +from ..base import _Extension, _Observable, _STIXBase +from ..custom import _custom_extension_builder, _custom_observable_builder +from ..exceptions import AtLeastOnePropertyError, DependentPropertiesError +from ..properties import ( + BinaryProperty, BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, + HashesProperty, HexProperty, IntegerProperty, ListProperty, + ObjectReferenceProperty, StringProperty, TimestampProperty, TypeProperty, +) class Artifact(_Observable): @@ -99,8 +25,7 @@ class Artifact(_Observable): """ # noqa _type = 'artifact' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('mime_type', StringProperty()), ('payload_bin', BinaryProperty()), @@ -111,8 +36,8 @@ class Artifact(_Observable): def _check_object_constraints(self): super(Artifact, self)._check_object_constraints() - self._check_mutually_exclusive_properties(["payload_bin", "url"]) - self._check_properties_dependency(["hashes"], ["url"]) + self._check_mutually_exclusive_properties(['payload_bin', 'url']) + self._check_properties_dependency(['hashes'], ['url']) class AutonomousSystem(_Observable): @@ -121,8 +46,7 @@ class AutonomousSystem(_Observable): """ # noqa _type = 'autonomous-system' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('number', IntegerProperty(required=True)), ('name', StringProperty()), @@ -137,8 +61,7 @@ class Directory(_Observable): """ # noqa _type = 'directory' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('path', StringProperty(required=True)), ('path_enc', StringProperty()), @@ -157,8 +80,7 @@ class DomainName(_Observable): """ # noqa _type = 'domain-name' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'domain-name']))), @@ -172,8 +94,7 @@ class EmailAddress(_Observable): """ # noqa _type = 'email-addr' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('value', StringProperty(required=True)), ('display_name', StringProperty()), @@ -187,8 +108,7 @@ class EmailMIMEComponent(_STIXBase): `the STIX 2.0 specification `__. """ # noqa - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('body', StringProperty()), ('body_raw_ref', ObjectReferenceProperty(valid_types=['artifact', 'file'])), ('content_type', StringProperty()), @@ -197,7 +117,7 @@ class EmailMIMEComponent(_STIXBase): def _check_object_constraints(self): super(EmailMIMEComponent, self)._check_object_constraints() - self._check_at_least_one_property(["body", "body_raw_ref"]) + self._check_at_least_one_property(['body', 'body_raw_ref']) class EmailMessage(_Observable): @@ -206,8 +126,7 @@ class EmailMessage(_Observable): """ # noqa _type = 'email-message' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('is_multipart', BooleanProperty(required=True)), ('date', TimestampProperty()), @@ -228,10 +147,10 @@ class EmailMessage(_Observable): def _check_object_constraints(self): super(EmailMessage, self)._check_object_constraints() - self._check_properties_dependency(["is_multipart"], ["body_multipart"]) - if self.get("is_multipart") is True and self.get("body"): + self._check_properties_dependency(['is_multipart'], ['body_multipart']) + if self.get('is_multipart') is True and self.get('body'): # 'body' MAY only be used if is_multipart is false. - raise DependentPropertiesError(self.__class__, [("is_multipart", "body")]) + raise DependentPropertiesError(self.__class__, [('is_multipart', 'body')]) class ArchiveExt(_Extension): @@ -240,8 +159,7 @@ class ArchiveExt(_Extension): """ # noqa _type = 'archive-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('contains_refs', ListProperty(ObjectReferenceProperty(valid_types='file'), required=True)), ('version', StringProperty()), ('comment', StringProperty()), @@ -253,8 +171,7 @@ class AlternateDataStream(_STIXBase): `the STIX 2.0 specification `__. """ # noqa - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('name', StringProperty(required=True)), ('hashes', HashesProperty()), ('size', IntegerProperty()), @@ -267,8 +184,7 @@ class NTFSExt(_Extension): """ # noqa _type = 'ntfs-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('sid', StringProperty()), ('alternate_data_streams', ListProperty(EmbeddedObjectProperty(type=AlternateDataStream))), ]) @@ -280,8 +196,7 @@ class PDFExt(_Extension): """ # noqa _type = 'pdf-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('version', StringProperty()), ('is_optimized', BooleanProperty()), ('document_info_dict', DictionaryProperty()), @@ -296,8 +211,7 @@ class RasterImageExt(_Extension): """ # noqa _type = 'raster-image-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('image_height', IntegerProperty()), ('image_width', IntegerProperty()), ('bits_per_pixel', IntegerProperty()), @@ -311,8 +225,7 @@ class WindowsPEOptionalHeaderType(_STIXBase): `the STIX 2.0 specification `__. """ # noqa - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('magic_hex', HexProperty()), ('major_linker_version', IntegerProperty()), ('minor_linker_version', IntegerProperty()), @@ -356,8 +269,7 @@ class WindowsPESection(_STIXBase): `the STIX 2.0 specification `__. """ # noqa - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('name', StringProperty(required=True)), ('size', IntegerProperty()), ('entropy', FloatProperty()), @@ -371,8 +283,7 @@ class WindowsPEBinaryExt(_Extension): """ # noqa _type = 'windows-pebinary-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('pe_type', StringProperty(required=True)), # open_vocab ('imphash', StringProperty()), ('machine_hex', HexProperty()), @@ -394,8 +305,7 @@ class File(_Observable): """ # noqa _type = 'file' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('hashes', HashesProperty()), ('size', IntegerProperty()), @@ -418,8 +328,8 @@ class File(_Observable): def _check_object_constraints(self): super(File, self)._check_object_constraints() - self._check_properties_dependency(["is_encrypted"], ["encryption_algorithm", "decryption_key"]) - self._check_at_least_one_property(["hashes", "name"]) + self._check_properties_dependency(['is_encrypted'], ['encryption_algorithm', 'decryption_key']) + self._check_at_least_one_property(['hashes', 'name']) class IPv4Address(_Observable): @@ -428,8 +338,7 @@ class IPv4Address(_Observable): """ # noqa _type = 'ipv4-addr' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))), @@ -444,8 +353,7 @@ class IPv6Address(_Observable): """ # noqa _type = 'ipv6-addr' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))), @@ -460,8 +368,7 @@ class MACAddress(_Observable): """ # noqa _type = 'mac-addr' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('value', StringProperty(required=True)), ('extensions', ExtensionsProperty(enclosing_type=_type)), @@ -474,8 +381,7 @@ class Mutex(_Observable): """ # noqa _type = 'mutex' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('name', StringProperty(required=True)), ('extensions', ExtensionsProperty(enclosing_type=_type)), @@ -488,8 +394,7 @@ class HTTPRequestExt(_Extension): """ # noqa _type = 'http-request-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('request_method', StringProperty(required=True)), ('request_value', StringProperty(required=True)), ('request_version', StringProperty()), @@ -505,8 +410,7 @@ class ICMPExt(_Extension): """ # noqa _type = 'icmp-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('icmp_type_hex', HexProperty(required=True)), ('icmp_code_hex', HexProperty(required=True)), ]) @@ -518,36 +422,43 @@ class SocketExt(_Extension): """ # noqa _type = 'socket-ext' - _properties = OrderedDict() - _properties.update([ - ('address_family', EnumProperty(allowed=[ - "AF_UNSPEC", - "AF_INET", - "AF_IPX", - "AF_APPLETALK", - "AF_NETBIOS", - "AF_INET6", - "AF_IRDA", - "AF_BTH", - ], required=True)), + _properties = OrderedDict([ + ( + 'address_family', EnumProperty( + allowed=[ + "AF_UNSPEC", + "AF_INET", + "AF_IPX", + "AF_APPLETALK", + "AF_NETBIOS", + "AF_INET6", + "AF_IRDA", + "AF_BTH", + ], required=True, + ), + ), ('is_blocking', BooleanProperty()), ('is_listening', BooleanProperty()), - ('protocol_family', EnumProperty(allowed=[ - "PF_INET", - "PF_IPX", - "PF_APPLETALK", - "PF_INET6", - "PF_AX25", - "PF_NETROM" - ])), + ( + 'protocol_family', EnumProperty(allowed=[ + "PF_INET", + "PF_IPX", + "PF_APPLETALK", + "PF_INET6", + "PF_AX25", + "PF_NETROM", + ]), + ), ('options', DictionaryProperty()), - ('socket_type', EnumProperty(allowed=[ - "SOCK_STREAM", - "SOCK_DGRAM", - "SOCK_RAW", - "SOCK_RDM", - "SOCK_SEQPACKET", - ])), + ( + 'socket_type', EnumProperty(allowed=[ + "SOCK_STREAM", + "SOCK_DGRAM", + "SOCK_RAW", + "SOCK_RDM", + "SOCK_SEQPACKET", + ]), + ), ('socket_descriptor', IntegerProperty()), ('socket_handle', IntegerProperty()), ]) @@ -559,8 +470,7 @@ class TCPExt(_Extension): """ # noqa _type = 'tcp-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('src_flags_hex', HexProperty()), ('dst_flags_hex', HexProperty()), ]) @@ -572,8 +482,7 @@ class NetworkTraffic(_Observable): """ # noqa _type = 'network-traffic' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('start', TimestampProperty()), ('end', TimestampProperty()), @@ -597,7 +506,7 @@ class NetworkTraffic(_Observable): def _check_object_constraints(self): super(NetworkTraffic, self)._check_object_constraints() - self._check_at_least_one_property(["src_ref", "dst_ref"]) + self._check_at_least_one_property(['src_ref', 'dst_ref']) class WindowsProcessExt(_Extension): @@ -606,8 +515,7 @@ class WindowsProcessExt(_Extension): """ # noqa _type = 'windows-process-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('aslr_enabled', BooleanProperty()), ('dep_enabled', BooleanProperty()), ('priority', StringProperty()), @@ -623,35 +531,40 @@ class WindowsServiceExt(_Extension): """ # noqa _type = 'windows-service-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('service_name', StringProperty(required=True)), ('descriptions', ListProperty(StringProperty)), ('display_name', StringProperty()), ('group_name', StringProperty()), - ('start_type', EnumProperty(allowed=[ - "SERVICE_AUTO_START", - "SERVICE_BOOT_START", - "SERVICE_DEMAND_START", - "SERVICE_DISABLED", - "SERVICE_SYSTEM_ALERT", - ])), + ( + 'start_type', EnumProperty(allowed=[ + "SERVICE_AUTO_START", + "SERVICE_BOOT_START", + "SERVICE_DEMAND_START", + "SERVICE_DISABLED", + "SERVICE_SYSTEM_ALERT", + ]), + ), ('service_dll_refs', ListProperty(ObjectReferenceProperty(valid_types='file'))), - ('service_type', EnumProperty(allowed=[ - "SERVICE_KERNEL_DRIVER", - "SERVICE_FILE_SYSTEM_DRIVER", - "SERVICE_WIN32_OWN_PROCESS", - "SERVICE_WIN32_SHARE_PROCESS", - ])), - ('service_status', EnumProperty(allowed=[ - "SERVICE_CONTINUE_PENDING", - "SERVICE_PAUSE_PENDING", - "SERVICE_PAUSED", - "SERVICE_RUNNING", - "SERVICE_START_PENDING", - "SERVICE_STOP_PENDING", - "SERVICE_STOPPED", - ])), + ( + 'service_type', EnumProperty(allowed=[ + "SERVICE_KERNEL_DRIVER", + "SERVICE_FILE_SYSTEM_DRIVER", + "SERVICE_WIN32_OWN_PROCESS", + "SERVICE_WIN32_SHARE_PROCESS", + ]), + ), + ( + 'service_status', EnumProperty(allowed=[ + "SERVICE_CONTINUE_PENDING", + "SERVICE_PAUSE_PENDING", + "SERVICE_PAUSED", + "SERVICE_RUNNING", + "SERVICE_START_PENDING", + "SERVICE_STOP_PENDING", + "SERVICE_STOPPED", + ]), + ), ]) @@ -661,8 +574,7 @@ class Process(_Observable): """ # noqa _type = 'process' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('is_hidden', BooleanProperty()), ('pid', IntegerProperty()), @@ -686,14 +598,14 @@ class Process(_Observable): super(Process, self)._check_object_constraints() try: self._check_at_least_one_property() - if "windows-process-ext" in self.get('extensions', {}): - self.extensions["windows-process-ext"]._check_at_least_one_property() + if 'windows-process-ext' in self.get('extensions', {}): + self.extensions['windows-process-ext']._check_at_least_one_property() except AtLeastOnePropertyError as enclosing_exc: if 'extensions' not in self: raise enclosing_exc else: - if "windows-process-ext" in self.get('extensions', {}): - self.extensions["windows-process-ext"]._check_at_least_one_property() + if 'windows-process-ext' in self.get('extensions', {}): + self.extensions['windows-process-ext']._check_at_least_one_property() class Software(_Observable): @@ -702,8 +614,7 @@ class Software(_Observable): """ # noqa _type = 'software' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('name', StringProperty(required=True)), ('cpe', StringProperty()), @@ -720,8 +631,7 @@ class URL(_Observable): """ # noqa _type = 'url' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('value', StringProperty(required=True)), ('extensions', ExtensionsProperty(enclosing_type=_type)), @@ -734,8 +644,7 @@ class UNIXAccountExt(_Extension): """ # noqa _type = 'unix-account-ext' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('gid', IntegerProperty()), ('groups', ListProperty(StringProperty)), ('home_dir', StringProperty()), @@ -749,8 +658,7 @@ class UserAccount(_Observable): """ # noqa _type = 'user-account' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('user_id', StringProperty(required=True)), ('account_login', StringProperty()), @@ -775,25 +683,26 @@ class WindowsRegistryValueType(_STIXBase): """ # noqa _type = 'windows-registry-value-type' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('name', StringProperty(required=True)), ('data', StringProperty()), - ('data_type', EnumProperty(allowed=[ - 'REG_NONE', - 'REG_SZ', - 'REG_EXPAND_SZ', - 'REG_BINARY', - 'REG_DWORD', - 'REG_DWORD_BIG_ENDIAN', - 'REG_LINK', - 'REG_MULTI_SZ', - 'REG_RESOURCE_LIST', - 'REG_FULL_RESOURCE_DESCRIPTION', - 'REG_RESOURCE_REQUIREMENTS_LIST', - 'REG_QWORD', - 'REG_INVALID_TYPE', - ])), + ( + 'data_type', EnumProperty(allowed=[ + "REG_NONE", + "REG_SZ", + "REG_EXPAND_SZ", + "REG_BINARY", + "REG_DWORD", + "REG_DWORD_BIG_ENDIAN", + "REG_LINK", + "REG_MULTI_SZ", + "REG_RESOURCE_LIST", + "REG_FULL_RESOURCE_DESCRIPTION", + "REG_RESOURCE_REQUIREMENTS_LIST", + "REG_QWORD", + "REG_INVALID_TYPE", + ]), + ), ]) @@ -803,8 +712,7 @@ class WindowsRegistryKey(_Observable): """ # noqa _type = 'windows-registry-key' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('key', StringProperty(required=True)), ('values', ListProperty(EmbeddedObjectProperty(type=WindowsRegistryValueType))), @@ -827,8 +735,7 @@ class X509V3ExtenstionsType(_STIXBase): """ # noqa _type = 'x509-v3-extensions-type' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('basic_constraints', StringProperty()), ('name_constraints', StringProperty()), ('policy_constraints', StringProperty()), @@ -854,8 +761,7 @@ class X509Certificate(_Observable): """ # noqa _type = 'x509-certificate' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('is_self_signed', BooleanProperty()), ('hashes', HashesProperty()), @@ -874,215 +780,33 @@ class X509Certificate(_Observable): ]) -OBJ_MAP_OBSERVABLE = { - 'artifact': Artifact, - 'autonomous-system': AutonomousSystem, - 'directory': Directory, - 'domain-name': DomainName, - 'email-addr': EmailAddress, - 'email-message': EmailMessage, - 'file': File, - 'ipv4-addr': IPv4Address, - 'ipv6-addr': IPv6Address, - 'mac-addr': MACAddress, - 'mutex': Mutex, - 'network-traffic': NetworkTraffic, - 'process': Process, - 'software': Software, - 'url': URL, - 'user-account': UserAccount, - 'windows-registry-key': WindowsRegistryKey, - 'x509-certificate': X509Certificate, -} - - -EXT_MAP = { - 'file': { - 'archive-ext': ArchiveExt, - 'ntfs-ext': NTFSExt, - 'pdf-ext': PDFExt, - 'raster-image-ext': RasterImageExt, - 'windows-pebinary-ext': WindowsPEBinaryExt - }, - 'network-traffic': { - 'http-request-ext': HTTPRequestExt, - 'icmp-ext': ICMPExt, - 'socket-ext': SocketExt, - 'tcp-ext': TCPExt, - }, - 'process': { - 'windows-process-ext': WindowsProcessExt, - 'windows-service-ext': WindowsServiceExt, - }, - 'user-account': { - 'unix-account-ext': UNIXAccountExt, - }, -} - - -def parse_observable(data, _valid_refs=None, allow_custom=False): - """Deserialize a string or file-like object into a STIX Cyber Observable - object. - - Args: - data: The STIX 2 string to be parsed. - _valid_refs: A list of object references valid for the scope of the - object being parsed. Use empty list if no valid refs are present. - allow_custom: Whether to allow custom properties or not. - Default: False. - - Returns: - An instantiated Python STIX Cyber Observable object. - """ - - obj = _get_dict(data) - # get deep copy since we are going modify the dict and might - # modify the original dict as _get_dict() does not return new - # dict when passed a dict - obj = copy.deepcopy(obj) - - obj['_valid_refs'] = _valid_refs or [] - - if 'type' not in obj: - raise ParseError("Can't parse observable with no 'type' property: %s" % str(obj)) - try: - obj_class = OBJ_MAP_OBSERVABLE[obj['type']] - except KeyError: - if allow_custom: - # flag allows for unknown custom objects too, but will not - # be parsed into STIX observable object, just returned as is - return obj - raise CustomContentError("Can't parse unknown observable type '%s'! For custom observables, " - "use the CustomObservable decorator." % obj['type']) - - if 'extensions' in obj and obj['type'] in EXT_MAP: - for name, ext in obj['extensions'].items(): - try: - ext_class = EXT_MAP[obj['type']][name] - except KeyError: - if not allow_custom: - raise CustomContentError("Can't parse unknown extension type '%s'" - "for observable type '%s'!" % (name, obj['type'])) - else: # extension was found - obj['extensions'][name] = ext_class(allow_custom=allow_custom, **obj['extensions'][name]) - - return obj_class(allow_custom=allow_custom, **obj) - - -def _register_observable(new_observable): - """Register a custom STIX Cyber Observable type. - """ - - OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable - - def CustomObservable(type='x-custom-observable', properties=None): """Custom STIX Cyber Observable Object type decorator. Example: + >>> from stix2.v20 import CustomObservable + >>> from stix2.properties import IntegerProperty, StringProperty >>> @CustomObservable('x-custom-observable', [ ... ('property1', StringProperty(required=True)), ... ('property2', IntegerProperty()), ... ]) ... class MyNewObservableType(): ... pass + """ - - def custom_builder(cls): - - class _Custom(cls, _Observable): - - if not re.match(TYPE_REGEX, type): - raise ValueError("Invalid observable type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type) - elif len(type) < 3 or len(type) > 250: - raise ValueError("Invalid observable type name '%s': must be between 3 and 250 characters." % type) - - _type = type - _properties = OrderedDict() - _properties.update([ - ('type', TypeProperty(_type)), - ]) - - if not properties or not isinstance(properties, list): - raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - - # Check properties ending in "_ref/s" are ObjectReferenceProperties - for prop_name, prop in properties: - if prop_name.endswith('_ref') and not isinstance(prop, ObjectReferenceProperty): - raise ValueError("'%s' is named like an object reference property but " - "is not an ObjectReferenceProperty." % prop_name) - elif (prop_name.endswith('_refs') and (not isinstance(prop, ListProperty) - or not isinstance(prop.contained, ObjectReferenceProperty))): - raise ValueError("'%s' is named like an object reference list property but " - "is not a ListProperty containing ObjectReferenceProperty." % prop_name) - - _properties.update(properties) - _properties.update([ - ('extensions', ExtensionsProperty(enclosing_type=_type)), - ]) - - def __init__(self, **kwargs): - _Observable.__init__(self, **kwargs) - _cls_init(cls, self, kwargs) - - _register_observable(_Custom) - return _Custom - - return custom_builder + def wrapper(cls): + _properties = list(itertools.chain.from_iterable([ + [('type', TypeProperty(type))], + properties, + [('extensions', ExtensionsProperty(enclosing_type=type))], + ])) + return _custom_observable_builder(cls, type, _properties, '2.0') + return wrapper -def _register_extension(observable, new_extension): - """Register a custom extension to a STIX Cyber Observable type. - """ - - try: - observable_type = observable._type - except AttributeError: - raise ValueError("Unknown observable type. Custom observables must be " - "created with the @CustomObservable decorator.") - - try: - EXT_MAP[observable_type][new_extension._type] = new_extension - except KeyError: - if observable_type not in OBJ_MAP_OBSERVABLE: - raise ValueError("Unknown observable type '%s'. Custom observables " - "must be created with the @CustomObservable decorator." - % observable_type) - else: - EXT_MAP[observable_type] = {new_extension._type: new_extension} - - -def CustomExtension(observable=None, type='x-custom-observable', properties=None): +def CustomExtension(observable=None, type='x-custom-observable-ext', properties=None): """Decorator for custom extensions to STIX Cyber Observables. """ - - if not observable or not issubclass(observable, _Observable): - raise ValueError("'observable' must be a valid Observable class!") - - def custom_builder(cls): - - class _Custom(cls, _Extension): - - if not re.match(TYPE_REGEX, type): - raise ValueError("Invalid extension type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type) - elif len(type) < 3 or len(type) > 250: - raise ValueError("Invalid extension type name '%s': must be between 3 and 250 characters." % type) - - _type = type - _properties = OrderedDict() - - if not properties or not isinstance(properties, list): - raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - - _properties.update(properties) - - def __init__(self, **kwargs): - _Extension.__init__(self, **kwargs) - _cls_init(cls, self, kwargs) - - _register_extension(observable, _Custom) - return _Custom - - return custom_builder + def wrapper(cls): + return _custom_extension_builder(cls, observable, type, properties, '2.0') + return wrapper diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py index 1a688d1..cd99e69 100644 --- a/stix2/v20/sdo.py +++ b/stix2/v20/sdo.py @@ -1,23 +1,17 @@ -"""STIX 2.0 Domain Objects. -""" +"""STIX 2.0 Domain Objects.""" from collections import OrderedDict -import re +import itertools -import stix2 - -from ..base import _cls_init, _STIXBase -from ..markings import _MarkingsMixin -from ..properties import (BooleanProperty, IDProperty, IntegerProperty, - ListProperty, PatternProperty, ReferenceProperty, - StringProperty, TimestampProperty, TypeProperty) -from ..utils import NOW, TYPE_REGEX +from ..core import STIXDomainObject +from ..custom import _custom_object_builder +from ..properties import ( + BooleanProperty, IDProperty, IntegerProperty, ListProperty, + ObservableProperty, PatternProperty, ReferenceProperty, StringProperty, + TimestampProperty, TypeProperty, +) +from ..utils import NOW from .common import ExternalReference, GranularMarking, KillChainPhase -from .observables import ObservableProperty - - -class STIXDomainObject(_STIXBase, _MarkingsMixin): - pass class AttackPattern(STIXDomainObject): @@ -26,11 +20,10 @@ class AttackPattern(STIXDomainObject): """ _type = 'attack-pattern' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -39,7 +32,7 @@ class AttackPattern(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -50,11 +43,10 @@ class Campaign(STIXDomainObject): """ _type = 'campaign' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -66,7 +58,7 @@ class Campaign(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -77,11 +69,10 @@ class CourseOfAction(STIXDomainObject): """ _type = 'course-of-action' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -89,7 +80,7 @@ class CourseOfAction(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -100,11 +91,10 @@ class Identity(STIXDomainObject): """ _type = 'identity' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -115,7 +105,7 @@ class Identity(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -126,11 +116,10 @@ class Indicator(STIXDomainObject): """ _type = 'indicator' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty()), @@ -142,7 +131,7 @@ class Indicator(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty, required=True)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -153,11 +142,10 @@ class IntrusionSet(STIXDomainObject): """ _type = 'intrusion-set' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -172,7 +160,7 @@ class IntrusionSet(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -183,11 +171,10 @@ class Malware(STIXDomainObject): """ _type = 'malware' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -196,7 +183,7 @@ class Malware(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty, required=True)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -207,21 +194,20 @@ class ObservedData(STIXDomainObject): """ _type = 'observed-data' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('first_observed', TimestampProperty(required=True)), ('last_observed', TimestampProperty(required=True)), - ('number_observed', IntegerProperty(required=True)), + ('number_observed', IntegerProperty(min=1, max=999999999, required=True)), ('objects', ObservableProperty(required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -238,11 +224,10 @@ class Report(STIXDomainObject): """ _type = 'report' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -252,7 +237,7 @@ class Report(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty, required=True)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -263,11 +248,10 @@ class ThreatActor(STIXDomainObject): """ _type = 'threat-actor' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -283,7 +267,7 @@ class ThreatActor(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty, required=True)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -294,11 +278,10 @@ class Tool(STIXDomainObject): """ _type = 'tool' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -308,7 +291,7 @@ class Tool(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty, required=True)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -319,11 +302,10 @@ class Vulnerability(STIXDomainObject): """ _type = 'vulnerability' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('name', StringProperty(required=True)), @@ -331,7 +313,7 @@ class Vulnerability(STIXDomainObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) @@ -340,6 +322,8 @@ def CustomObject(type='x-custom-type', properties=None): """Custom STIX Object type decorator. Example: + >>> from stix2.v20 import CustomObject + >>> from stix2.properties import IntegerProperty, StringProperty >>> @CustomObject('x-type-name', [ ... ('property1', StringProperty(required=True)), ... ('property2', IntegerProperty()), @@ -351,6 +335,8 @@ def CustomObject(type='x-custom-type', properties=None): type. Don't call ``super().__init__()`` though - doing so will cause an error. Example: + >>> from stix2.v20 import CustomObject + >>> from stix2.properties import IntegerProperty, StringProperty >>> @CustomObject('x-type-name', [ ... ('property1', StringProperty(required=True)), ... ('property2', IntegerProperty()), @@ -359,50 +345,26 @@ def CustomObject(type='x-custom-type', properties=None): ... def __init__(self, property2=None, **kwargs): ... if property2 and property2 < 10: ... raise ValueError("'property2' is too small.") + """ - - def custom_builder(cls): - - class _Custom(cls, STIXDomainObject): - - if not re.match(TYPE_REGEX, type): - raise ValueError("Invalid type name '%s': must only contain the " - "characters a-z (lowercase ASCII), 0-9, and hyphen (-)." % type) - elif len(type) < 3 or len(type) > 250: - raise ValueError("Invalid type name '%s': must be between 3 and 250 characters." % type) - - _type = type - _properties = OrderedDict() - _properties.update([ - ('type', TypeProperty(_type)), - ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + def wrapper(cls): + _properties = list(itertools.chain.from_iterable([ + [ + ('type', TypeProperty(type)), + ('id', IDProperty(type)), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), - ]) - - if not properties or not isinstance(properties, list): - raise ValueError("Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]") - - _properties.update([x for x in properties if not x[0].startswith("x_")]) - - # This is to follow the general properties structure. - _properties.update([ + ], + [x for x in properties if not x[0].startswith('x_')], + [ ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), - ]) - - # Put all custom properties at the bottom, sorted alphabetically. - _properties.update(sorted([x for x in properties if x[0].startswith("x_")], key=lambda x: x[0])) - - def __init__(self, **kwargs): - _STIXBase.__init__(self, **kwargs) - _cls_init(cls, self, kwargs) - - stix2._register_type(_Custom, version="2.0") - return _Custom - - return custom_builder + ], + sorted([x for x in properties if x[0].startswith('x_')], key=lambda x: x[0]), + ])) + return _custom_object_builder(cls, type, _properties, '2.0') + return wrapper diff --git a/stix2/v20/sro.py b/stix2/v20/sro.py index e488229..dbf6812 100644 --- a/stix2/v20/sro.py +++ b/stix2/v20/sro.py @@ -2,30 +2,25 @@ from collections import OrderedDict -from ..base import _STIXBase -from ..markings import _MarkingsMixin -from ..properties import (BooleanProperty, IDProperty, IntegerProperty, - ListProperty, ReferenceProperty, StringProperty, - TimestampProperty, TypeProperty) +from ..core import STIXRelationshipObject +from ..properties import ( + BooleanProperty, IDProperty, IntegerProperty, ListProperty, + ReferenceProperty, StringProperty, TimestampProperty, TypeProperty, +) from ..utils import NOW from .common import ExternalReference, GranularMarking -class STIXRelationshipObject(_STIXBase, _MarkingsMixin): - pass - - class Relationship(STIXRelationshipObject): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ _type = 'relationship' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('relationship_type', StringProperty(required=True)), @@ -35,13 +30,15 @@ class Relationship(STIXRelationshipObject): ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) # Explicitly define the first three kwargs to make readable Relationship declarations. - def __init__(self, source_ref=None, relationship_type=None, - target_ref=None, **kwargs): + def __init__( + self, source_ref=None, relationship_type=None, + target_ref=None, **kwargs + ): # Allow (source_ref, relationship_type, target_ref) as positional args. if source_ref and not kwargs.get('source_ref'): kwargs['source_ref'] = source_ref @@ -59,24 +56,23 @@ class Sighting(STIXRelationshipObject): """ _type = 'sighting' - _properties = OrderedDict() - _properties.update([ + _properties = OrderedDict([ ('type', TypeProperty(_type)), ('id', IDProperty(_type)), - ('created_by_ref', ReferenceProperty(type="identity")), + ('created_by_ref', ReferenceProperty(type='identity')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), - ('count', IntegerProperty()), + ('count', IntegerProperty(min=0, max=999999999)), ('sighting_of_ref', ReferenceProperty(required=True)), - ('observed_data_refs', ListProperty(ReferenceProperty(type="observed-data"))), - ('where_sighted_refs', ListProperty(ReferenceProperty(type="identity"))), + ('observed_data_refs', ListProperty(ReferenceProperty(type='observed-data'))), + ('where_sighted_refs', ListProperty(ReferenceProperty(type='identity'))), ('summary', BooleanProperty(default=lambda: False)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(type="marking-definition"))), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), ('granular_markings', ListProperty(GranularMarking)), ]) diff --git a/stix2/v21/__init__.py b/stix2/v21/__init__.py new file mode 100644 index 0000000..4a8fe29 --- /dev/null +++ b/stix2/v21/__init__.py @@ -0,0 +1,94 @@ +"""STIX 2.1 API Objects.""" + +# flake8: noqa + +from .bundle import Bundle +from .common import ( + TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, CustomMarking, ExternalReference, + GranularMarking, KillChainPhase, LanguageContent, MarkingDefinition, + StatementMarking, TLPMarking, +) +from .observables import ( + URL, AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, + CustomExtension, CustomObservable, Directory, DomainName, EmailAddress, + EmailMessage, EmailMIMEComponent, File, HTTPRequestExt, ICMPExt, + IPv4Address, IPv6Address, MACAddress, Mutex, NetworkTraffic, NTFSExt, + PDFExt, Process, RasterImageExt, SocketExt, Software, TCPExt, + UNIXAccountExt, UserAccount, WindowsPEBinaryExt, + WindowsPEOptionalHeaderType, WindowsPESection, WindowsProcessExt, + WindowsRegistryKey, WindowsRegistryValueType, WindowsServiceExt, + X509Certificate, X509V3ExtenstionsType, +) +from .sdo import ( + AttackPattern, Campaign, CourseOfAction, CustomObject, Identity, Indicator, + IntrusionSet, Location, Malware, Note, ObservedData, Opinion, Report, + ThreatActor, Tool, Vulnerability, +) +from .sro import Relationship, Sighting + +OBJ_MAP = { + 'attack-pattern': AttackPattern, + 'bundle': Bundle, + 'campaign': Campaign, + 'course-of-action': CourseOfAction, + 'identity': Identity, + 'indicator': Indicator, + 'intrusion-set': IntrusionSet, + 'language-content': LanguageContent, + 'location': Location, + 'malware': Malware, + 'note': Note, + 'marking-definition': MarkingDefinition, + 'observed-data': ObservedData, + 'opinion': Opinion, + 'report': Report, + 'relationship': Relationship, + 'threat-actor': ThreatActor, + 'tool': Tool, + 'sighting': Sighting, + 'vulnerability': Vulnerability, +} + +OBJ_MAP_OBSERVABLE = { + 'artifact': Artifact, + 'autonomous-system': AutonomousSystem, + 'directory': Directory, + 'domain-name': DomainName, + 'email-addr': EmailAddress, + 'email-message': EmailMessage, + 'file': File, + 'ipv4-addr': IPv4Address, + 'ipv6-addr': IPv6Address, + 'mac-addr': MACAddress, + 'mutex': Mutex, + 'network-traffic': NetworkTraffic, + 'process': Process, + 'software': Software, + 'url': URL, + 'user-account': UserAccount, + 'windows-registry-key': WindowsRegistryKey, + 'x509-certificate': X509Certificate, +} + +EXT_MAP = { + 'file': { + 'archive-ext': ArchiveExt, + 'ntfs-ext': NTFSExt, + 'pdf-ext': PDFExt, + 'raster-image-ext': RasterImageExt, + 'windows-pebinary-ext': WindowsPEBinaryExt, + }, + 'network-traffic': { + 'http-request-ext': HTTPRequestExt, + 'icmp-ext': ICMPExt, + 'socket-ext': SocketExt, + 'tcp-ext': TCPExt, + }, + 'process': { + 'windows-process-ext': WindowsProcessExt, + 'windows-service-ext': WindowsServiceExt, + }, + 'user-account': { + 'unix-account-ext': UNIXAccountExt, + }, +} diff --git a/stix2/v21/bundle.py b/stix2/v21/bundle.py new file mode 100644 index 0000000..c9e083a --- /dev/null +++ b/stix2/v21/bundle.py @@ -0,0 +1,35 @@ +"""STIX 2.1 Bundle Representation.""" + +from collections import OrderedDict + +from ..base import _STIXBase +from ..properties import ( + IDProperty, ListProperty, STIXObjectProperty, TypeProperty, +) + + +class Bundle(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'bundle' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('id', IDProperty(_type)), + ('objects', ListProperty(STIXObjectProperty(spec_version='2.1'))), + ]) + + def __init__(self, *args, **kwargs): + # Add any positional arguments to the 'objects' kwarg. + if args: + if isinstance(args[0], list): + kwargs['objects'] = args[0] + list(args[1:]) + kwargs.get('objects', []) + else: + kwargs['objects'] = list(args) + kwargs.get('objects', []) + + self.__allow_custom = kwargs.get('allow_custom', False) + self._properties['objects'].contained.allow_custom = kwargs.get('allow_custom', False) + + super(Bundle, self).__init__(**kwargs) diff --git a/stix2/v21/common.py b/stix2/v21/common.py new file mode 100644 index 0000000..0aded3b --- /dev/null +++ b/stix2/v21/common.py @@ -0,0 +1,231 @@ +"""STIX 2.1 Common Data Types and Properties.""" + +from collections import OrderedDict +import copy + +from ..base import _STIXBase +from ..custom import _custom_marking_builder +from ..markings import _MarkingsMixin +from ..properties import ( + BooleanProperty, DictionaryProperty, HashesProperty, IDProperty, + IntegerProperty, ListProperty, Property, ReferenceProperty, + SelectorProperty, StringProperty, TimestampProperty, TypeProperty, +) +from ..utils import NOW, _get_dict + + +class ExternalReference(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _properties = OrderedDict([ + ('source_name', StringProperty(required=True)), + ('description', StringProperty()), + ('url', StringProperty()), + ('hashes', HashesProperty(spec_version='2.1')), + ('external_id', StringProperty()), + ]) + + def _check_object_constraints(self): + super(ExternalReference, self)._check_object_constraints() + self._check_at_least_one_property(['description', 'external_id', 'url']) + + +class KillChainPhase(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _properties = OrderedDict([ + ('kill_chain_name', StringProperty(required=True)), + ('phase_name', StringProperty(required=True)), + ]) + + +class GranularMarking(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _properties = OrderedDict([ + ('lang', StringProperty()), + ('marking_ref', ReferenceProperty(type='marking-definition')), + ('selectors', ListProperty(SelectorProperty, required=True)), + ]) + + def _check_object_constraints(self): + super(GranularMarking, self)._check_object_constraints() + self._check_at_least_one_property(['lang', 'marking_ref']) + + +class LanguageContent(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'language-content' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('object_ref', ReferenceProperty(required=True)), + # TODO: 'object_modified' it MUST be an exact match for the modified time of the STIX Object (SRO or SDO) being referenced. + ('object_modified', TimestampProperty(required=True, precision='millisecond')), + # TODO: 'contents' https://docs.google.com/document/d/1ShNq4c3e1CkfANmD9O--mdZ5H0O_GLnjN28a_yrEaco/edit#heading=h.cfz5hcantmvx + ('contents', DictionaryProperty(spec_version='2.1', required=True)), + ('revoked', BooleanProperty()), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class TLPMarking(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'tlp' + _properties = OrderedDict([ + ('tlp', StringProperty(required=True)), + ]) + + +class StatementMarking(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'statement' + _properties = OrderedDict([ + ('statement', StringProperty(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) + + +class MarkingProperty(Property): + """Represent the marking objects in the ``definition`` property of + marking-definition objects. + """ + + def clean(self, value): + if type(value) in OBJ_MAP_MARKING.values(): + return value + else: + raise ValueError("must be a Statement, TLP Marking or a registered marking.") + + +class MarkingDefinition(_STIXBase, _MarkingsMixin): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'marking-definition' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW)), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ('definition_type', StringProperty(required=True)), + ('definition', MarkingProperty(required=True)), + ]) + + def __init__(self, **kwargs): + if set(('definition_type', 'definition')).issubset(kwargs.keys()): + # Create correct marking type object + try: + marking_type = OBJ_MAP_MARKING[kwargs['definition_type']] + except KeyError: + raise ValueError("definition_type must be a valid marking type") + + if marking_type == TLPMarking: + # TLP instances in the spec have millisecond precision unlike other markings + self._properties = copy.deepcopy(self._properties) + self._properties.update([ + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ]) + + if not isinstance(kwargs['definition'], marking_type): + defn = _get_dict(kwargs['definition']) + kwargs['definition'] = marking_type(**defn) + + super(MarkingDefinition, self).__init__(**kwargs) + + +OBJ_MAP_MARKING = { + 'tlp': TLPMarking, + 'statement': StatementMarking, +} + + +def CustomMarking(type='x-custom-marking', properties=None): + """Custom STIX Marking decorator. + + Example: + >>> from stix2.v21 import CustomMarking + >>> from stix2.properties import IntegerProperty, StringProperty + >>> @CustomMarking('x-custom-marking', [ + ... ('property1', StringProperty(required=True)), + ... ('property2', IntegerProperty()), + ... ]) + ... class MyNewMarkingObjectType(): + ... pass + + """ + def wrapper(cls): + return _custom_marking_builder(cls, type, properties, '2.1') + return wrapper + + +# TODO: don't allow the creation of any other TLPMarkings than the ones below + +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/v21/observables.py b/stix2/v21/observables.py new file mode 100644 index 0000000..1b2251d --- /dev/null +++ b/stix2/v21/observables.py @@ -0,0 +1,867 @@ +"""STIX 2.1 Cyber Observable Objects. + +Embedded observable object types, such as Email MIME Component, which is +embedded in Email Message objects, inherit from ``_STIXBase`` instead of +Observable and do not have a ``_type`` attribute. +""" + +from collections import OrderedDict +import itertools + +from ..base import _Extension, _Observable, _STIXBase +from ..custom import _custom_extension_builder, _custom_observable_builder +from ..exceptions import AtLeastOnePropertyError, DependentPropertiesError +from ..properties import ( + BinaryProperty, BooleanProperty, DictionaryProperty, + EmbeddedObjectProperty, EnumProperty, ExtensionsProperty, FloatProperty, + HashesProperty, HexProperty, IntegerProperty, ListProperty, + ObjectReferenceProperty, StringProperty, TimestampProperty, TypeProperty, +) + + +class Artifact(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'artifact' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('mime_type', StringProperty()), + ('payload_bin', BinaryProperty()), + ('url', StringProperty()), + ('hashes', HashesProperty(spec_version='2.1')), + ('encryption_algorithm', StringProperty()), + ('decryption_key', StringProperty()), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + def _check_object_constraints(self): + super(Artifact, self)._check_object_constraints() + self._check_mutually_exclusive_properties(['payload_bin', 'url']) + self._check_properties_dependency(['hashes'], ['url']) + + +class AutonomousSystem(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'autonomous-system' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('number', IntegerProperty(required=True)), + ('name', StringProperty()), + ('rir', StringProperty()), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class Directory(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'directory' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('path', StringProperty(required=True)), + ('path_enc', StringProperty()), + # these are not the created/modified timestamps of the object itself + ('created', TimestampProperty()), + ('modified', TimestampProperty()), + ('accessed', TimestampProperty()), + ('contains_refs', ListProperty(ObjectReferenceProperty(valid_types=['file', 'directory']))), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class DomainName(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'domain-name' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('value', StringProperty(required=True)), + ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'domain-name']))), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class EmailAddress(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'email-addr' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('value', StringProperty(required=True)), + ('display_name', StringProperty()), + ('belongs_to_ref', ObjectReferenceProperty(valid_types='user-account')), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class EmailMIMEComponent(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _properties = OrderedDict([ + ('body', StringProperty()), + ('body_raw_ref', ObjectReferenceProperty(valid_types=['artifact', 'file'])), + ('content_type', StringProperty()), + ('content_disposition', StringProperty()), + ]) + + def _check_object_constraints(self): + super(EmailMIMEComponent, self)._check_object_constraints() + self._check_at_least_one_property(['body', 'body_raw_ref']) + + +class EmailMessage(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'email-message' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('is_multipart', BooleanProperty(required=True)), + ('date', TimestampProperty()), + ('content_type', StringProperty()), + ('from_ref', ObjectReferenceProperty(valid_types='email-addr')), + ('sender_ref', ObjectReferenceProperty(valid_types='email-addr')), + ('to_refs', ListProperty(ObjectReferenceProperty(valid_types='email-addr'))), + ('cc_refs', ListProperty(ObjectReferenceProperty(valid_types='email-addr'))), + ('bcc_refs', ListProperty(ObjectReferenceProperty(valid_types='email-addr'))), + ('subject', StringProperty()), + ('received_lines', ListProperty(StringProperty)), + ('additional_header_fields', DictionaryProperty(spec_version='2.1')), + ('body', StringProperty()), + ('body_multipart', ListProperty(EmbeddedObjectProperty(type=EmailMIMEComponent))), + ('raw_email_ref', ObjectReferenceProperty(valid_types='artifact')), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + def _check_object_constraints(self): + super(EmailMessage, self)._check_object_constraints() + self._check_properties_dependency(['is_multipart'], ['body_multipart']) + if self.get('is_multipart') is True and self.get('body'): + # 'body' MAY only be used if is_multipart is false. + raise DependentPropertiesError(self.__class__, [('is_multipart', 'body')]) + + +class ArchiveExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'archive-ext' + _properties = OrderedDict([ + ('contains_refs', ListProperty(ObjectReferenceProperty(valid_types='file'), required=True)), + ('comment', StringProperty()), + ]) + + +class AlternateDataStream(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _properties = OrderedDict([ + ('name', StringProperty(required=True)), + ('hashes', HashesProperty(spec_version='2.1')), + ('size', IntegerProperty()), + ]) + + +class NTFSExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'ntfs-ext' + _properties = OrderedDict([ + ('sid', StringProperty()), + ('alternate_data_streams', ListProperty(EmbeddedObjectProperty(type=AlternateDataStream))), + ]) + + +class PDFExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'pdf-ext' + _properties = OrderedDict([ + ('version', StringProperty()), + ('is_optimized', BooleanProperty()), + ('document_info_dict', DictionaryProperty(spec_version='2.1')), + ('pdfid0', StringProperty()), + ('pdfid1', StringProperty()), + ]) + + +class RasterImageExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'raster-image-ext' + _properties = OrderedDict([ + ('image_height', IntegerProperty()), + ('image_width', IntegerProperty()), + ('bits_per_pixel', IntegerProperty()), + ('exif_tags', DictionaryProperty(spec_version='2.1')), + ]) + + +class WindowsPEOptionalHeaderType(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _properties = OrderedDict([ + ('magic_hex', HexProperty()), + ('major_linker_version', IntegerProperty()), + ('minor_linker_version', IntegerProperty()), + ('size_of_code', IntegerProperty(min=0)), + ('size_of_initialized_data', IntegerProperty(min=0)), + ('size_of_uninitialized_data', IntegerProperty(min=0)), + ('address_of_entry_point', IntegerProperty()), + ('base_of_code', IntegerProperty()), + ('base_of_data', IntegerProperty()), + ('image_base', IntegerProperty()), + ('section_alignment', IntegerProperty()), + ('file_alignment', IntegerProperty()), + ('major_os_version', IntegerProperty()), + ('minor_os_version', IntegerProperty()), + ('major_image_version', IntegerProperty()), + ('minor_image_version', IntegerProperty()), + ('major_subsystem_version', IntegerProperty()), + ('minor_subsystem_version', IntegerProperty()), + ('win32_version_value_hex', HexProperty()), + ('size_of_image', IntegerProperty(min=0)), + ('size_of_headers', IntegerProperty(min=0)), + ('checksum_hex', HexProperty()), + ('subsystem_hex', HexProperty()), + ('dll_characteristics_hex', HexProperty()), + ('size_of_stack_reserve', IntegerProperty(min=0)), + ('size_of_stack_commit', IntegerProperty(min=0)), + ('size_of_heap_reserve', IntegerProperty()), + ('size_of_heap_commit', IntegerProperty()), + ('loader_flags_hex', HexProperty()), + ('number_of_rva_and_sizes', IntegerProperty()), + ('hashes', HashesProperty(spec_version='2.1')), + ]) + + def _check_object_constraints(self): + super(WindowsPEOptionalHeaderType, self)._check_object_constraints() + self._check_at_least_one_property() + + +class WindowsPESection(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _properties = OrderedDict([ + ('name', StringProperty(required=True)), + ('size', IntegerProperty(min=0)), + ('entropy', FloatProperty()), + ('hashes', HashesProperty(spec_version='2.1')), + ]) + + +class WindowsPEBinaryExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'windows-pebinary-ext' + _properties = OrderedDict([ + ('pe_type', StringProperty(required=True)), # open_vocab + ('imphash', StringProperty()), + ('machine_hex', HexProperty()), + ('number_of_sections', IntegerProperty(min=0)), + ('time_date_stamp', TimestampProperty(precision='second')), + ('pointer_to_symbol_table_hex', HexProperty()), + ('number_of_symbols', IntegerProperty(min=0)), + ('size_of_optional_header', IntegerProperty(min=0)), + ('characteristics_hex', HexProperty()), + ('file_header_hashes', HashesProperty(spec_version='2.1')), + ('optional_header', EmbeddedObjectProperty(type=WindowsPEOptionalHeaderType)), + ('sections', ListProperty(EmbeddedObjectProperty(type=WindowsPESection))), + ]) + + +class File(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'file' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('hashes', HashesProperty(spec_version='2.1')), + ('size', IntegerProperty(min=0)), + ('name', StringProperty()), + ('name_enc', StringProperty()), + ('magic_number_hex', HexProperty()), + ('mime_type', StringProperty()), + # these are not the created/modified timestamps of the object itself + ('created', TimestampProperty()), + ('modified', TimestampProperty()), + ('accessed', TimestampProperty()), + ('parent_directory_ref', ObjectReferenceProperty(valid_types='directory')), + ('contains_refs', ListProperty(ObjectReferenceProperty)), + ('content_ref', ObjectReferenceProperty(valid_types='artifact')), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + def _check_object_constraints(self): + super(File, self)._check_object_constraints() + self._check_at_least_one_property(['hashes', 'name']) + + +class IPv4Address(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'ipv4-addr' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('value', StringProperty(required=True)), + ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))), + ('belongs_to_refs', ListProperty(ObjectReferenceProperty(valid_types='autonomous-system'))), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class IPv6Address(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'ipv6-addr' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('value', StringProperty(required=True)), + ('resolves_to_refs', ListProperty(ObjectReferenceProperty(valid_types='mac-addr'))), + ('belongs_to_refs', ListProperty(ObjectReferenceProperty(valid_types='autonomous-system'))), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class MACAddress(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'mac-addr' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('value', StringProperty(required=True)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class Mutex(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'mutex' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('name', StringProperty(required=True)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class HTTPRequestExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'http-request-ext' + _properties = OrderedDict([ + ('request_method', StringProperty(required=True)), + ('request_value', StringProperty(required=True)), + ('request_version', StringProperty()), + ('request_header', DictionaryProperty(spec_version='2.1')), + ('message_body_length', IntegerProperty()), + ('message_body_data_ref', ObjectReferenceProperty(valid_types='artifact')), + ]) + + +class ICMPExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'icmp-ext' + _properties = OrderedDict([ + ('icmp_type_hex', HexProperty(required=True)), + ('icmp_code_hex', HexProperty(required=True)), + ]) + + +class SocketExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'socket-ext' + _properties = OrderedDict([ + ( + 'address_family', EnumProperty( + allowed=[ + "AF_UNSPEC", + "AF_INET", + "AF_IPX", + "AF_APPLETALK", + "AF_NETBIOS", + "AF_INET6", + "AF_IRDA", + "AF_BTH", + ], required=True, + ), + ), + ('is_blocking', BooleanProperty()), + ('is_listening', BooleanProperty()), + ( + 'protocol_family', EnumProperty(allowed=[ + "PF_INET", + "PF_IPX", + "PF_APPLETALK", + "PF_INET6", + "PF_AX25", + "PF_NETROM", + ]), + ), + ('options', DictionaryProperty(spec_version='2.1')), + ( + 'socket_type', EnumProperty(allowed=[ + "SOCK_STREAM", + "SOCK_DGRAM", + "SOCK_RAW", + "SOCK_RDM", + "SOCK_SEQPACKET", + ]), + ), + ('socket_descriptor', IntegerProperty(min=0)), + ('socket_handle', IntegerProperty()), + ]) + + +class TCPExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'tcp-ext' + _properties = OrderedDict([ + ('src_flags_hex', HexProperty()), + ('dst_flags_hex', HexProperty()), + ]) + + +class NetworkTraffic(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'network-traffic' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('start', TimestampProperty()), + ('end', TimestampProperty()), + ('is_active', BooleanProperty()), + ('src_ref', ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'mac-addr', 'domain-name'])), + ('dst_ref', ObjectReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'mac-addr', 'domain-name'])), + ('src_port', IntegerProperty(min=0, max=65535)), + ('dst_port', IntegerProperty(min=0, max=65535)), + ('protocols', ListProperty(StringProperty, required=True)), + ('src_byte_count', IntegerProperty(min=0)), + ('dst_byte_count', IntegerProperty(min=0)), + ('src_packets', IntegerProperty(min=0)), + ('dst_packets', IntegerProperty(min=0)), + ('ipfix', DictionaryProperty(spec_version='2.1')), + ('src_payload_ref', ObjectReferenceProperty(valid_types='artifact')), + ('dst_payload_ref', ObjectReferenceProperty(valid_types='artifact')), + ('encapsulates_refs', ListProperty(ObjectReferenceProperty(valid_types='network-traffic'))), + ('encapsulates_by_ref', ObjectReferenceProperty(valid_types='network-traffic')), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + def _check_object_constraints(self): + super(NetworkTraffic, self)._check_object_constraints() + self._check_at_least_one_property(['src_ref', 'dst_ref']) + + start = self.get('start') + end = self.get('end') + is_active = self.get('is_active') + + if end and is_active is not False: + msg = "{0.id} 'is_active' must be False if 'end' is present" + raise ValueError(msg.format(self)) + + if end and is_active is True: + msg = "{0.id} if 'is_active' is True, 'end' must not be included" + raise ValueError(msg.format(self)) + + if start and end and end <= start: + msg = "{0.id} 'end' must be greater than 'start'" + raise ValueError(msg.format(self)) + + +class WindowsProcessExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'windows-process-ext' + _properties = OrderedDict([ + ('aslr_enabled', BooleanProperty()), + ('dep_enabled', BooleanProperty()), + ('priority', StringProperty()), + ('owner_sid', StringProperty()), + ('window_title', StringProperty()), + ('startup_info', DictionaryProperty(spec_version='2.1')), + ( + 'integrity_level', EnumProperty(allowed=[ + "low", + "medium", + "high", + "system", + ]), + ), + ]) + + +class WindowsServiceExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'windows-service-ext' + _properties = OrderedDict([ + ('service_name', StringProperty()), + ('descriptions', ListProperty(StringProperty)), + ('display_name', StringProperty()), + ('group_name', StringProperty()), + ( + 'start_type', EnumProperty(allowed=[ + "SERVICE_AUTO_START", + "SERVICE_BOOT_START", + "SERVICE_DEMAND_START", + "SERVICE_DISABLED", + "SERVICE_SYSTEM_ALERT", + ]), + ), + ('service_dll_refs', ListProperty(ObjectReferenceProperty(valid_types='file'))), + ( + 'service_type', EnumProperty(allowed=[ + "SERVICE_KERNEL_DRIVER", + "SERVICE_FILE_SYSTEM_DRIVER", + "SERVICE_WIN32_OWN_PROCESS", + "SERVICE_WIN32_SHARE_PROCESS", + ]), + ), + ( + 'service_status', EnumProperty(allowed=[ + "SERVICE_CONTINUE_PENDING", + "SERVICE_PAUSE_PENDING", + "SERVICE_PAUSED", + "SERVICE_RUNNING", + "SERVICE_START_PENDING", + "SERVICE_STOP_PENDING", + "SERVICE_STOPPED", + ]), + ), + ]) + + +class Process(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'process' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('is_hidden', BooleanProperty()), + ('pid', IntegerProperty()), + # this is not the created timestamps of the object itself + ('created', TimestampProperty()), + ('cwd', StringProperty()), + ('command_line', StringProperty()), + ('environment_variables', DictionaryProperty(spec_version='2.1')), + ('opened_connection_refs', ListProperty(ObjectReferenceProperty(valid_types='network-traffic'))), + ('creator_user_ref', ObjectReferenceProperty(valid_types='user-account')), + ('image_ref', ObjectReferenceProperty(valid_types='file')), + ('parent_ref', ObjectReferenceProperty(valid_types='process')), + ('child_refs', ListProperty(ObjectReferenceProperty('process'))), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + def _check_object_constraints(self): + # no need to check windows-service-ext, since it has a required property + super(Process, self)._check_object_constraints() + try: + self._check_at_least_one_property() + if 'windows-process-ext' in self.get('extensions', {}): + self.extensions['windows-process-ext']._check_at_least_one_property() + except AtLeastOnePropertyError as enclosing_exc: + if 'extensions' not in self: + raise enclosing_exc + else: + if 'windows-process-ext' in self.get('extensions', {}): + self.extensions['windows-process-ext']._check_at_least_one_property() + + +class Software(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'software' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('name', StringProperty(required=True)), + ('cpe', StringProperty()), + ('languages', ListProperty(StringProperty)), + ('vendor', StringProperty()), + ('version', StringProperty()), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class URL(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'url' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('value', StringProperty(required=True)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class UNIXAccountExt(_Extension): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'unix-account-ext' + _properties = OrderedDict([ + ('gid', IntegerProperty()), + ('groups', ListProperty(StringProperty)), + ('home_dir', StringProperty()), + ('shell', StringProperty()), + ]) + + +class UserAccount(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'user-account' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('user_id', StringProperty()), + ('credential', StringProperty()), + ('account_login', StringProperty()), + ('account_type', StringProperty()), # open vocab + ('display_name', StringProperty()), + ('is_service_account', BooleanProperty()), + ('is_privileged', BooleanProperty()), + ('can_escalate_privs', BooleanProperty()), + ('is_disabled', BooleanProperty()), + ('account_created', TimestampProperty()), + ('account_expires', TimestampProperty()), + ('credential_last_changed', TimestampProperty()), + ('account_first_login', TimestampProperty()), + ('account_last_login', TimestampProperty()), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +class WindowsRegistryValueType(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'windows-registry-value-type' + _properties = OrderedDict([ + ('name', StringProperty()), + ('data', StringProperty()), + ( + 'data_type', EnumProperty(allowed=[ + "REG_NONE", + "REG_SZ", + "REG_EXPAND_SZ", + "REG_BINARY", + "REG_DWORD", + "REG_DWORD_BIG_ENDIAN", + "REG_LINK", + "REG_MULTI_SZ", + "REG_RESOURCE_LIST", + "REG_FULL_RESOURCE_DESCRIPTION", + "REG_RESOURCE_REQUIREMENTS_LIST", + "REG_QWORD", + "REG_INVALID_TYPE", + ]), + ), + ]) + + +class WindowsRegistryKey(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'windows-registry-key' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('key', StringProperty()), + ('values', ListProperty(EmbeddedObjectProperty(type=WindowsRegistryValueType))), + # this is not the modified timestamps of the object itself + ('modified', TimestampProperty()), + ('creator_user_ref', ObjectReferenceProperty(valid_types='user-account')), + ('number_of_subkeys', IntegerProperty()), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + @property + def values(self): + # Needed because 'values' is a property on collections.Mapping objects + return self._inner['values'] + + +class X509V3ExtenstionsType(_STIXBase): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'x509-v3-extensions-type' + _properties = OrderedDict([ + ('basic_constraints', StringProperty()), + ('name_constraints', StringProperty()), + ('policy_constraints', StringProperty()), + ('key_usage', StringProperty()), + ('extended_key_usage', StringProperty()), + ('subject_key_identifier', StringProperty()), + ('authority_key_identifier', StringProperty()), + ('subject_alternative_name', StringProperty()), + ('issuer_alternative_name', StringProperty()), + ('subject_directory_attributes', StringProperty()), + ('crl_distribution_points', StringProperty()), + ('inhibit_any_policy', StringProperty()), + ('private_key_usage_period_not_before', TimestampProperty()), + ('private_key_usage_period_not_after', TimestampProperty()), + ('certificate_policies', StringProperty()), + ('policy_mappings', StringProperty()), + ]) + + +class X509Certificate(_Observable): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'x509-certificate' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('is_self_signed', BooleanProperty()), + ('hashes', HashesProperty(spec_version='2.1')), + ('version', StringProperty()), + ('serial_number', StringProperty()), + ('signature_algorithm', StringProperty()), + ('issuer', StringProperty()), + ('validity_not_before', TimestampProperty()), + ('validity_not_after', TimestampProperty()), + ('subject', StringProperty()), + ('subject_public_key_algorithm', StringProperty()), + ('subject_public_key_modulus', StringProperty()), + ('subject_public_key_exponent', IntegerProperty()), + ('x509_v3_extensions', EmbeddedObjectProperty(type=X509V3ExtenstionsType)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), + ]) + + +def CustomObservable(type='x-custom-observable', properties=None): + """Custom STIX Cyber Observable Object type decorator. + + Example: + >>> from stix2.v21 import CustomObservable + >>> from stix2.properties import IntegerProperty, StringProperty + >>> @CustomObservable('x-custom-observable', [ + ... ('property1', StringProperty(required=True)), + ... ('property2', IntegerProperty()), + ... ]) + ... class MyNewObservableType(): + ... pass + + """ + def wrapper(cls): + _properties = list(itertools.chain.from_iterable([ + [('type', TypeProperty(type))], + properties, + [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))], + ])) + return _custom_observable_builder(cls, type, _properties, '2.1') + return wrapper + + +def CustomExtension(observable=None, type='x-custom-observable-ext', properties=None): + """Decorator for custom extensions to STIX Cyber Observables. + """ + def wrapper(cls): + return _custom_extension_builder(cls, observable, type, properties, '2.1') + return wrapper diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py new file mode 100644 index 0000000..37699a6 --- /dev/null +++ b/stix2/v21/sdo.py @@ -0,0 +1,581 @@ +"""STIX 2.1 Domain Objects.""" + +from collections import OrderedDict +import itertools + +from ..core import STIXDomainObject +from ..custom import _custom_object_builder +from ..properties import ( + BooleanProperty, EnumProperty, FloatProperty, IDProperty, IntegerProperty, + ListProperty, ObservableProperty, PatternProperty, ReferenceProperty, + StringProperty, TimestampProperty, TypeProperty, +) +from ..utils import NOW +from .common import ExternalReference, GranularMarking, KillChainPhase + + +class AttackPattern(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'attack-pattern' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('kill_chain_phases', ListProperty(KillChainPhase)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class Campaign(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'campaign' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('aliases', ListProperty(StringProperty)), + ('first_seen', TimestampProperty()), + ('last_seen', TimestampProperty()), + ('objective', StringProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + first_seen = self.get('first_seen') + last_seen = self.get('last_seen') + + if first_seen and last_seen and last_seen < first_seen: + msg = "{0.id} 'last_seen' must be greater than or equal 'first_seen'" + raise ValueError(msg.format(self)) + + +class CourseOfAction(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'course-of-action' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class Identity(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'identity' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('roles', ListProperty(StringProperty)), + ('identity_class', StringProperty(required=True)), + ('sectors', ListProperty(StringProperty)), + ('contact_information', StringProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class Indicator(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'indicator' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty()), + ('description', StringProperty()), + ('indicator_types', ListProperty(StringProperty, required=True)), + ('pattern', PatternProperty(required=True)), + ('valid_from', TimestampProperty(default=lambda: NOW)), + ('valid_until', TimestampProperty()), + ('kill_chain_phases', ListProperty(KillChainPhase)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + valid_from = self.get('valid_from') + valid_until = self.get('valid_until') + + if valid_from and valid_until and valid_until <= valid_from: + msg = "{0.id} 'valid_until' must be greater than 'valid_from'" + raise ValueError(msg.format(self)) + + +class IntrusionSet(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'intrusion-set' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('aliases', ListProperty(StringProperty)), + ('first_seen', TimestampProperty()), + ('last_seen', TimestampProperty()), + ('goals', ListProperty(StringProperty)), + ('resource_level', StringProperty()), + ('primary_motivation', StringProperty()), + ('secondary_motivations', ListProperty(StringProperty)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + first_seen = self.get('first_seen') + last_seen = self.get('last_seen') + + if first_seen and last_seen and last_seen < first_seen: + msg = "{0.id} 'last_seen' must be greater than or equal to 'first_seen'" + raise ValueError(msg.format(self)) + + +class Location(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'location' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('description', StringProperty()), + ('latitude', FloatProperty(min=-90.0, max=90.0)), + ('longitude', FloatProperty(min=-180.0, max=180.0)), + ('precision', FloatProperty(min=0.0)), + ('region', StringProperty()), + ('country', StringProperty()), + ('administrative_area', StringProperty()), + ('city', StringProperty()), + ('street_address', StringProperty()), + ('postal_code', StringProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + if self.get('precision') is not None: + self._check_properties_dependency(['longitude', 'latitude'], ['precision']) + + self._check_properties_dependency(['latitude'], ['longitude']) + self._check_properties_dependency(['longitude'], ['latitude']) + + +class Malware(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'malware' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('malware_types', ListProperty(StringProperty, required=True)), + ('kill_chain_phases', ListProperty(KillChainPhase)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class Note(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'note' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('abstract', StringProperty()), + ('content', StringProperty(required=True)), + ('authors', ListProperty(StringProperty)), + ('object_refs', ListProperty(ReferenceProperty, required=True)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class ObservedData(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'observed-data' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('first_observed', TimestampProperty(required=True)), + ('last_observed', TimestampProperty(required=True)), + ('number_observed', IntegerProperty(min=1, max=999999999, required=True)), + ('objects', ObservableProperty(spec_version='2.1', required=True)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + def __init__(self, *args, **kwargs): + self.__allow_custom = kwargs.get('allow_custom', False) + self._properties['objects'].allow_custom = kwargs.get('allow_custom', False) + + super(ObservedData, self).__init__(*args, **kwargs) + + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + if self.get('number_observed', 1) == 1: + self._check_properties_dependency(['first_observed'], ['last_observed']) + self._check_properties_dependency(['last_observed'], ['first_observed']) + + first_observed = self.get('first_observed') + last_observed = self.get('last_observed') + + if first_observed and last_observed and last_observed < first_observed: + msg = "{0.id} 'last_observed' must be greater than or equal to 'first_observed'" + raise ValueError(msg.format(self)) + + +class Opinion(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'opinion' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('explanation', StringProperty()), + ('authors', ListProperty(StringProperty)), + ('object_refs', ListProperty(ReferenceProperty, required=True)), + ( + 'opinion', EnumProperty( + allowed=[ + 'strongly-disagree', + 'disagree', + 'neutral', + 'agree', + 'strongly-agree', + ], required=True, + ), + ), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class Report(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'report' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('report_types', ListProperty(StringProperty, required=True)), + ('published', TimestampProperty(required=True)), + ('object_refs', ListProperty(ReferenceProperty, required=True)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class ThreatActor(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'threat-actor' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('threat_actor_types', ListProperty(StringProperty, required=True)), + ('aliases', ListProperty(StringProperty)), + ('roles', ListProperty(StringProperty)), + ('goals', ListProperty(StringProperty)), + ('sophistication', StringProperty()), + ('resource_level', StringProperty()), + ('primary_motivation', StringProperty()), + ('secondary_motivations', ListProperty(StringProperty)), + ('personal_motivations', ListProperty(StringProperty)), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class Tool(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'tool' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('tool_types', ListProperty(StringProperty, required=True)), + ('kill_chain_phases', ListProperty(KillChainPhase)), + ('tool_version', StringProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +class Vulnerability(STIXDomainObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'vulnerability' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('name', StringProperty(required=True)), + ('description', StringProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + +def CustomObject(type='x-custom-type', properties=None): + """Custom STIX Object type decorator. + + Example: + >>> from stix2.v21 import CustomObject + >>> from stix2.properties import IntegerProperty, StringProperty + >>> @CustomObject('x-type-name', [ + ... ('property1', StringProperty(required=True)), + ... ('property2', IntegerProperty()), + ... ]) + ... class MyNewObjectType(): + ... pass + + Supply an ``__init__()`` function to add any special validations to the custom + type. Don't call ``super().__init__()`` though - doing so will cause an error. + + Example: + >>> from stix2.v21 import CustomObject + >>> from stix2.properties import IntegerProperty, StringProperty + >>> @CustomObject('x-type-name', [ + ... ('property1', StringProperty(required=True)), + ... ('property2', IntegerProperty()), + ... ]) + ... class MyNewObjectType(): + ... def __init__(self, property2=None, **kwargs): + ... if property2 and property2 < 10: + ... raise ValueError("'property2' is too small.") + + """ + def wrapper(cls): + _properties = list(itertools.chain.from_iterable([ + [ + ('type', TypeProperty(type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ], + [x for x in properties if not x[0].startswith('x_')], + [ + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ], + sorted([x for x in properties if x[0].startswith('x_')], key=lambda x: x[0]), + ])) + return _custom_object_builder(cls, type, _properties, '2.1') + + return wrapper diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py new file mode 100644 index 0000000..f947b2e --- /dev/null +++ b/stix2/v21/sro.py @@ -0,0 +1,115 @@ +"""STIX 2.1 Relationship Objects.""" + +from collections import OrderedDict + +from ..core import STIXRelationshipObject +from ..properties import ( + BooleanProperty, IDProperty, IntegerProperty, ListProperty, + ReferenceProperty, StringProperty, TimestampProperty, TypeProperty, +) +from ..utils import NOW +from .common import ExternalReference, GranularMarking + + +class Relationship(STIXRelationshipObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'relationship' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('relationship_type', StringProperty(required=True)), + ('description', StringProperty()), + ('source_ref', ReferenceProperty(required=True)), + ('target_ref', ReferenceProperty(required=True)), + ('start_time', TimestampProperty()), + ('stop_time', TimestampProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + # Explicitly define the first three kwargs to make readable Relationship declarations. + def __init__( + self, source_ref=None, relationship_type=None, + target_ref=None, **kwargs + ): + # Allow (source_ref, relationship_type, target_ref) as positional args. + if source_ref and not kwargs.get('source_ref'): + kwargs['source_ref'] = source_ref + if relationship_type and not kwargs.get('relationship_type'): + kwargs['relationship_type'] = relationship_type + if target_ref and not kwargs.get('target_ref'): + kwargs['target_ref'] = target_ref + + super(Relationship, self).__init__(**kwargs) + + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + start_time = self.get('start_time') + stop_time = self.get('stop_time') + + if start_time and stop_time and stop_time <= start_time: + msg = "{0.id} 'stop_time' must be later than 'start_time'" + raise ValueError(msg.format(self)) + + +class Sighting(STIXRelationshipObject): + # TODO: Add link + """For more detailed information on this object's properties, see + `the STIX 2.1 specification `__. + """ + + _type = 'sighting' + _properties = OrderedDict([ + ('type', TypeProperty(_type)), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(_type)), + ('created_by_ref', ReferenceProperty(type='identity')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')), + ('first_seen', TimestampProperty()), + ('last_seen', TimestampProperty()), + ('count', IntegerProperty(min=0, max=999999999)), + ('sighting_of_ref', ReferenceProperty(required=True)), + ('observed_data_refs', ListProperty(ReferenceProperty(type='observed-data'))), + ('where_sighted_refs', ListProperty(ReferenceProperty(type='identity'))), + ('summary', BooleanProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition'))), + ('granular_markings', ListProperty(GranularMarking)), + ]) + + # Explicitly define the first kwargs to make readable Sighting declarations. + def __init__(self, sighting_of_ref=None, **kwargs): + # 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) + + def _check_object_constraints(self): + super(self.__class__, self)._check_object_constraints() + + first_seen = self.get('first_seen') + last_seen = self.get('last_seen') + + if first_seen and last_seen and last_seen <= first_seen: + msg = "{0.id} 'last_seen' must be later than 'first_seen'" + raise ValueError(msg.format(self)) diff --git a/stix2/workbench.py b/stix2/workbench.py index e34f028..e621073 100644 --- a/stix2/workbench.py +++ b/stix2/workbench.py @@ -1,4 +1,4 @@ -"""Functions and class wrappers for interacting with STIX data at a high level. +"""Functions and class wrappers for interacting with STIX2 data at a high level. .. autofunction:: create .. autofunction:: set_default_creator @@ -33,21 +33,23 @@ from . import Report as _Report from . import ThreatActor as _ThreatActor from . import Tool as _Tool from . import Vulnerability as _Vulnerability -from . import (AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, # noqa: F401 - Bundle, CustomExtension, CustomMarking, CustomObservable, - Directory, DomainName, EmailAddress, EmailMessage, - EmailMIMEComponent, Environment, ExtensionsProperty, - ExternalReference, File, FileSystemSource, Filter, - GranularMarking, HTTPRequestExt, ICMPExt, IPv4Address, - IPv6Address, KillChainPhase, MACAddress, MarkingDefinition, - MemoryStore, Mutex, NetworkTraffic, NTFSExt, parse_observable, - PDFExt, Process, RasterImageExt, Relationship, Sighting, - SocketExt, Software, StatementMarking, TAXIICollectionSource, - TCPExt, TLP_AMBER, TLP_GREEN, TLP_RED, TLP_WHITE, TLPMarking, - UNIXAccountExt, URL, UserAccount, WindowsPEBinaryExt, - WindowsPEOptionalHeaderType, WindowsPESection, - WindowsProcessExt, WindowsRegistryKey, WindowsRegistryValueType, - WindowsServiceExt, X509Certificate, X509V3ExtenstionsType) +from . import ( # noqa: F401 + AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, + Bundle, CustomExtension, CustomMarking, CustomObservable, + Directory, DomainName, EmailAddress, EmailMessage, + EmailMIMEComponent, Environment, ExternalReference, File, + FileSystemSource, Filter, GranularMarking, HTTPRequestExt, + ICMPExt, IPv4Address, IPv6Address, KillChainPhase, MACAddress, + MarkingDefinition, MemoryStore, Mutex, NetworkTraffic, NTFSExt, + parse_observable, PDFExt, Process, RasterImageExt, Relationship, + Sighting, SocketExt, Software, StatementMarking, + TAXIICollectionSource, TCPExt, TLP_AMBER, TLP_GREEN, TLP_RED, + TLP_WHITE, TLPMarking, UNIXAccountExt, URL, UserAccount, + WindowsPEBinaryExt, WindowsPEOptionalHeaderType, + WindowsPESection, WindowsProcessExt, WindowsRegistryKey, + WindowsRegistryValueType, WindowsServiceExt, X509Certificate, + X509V3ExtenstionsType +) from .datastore.filters import FilterSet # Use an implicit MemoryStore @@ -75,9 +77,11 @@ add_data_sources = _environ.source.add_data_sources # Wrap SDOs with helper functions -STIX_OBJS = [_AttackPattern, _Campaign, _CourseOfAction, _Identity, - _Indicator, _IntrusionSet, _Malware, _ObservedData, _Report, - _ThreatActor, _Tool, _Vulnerability] +STIX_OBJS = [ + _AttackPattern, _Campaign, _CourseOfAction, _Identity, + _Indicator, _IntrusionSet, _Malware, _ObservedData, _Report, + _ThreatActor, _Tool, _Vulnerability, +] STIX_OBJ_DOCS = """ @@ -93,9 +97,11 @@ STIX_OBJ_DOCS = """ {} -""".format(_environ.creator_of.__doc__, - _environ.relationships.__doc__, - _environ.related_to.__doc__) +""".format( + _environ.creator_of.__doc__, + _environ.relationships.__doc__, + _environ.related_to.__doc__ +) def _created_by_wrapper(self, *args, **kwargs): @@ -143,7 +149,7 @@ def _setup_workbench(): for obj_type in STIX_OBJS: new_class_dict = { '__new__': _constructor_wrapper(obj_type), - '__doc__': 'Workbench wrapper around the `{0} `__ object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS) + '__doc__': 'Workbench wrapper around the `{0} `__ object. {1}'.format(obj_type.__name__, STIX_OBJ_DOCS), } new_class = type(obj_type.__name__, (), new_class_dict) diff --git a/tox.ini b/tox.ini index 47fcce8..f3a10fb 100644 --- a/tox.ini +++ b/tox.ini @@ -11,8 +11,9 @@ deps = taxii2-client medallion commands = - pytest --ignore=stix2/test/test_workbench.py --cov=stix2 stix2/test/ --cov-report term-missing - pytest stix2/test/test_workbench.py --cov=stix2 --cov-report term-missing --cov-append + pytest --ignore=stix2/test/v20/test_workbench.py --ignore=stix2/test/v21/test_workbench.py --cov=stix2 stix2/test/ --cov-report term-missing + pytest stix2/test/v20/test_workbench.py --cov=stix2 --cov-report term-missing --cov-append + pytest stix2/test/v21/test_workbench.py --cov=stix2 --cov-report term-missing --cov-append passenv = CI TRAVIS TRAVIS_*