diff --git a/.github/workflows/python-ci-tests.yml b/.github/workflows/python-ci-tests.yml new file mode 100644 index 0000000..dc9ccc0 --- /dev/null +++ b/.github/workflows/python-ci-tests.yml @@ -0,0 +1,33 @@ +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: cti-python-stix2 test harness +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + + name: Python ${{ matrix.python-version }} Build + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install and update essential dependencies + run: | + pip install -U pip setuptools + pip install tox-gh-actions + pip install codecov + - name: Test with Tox + run: | + tox + - name: Upload coverage information to Codecov + uses: codecov/codecov-action@v1 + with: + fail_ci_if_error: true # optional (default = false) + verbose: true # optional (default = false) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7620c35..d127dd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,25 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.3.0 + rev: v3.4.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 + rev: v2.0.2 hooks: - id: add-trailing-comma -- repo: https://github.com/FalconSocial/pre-commit-python-sorter - sha: b57843b0b874df1d16eb0bef00b868792cb245c2 +- repo: https://github.com/PyCQA/flake8 + rev: 3.8.4 hooks: - - id: python-import-sorter + - id: flake8 + name: Check project styling + args: + - --max-line-length=160 +- repo: https://github.com/PyCQA/isort + rev: 5.7.0 + hooks: + - id: isort + name: Sort python imports (shows diff) + args: ["-c", "--diff"] + - id: isort + name: Sort python imports (fixes files) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ac02e4f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -os: linux -language: python -cache: pip -dist: bionic -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" -install: - - pip install -U pip setuptools - - pip install tox-travis - - pip install codecov - - pip install pre-commit -script: - - tox - - pre-commit run --all-files -after_success: - - codecov diff --git a/CHANGELOG b/CHANGELOG index 87d3c77..9c2e65f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,32 @@ CHANGELOG ========= +2.1.0 - 2020-11-20 + +* #337 Switches fuzzywuzzy dependency for rapidfuzz (@maxbachmann) +* #430 Adds ability to mix single objects and lists in the Bundle constructor +* #445, #475 Adds ability to calculate semantic equivalence of indicator patterns +* #449 Adds ability to calculate semantic equivalence of entire graphs of objects +* #427 Fixes protocol_family property on network socket extension +* #436 Fixes pattern visitor to handle expressions with both AND and OR +* #431 Fixes bug when adding custom object to FileSystemSink if the object type + hasn't been registered +* #439 Fixes bug with custom wrapped classes not retaining their name (@maybe-sybr) +* #438 Fixes bug with patterns when the object path contains numeric index steps +* #454 Fixes stix2.patterns.make_constant() to create TimestampConstants +* #455 Fixes bug with AND comparisons in patterns +* #460 Fixes bug when retrieving custom object from TAXIICollectionSource if + the object type hasn't been registered +* #444 Fixes bug so CompositeDataSource and deduplicate() handle unversioned + objects correctly +* #467 Fixes bug in semantic equivalence when Location objects don't have + latitude and longitude properties +* #470 Fixes bug where Sighting's where_sighted_refs property couldn't point to + a Location object +* #473 Fixes typo in name of X509V3ExtensionsType class +* #474 Fixes order of object properties when serialized to match examples from + the STIX specification + 2.0.2 - 2020-07-07 * #423 Fixes issue with six dependency. diff --git a/README.rst b/README.rst index 999a1a7..6055cf9 100644 --- a/README.rst +++ b/README.rst @@ -163,8 +163,8 @@ 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 +.. |Build_Status| image:: https://github.com/oasis-open/cti-python-stix2/workflows/cti-python-stix2%20test%20harness/badge.svg + :target: https://github.com/oasis-open/cti-python-stix2/actions?query=workflow%3A%22cti-python-stix2+test+harness%22 :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 diff --git a/docs/guide/filesystem.ipynb b/docs/guide/filesystem.ipynb index 845ded4..ec09422 100644 --- a/docs/guide/filesystem.ipynb +++ b/docs/guide/filesystem.ipynb @@ -65,43 +65,53 @@ "\n", "```\n", "stix2_content/\n", - " /STIX2 Domain Object type\n", - " STIX2 Domain Object\n", - " STIX2 Domain Object\n", + " STIX2 Domain Object type/\n", + " STIX2 Domain Object ID/\n", + " 'modified' timestamp.json\n", + " 'modified' timestamp.json\n", + " STIX2 Domain Object ID/\n", + " 'modified' timestamp.json\n", " .\n", " .\n", - " .\n", - " /STIX2 Domain Object type\n", - " STIX2 Domain Object\n", - " STIX2 Domain Object\n", + " STIX2 Domain Object type/\n", + " STIX2 Domain Object ID/\n", + " 'modified' timestamp.json\n", " .\n", " .\n", " .\n", " .\n", " .\n", " .\n", - " /STIX2 Domain Object type\n", + " STIX2 Domain Object type/\n", "```\n", "\n", - "The master STIX 2 content directory contains subdirectories, each of which aligns to a STIX 2 domain object type (i.e. \"attack-pattern\", \"campaign\", \"malware\", etc.). Within each STIX 2 domain object subdirectory are JSON files that are STIX 2 domain objects of the specified type. The name of the json files correspond to the ID of the STIX 2 domain object found within that file. A real example of the FileSystem directory structure:\n", + "The master STIX 2 content directory contains subdirectories, each of which aligns to a STIX 2 domain object type (i.e. \"attack-pattern\", \"campaign\", \"malware\", etc.). Within each STIX 2 domain object type's subdirectory are further subdirectories containing JSON files that are STIX 2 domain objects of the specified type; the name of each of these subdirectories is the ID of the associated STIX 2 domain object. Inside each of these subdirectories are JSON files, the names of which correspond to the ``modified`` timestamp of the STIX 2 domain object found within that file. A real example of the FileSystem directory structure:\n", "\n", "```\n", "stix2_content/\n", " /attack-pattern\n", - " attack-pattern--00d0b012-8a03-410e-95de-5826bf542de6.json\n", - " attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22.json\n", - " attack-pattern--1b7ba276-eedc-4951-a762-0ceea2c030ec.json\n", + " /attack-pattern--00d0b012-8a03-410e-95de-5826bf542de6\n", + " 20201211035036648071.json\n", + " /attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22\n", + " 20201210035036648071.json\n", + " /attack-pattern--1b7ba276-eedc-4951-a762-0ceea2c030ec\n", + " 20201111035036648071.json\n", " /campaign\n", " /course-of-action\n", - " course-of-action--2a8de25c-f743-4348-b101-3ee33ab5871b.json\n", - " course-of-action--2c3ce852-06a2-40ee-8fe6-086f6402a739.json\n", + " /course-of-action--2a8de25c-f743-4348-b101-3ee33ab5871b\n", + " 20201011035036648071.json\n", + " /course-of-action--2c3ce852-06a2-40ee-8fe6-086f6402a739\n", + " 20201010035036648071.json\n", " /identity\n", - " identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5.json\n", + " /identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5\n", + " 20201215035036648071.json\n", " /indicator\n", " /intrusion-set\n", " /malware\n", - " malware--1d808f62-cf63-4063-9727-ff6132514c22.json\n", - " malware--2eb9b131-d333-4a48-9eb4-d8dec46c19ee.json\n", + " /malware--1d808f62-cf63-4063-9727-ff6132514c22\n", + " 20201211045036648071.json\n", + " /malware--2eb9b131-d333-4a48-9eb4-d8dec46c19ee\n", + " 20201211035036648072.json\n", " /observed-data\n", " /report\n", " /threat-actor\n", @@ -1408,7 +1418,7 @@ "# add Campaign object to FileSystemSink\n", "fs_sink.add(camp)\n", "\n", - "# can also add STIX objects to FileSystemSink in on call\n", + "# can also add STIX objects to FileSystemSink in one call\n", "fs_sink.add([ind, ind1])" ] } @@ -1429,7 +1439,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.0a6" + "version": "3.6.7" } }, "nbformat": 4, diff --git a/setup.cfg b/setup.cfg index 12d29ce..0114556 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.0.2 +current_version = 2.1.0 commit = True tag = True diff --git a/setup.py b/setup.py index 2fc5d70..4bfc191 100644 --- a/setup.py +++ b/setup.py @@ -40,10 +40,10 @@ setup( 'Topic :: Security', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], keywords='stix stix2 json cti cyber threat intelligence', packages=find_packages(exclude=['*.test', '*.test.*']), diff --git a/stix2/__init__.py b/stix2/__init__.py index 72fb29b..31821cf 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -24,8 +24,6 @@ # flake8: noqa -DEFAULT_VERSION = '2.1' # Default version will always be the latest STIX 2.X version - from .confidence import scales from .datastore import CompositeDataSource from .datastore.filesystem import ( @@ -41,7 +39,7 @@ from .markings import ( add_markings, clear_markings, get_markings, is_marked, remove_markings, set_markings, ) -from .parsing import _collect_stix2_mappings, parse, parse_observable +from .parsing import parse, parse_observable from .patterns import ( AndBooleanExpression, AndObservationExpression, BasicObjectPathComponent, BinaryConstant, BooleanConstant, EqualityComparisonExpression, @@ -57,8 +55,9 @@ from .patterns import ( RepeatQualifier, StartStopQualifier, StringConstant, TimestampConstant, WithinQualifier, ) +from .registry import _collect_stix2_mappings from .v21 import * # This import will always be the latest STIX 2.X version -from .version import __version__ +from .version import DEFAULT_VERSION, __version__ from .versioning import new_version, revoke _collect_stix2_mappings() diff --git a/stix2/base.py b/stix2/base.py index 3f2a0d3..3d05a7a 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -165,8 +165,10 @@ class _STIXBase(Mapping): defaulted = [] for name, prop in self._properties.items(): try: - if (not prop.required and not hasattr(prop, '_fixed_value') and - prop.default() == setting_kwargs[name]): + if ( + not prop.required and not hasattr(prop, '_fixed_value') and + prop.default() == setting_kwargs[name] + ): defaulted.append(name) except (AttributeError, KeyError): continue @@ -195,8 +197,10 @@ class _STIXBase(Mapping): 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'" % - (self.__class__.__name__, name)) + raise AttributeError( + "'%s' object has no attribute '%s'" % + (self.__class__.__name__, name), + ) def __setattr__(self, name, value): if not name.startswith("_"): diff --git a/stix2/custom.py b/stix2/custom.py index 08574ef..b012f37 100644 --- a/stix2/custom.py +++ b/stix2/custom.py @@ -3,7 +3,7 @@ from collections import OrderedDict import six from .base import _cls_init -from .parsing import ( +from .registration import ( _register_marking, _register_object, _register_observable, _register_observable_extension, ) diff --git a/stix2/datastore/memory.py b/stix2/datastore/memory.py index f71b763..10fb76d 100644 --- a/stix2/datastore/memory.py +++ b/stix2/datastore/memory.py @@ -75,8 +75,10 @@ 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): @@ -188,11 +190,13 @@ class MemorySink(DataSink): def save_to_file(self, path, encoding="utf-8"): path = os.path.abspath(path) - all_objs = list(itertools.chain.from_iterable( - value.all_versions.values() if isinstance(value, _ObjectFamily) - else [value] - for value in self._data.values() - )) + all_objs = list( + itertools.chain.from_iterable( + value.all_versions.values() if isinstance(value, _ObjectFamily) + else [value] + for value in self._data.values() + ), + ) if any("spec_version" in x for x in all_objs): bundle = v21.Bundle(all_objs, allow_custom=self.allow_custom) diff --git a/stix2/equivalence/object/__init__.py b/stix2/equivalence/object/__init__.py index 8333ceb..0225788 100644 --- a/stix2/equivalence/object/__init__.py +++ b/stix2/equivalence/object/__init__.py @@ -4,6 +4,7 @@ import time from ...datastore import Filter from ...utils import STIXdatetime, parse_into_datetime +from ..pattern import equivalent_patterns logger = logging.getLogger(__name__) @@ -68,7 +69,7 @@ def semantically_equivalent(obj1, obj2, prop_scores={}, **weight_dict): sum_weights = 0.0 for prop in weights[type1]: - if check_property_present(prop, obj1, obj2) or prop == "longitude_latitude": + if check_property_present(prop, obj1, obj2): w = weights[type1][prop][0] comp_funct = weights[type1][prop][1] @@ -117,7 +118,10 @@ def semantically_equivalent(obj1, obj2, prop_scores={}, **weight_dict): def check_property_present(prop, obj1, obj2): """Helper method checks if a property is present on both objects.""" - if prop in obj1 and prop in obj2: + if prop == "longitude_latitude": + if all(x in obj1 and x in obj2 for x in ['latitude', 'longitude']): + return True + elif prop in obj1 and prop in obj2: return True return False @@ -208,8 +212,7 @@ def custom_pattern_based(pattern1, pattern2): float: Number between 0.0 and 1.0 depending on match criteria. """ - logger.warning("Indicator pattern equivalence is not fully defined; will default to zero if not completely identical") - return exact_match(pattern1, pattern2) # TODO: Implement pattern based equivalence + return equivalent_patterns(pattern1, pattern2) def partial_external_reference_based(refs1, refs2): diff --git a/stix2/equivalence/pattern/__init__.py b/stix2/equivalence/pattern/__init__.py index b2e5421..2b36f5d 100644 --- a/stix2/equivalence/pattern/__init__.py +++ b/stix2/equivalence/pattern/__init__.py @@ -9,18 +9,14 @@ | """ -import stix2 -from stix2.equivalence.pattern.compare.observation import ( - observation_expression_cmp, -) -from stix2.equivalence.pattern.transform import ( - ChainTransformer, SettleTransformer, -) -from stix2.equivalence.pattern.transform.observation import ( +from ... import pattern_visitor +from ...version import DEFAULT_VERSION +from .compare.observation import observation_expression_cmp +from .transform import ChainTransformer, SettleTransformer +from .transform.observation import ( AbsorptionTransformer, CanonicalizeComparisonExpressionsTransformer, DNFTransformer, FlattenTransformer, OrderDedupeTransformer, ) -import stix2.pattern_visitor # Lazy-initialize _pattern_canonicalizer = None @@ -30,7 +26,8 @@ def _get_pattern_canonicalizer(): """ Get a canonicalization transformer for STIX patterns. - :return: The transformer + Returns: + The transformer """ # The transformers are either stateless or contain no state which changes @@ -60,20 +57,23 @@ def _get_pattern_canonicalizer(): return _pattern_canonicalizer -def equivalent_patterns(pattern1, pattern2, stix_version=stix2.DEFAULT_VERSION): +def equivalent_patterns(pattern1, pattern2, stix_version=DEFAULT_VERSION): """ Determine whether two STIX patterns are semantically equivalent. - :param pattern1: The first STIX pattern - :param pattern2: The second STIX pattern - :param stix_version: The STIX version to use for pattern parsing, as a - string ("2.0", "2.1", etc). Defaults to library-wide default version. - :return: True if the patterns are semantically equivalent; False if not + Args: + pattern1: The first STIX pattern + pattern2: The second STIX pattern + stix_version: The STIX version to use for pattern parsing, as a string + ("2.0", "2.1", etc). Defaults to library-wide default version. + + Returns: + True if the patterns are semantically equivalent; False if not """ - patt_ast1 = stix2.pattern_visitor.create_pattern_object( + patt_ast1 = pattern_visitor.create_pattern_object( pattern1, version=stix_version, ) - patt_ast2 = stix2.pattern_visitor.create_pattern_object( + patt_ast2 = pattern_visitor.create_pattern_object( pattern2, version=stix_version, ) @@ -87,7 +87,7 @@ def equivalent_patterns(pattern1, pattern2, stix_version=stix2.DEFAULT_VERSION): def find_equivalent_patterns( - search_pattern, patterns, stix_version=stix2.DEFAULT_VERSION, + search_pattern, patterns, stix_version=DEFAULT_VERSION, ): """ Find patterns from a sequence which are equivalent to a given pattern. @@ -96,14 +96,16 @@ def find_equivalent_patterns( on an input iterable and is implemented as a generator of matches. So you can "stream" patterns in and matching patterns will be streamed out. - :param search_pattern: A search pattern as a string - :param patterns: An iterable over patterns as strings - :param stix_version: The STIX version to use for pattern parsing, as a - string ("2.0", "2.1", etc). Defaults to library-wide default version. - :return: A generator iterator producing the semantically equivalent - patterns + Args: + search_pattern: A search pattern as a string + patterns: An iterable over patterns as strings + stix_version: The STIX version to use for pattern parsing, as a string + ("2.0", "2.1", etc). Defaults to library-wide default version. + + Returns: + A generator iterator producing the semantically equivalent patterns """ - search_pattern_ast = stix2.pattern_visitor.create_pattern_object( + search_pattern_ast = pattern_visitor.create_pattern_object( search_pattern, version=stix_version, ) @@ -113,7 +115,7 @@ def find_equivalent_patterns( ) for pattern in patterns: - pattern_ast = stix2.pattern_visitor.create_pattern_object( + pattern_ast = pattern_visitor.create_pattern_object( pattern, version=stix_version, ) canon_pattern_ast, _ = pattern_canonicalizer.transform(pattern_ast) diff --git a/stix2/equivalence/pattern/compare/__init__.py b/stix2/equivalence/pattern/compare/__init__.py index e9d7ec9..8ee3562 100644 --- a/stix2/equivalence/pattern/compare/__init__.py +++ b/stix2/equivalence/pattern/compare/__init__.py @@ -16,9 +16,12 @@ def generic_cmp(value1, value2): Generic comparator of values which uses the builtin '<' and '>' operators. Assumes the values can be compared that way. - :param value1: The first value - :param value2: The second value - :return: -1, 0, or 1 depending on whether value1 is less, equal, or greater + Args: + value1: The first value + value2: The second value + + Returns: + -1, 0, or 1 depending on whether value1 is less, equal, or greater than value2 """ @@ -30,12 +33,15 @@ def iter_lex_cmp(seq1, seq2, cmp): Generic lexicographical compare function, which works on two iterables and a comparator function. - :param seq1: The first iterable - :param seq2: The second iterable - :param cmp: a two-arg callable comparator for values iterated over. It - must behave analogously to this function, returning <0, 0, or >0 to - express the ordering of the two values. - :return: <0 if seq1 < seq2; >0 if seq1 > seq2; 0 if they're equal + Args: + seq1: The first iterable + seq2: The second iterable + cmp: a two-arg callable comparator for values iterated over. It + must behave analogously to this function, returning <0, 0, or >0 to + express the ordering of the two values. + + Returns: + <0 if seq1 < seq2; >0 if seq1 > seq2; 0 if they're equal """ it1 = iter(seq1) @@ -84,11 +90,14 @@ def iter_in(value, seq, cmp): a comparator function. This function checks whether the given value is contained in the given iterable. - :param value: A value - :param seq: An iterable - :param cmp: A 2-arg comparator function which must return 0 if the args - are equal - :return: True if the value is found in the iterable, False if it is not + Args: + value: A value + seq: An iterable + cmp: A 2-arg comparator function which must return 0 if the args + are equal + + Returns: + True if the value is found in the iterable, False if it is not """ result = False for seq_val in seq: diff --git a/stix2/equivalence/pattern/compare/comparison.py b/stix2/equivalence/pattern/compare/comparison.py index e412705..07df36a 100644 --- a/stix2/equivalence/pattern/compare/comparison.py +++ b/stix2/equivalence/pattern/compare/comparison.py @@ -32,9 +32,12 @@ def generic_constant_cmp(const1, const2): Generic comparator for most _Constant instances. They must have a "value" attribute whose value supports the builtin comparison operators. - :param const1: The first _Constant instance - :param const2: The second _Constant instance - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + const1: The first _Constant instance + const2: The second _Constant instance + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ return generic_cmp(const1.value, const2.value) @@ -44,9 +47,12 @@ def bool_cmp(value1, value2): """ Compare two boolean constants. - :param value1: The first BooleanConstant instance - :param value2: The second BooleanConstant instance - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + value1: The first BooleanConstant instance + value2: The second BooleanConstant instance + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ @@ -72,9 +78,12 @@ def hex_cmp(value1, value2): Compare two STIX "hex" values. This decodes to bytes and compares that. It does *not* do a string compare on the hex representations. - :param value1: The first HexConstant - :param value2: The second HexConstant - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + value1: The first HexConstant + value2: The second HexConstant + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ bytes1 = bytes.fromhex(value1.value) @@ -88,9 +97,12 @@ def bin_cmp(value1, value2): Compare two STIX "binary" values. This decodes to bytes and compares that. It does *not* do a string compare on the base64 representations. - :param value1: The first BinaryConstant - :param value2: The second BinaryConstant - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + value1: The first BinaryConstant + value2: The second BinaryConstant + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ bytes1 = base64.standard_b64decode(value1.value) @@ -103,9 +115,12 @@ def list_cmp(value1, value2): """ Compare lists order-insensitively. - :param value1: The first ListConstant - :param value2: The second ListConstant - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + value1: The first ListConstant + value2: The second ListConstant + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ @@ -144,9 +159,12 @@ def object_path_component_cmp(comp1, comp2): Ints and strings compare as usual to each other; ints compare less than strings. - :param comp1: An object path component (string or int) - :param comp2: An object path component (string or int) - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + comp1: An object path component (string or int) + comp2: An object path component (string or int) + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ @@ -172,8 +190,11 @@ def object_path_to_raw_values(path): properties; "*" index steps become that string; and numeric index steps become integers. - :param path: An ObjectPath instance - :return: A generator iterator over the values + Args: + path: An ObjectPath instance + + Returns: + A generator iterator over the values """ for comp in path.property_path: @@ -195,9 +216,12 @@ def object_path_cmp(path1, path2): """ Compare two object paths. - :param path1: The first ObjectPath instance - :param path2: The second ObjectPath instance - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + path1: The first ObjectPath instance + path2: The second ObjectPath instance + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ if path1.object_type_name < path2.object_type_name: @@ -224,9 +248,12 @@ def comparison_operator_cmp(op1, op2): """ Compare two comparison operators. - :param op1: The first comparison operator (a string) - :param op2: The second comparison operator (a string) - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + op1: The first comparison operator (a string) + op2: The second comparison operator (a string) + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ op1_idx = _COMPARISON_OP_ORDER.index(op1) @@ -241,9 +268,12 @@ def constant_cmp(value1, value2): """ Compare two constants. - :param value1: The first _Constant instance - :param value2: The second _Constant instance - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + value1: The first _Constant instance + value2: The second _Constant instance + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ @@ -284,9 +314,12 @@ def simple_comparison_expression_cmp(expr1, expr2): Compare "simple" comparison expressions: those which aren't AND/OR combinations, just comparisons. - :param expr1: first _ComparisonExpression instance - :param expr2: second _ComparisonExpression instance - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + expr1: first _ComparisonExpression instance + expr2: second _ComparisonExpression instance + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ @@ -315,9 +348,12 @@ def comparison_expression_cmp(expr1, expr2): expressions' sub-components. To achieve an order-insensitive comparison, the ASTs must be canonically ordered first. - :param expr1: The first comparison expression - :param expr2: The second comparison expression - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + expr1: The first comparison expression + expr2: The second comparison expression + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ if isinstance(expr1, _ComparisonExpression) \ diff --git a/stix2/equivalence/pattern/compare/observation.py b/stix2/equivalence/pattern/compare/observation.py index 8df9e3f..eff03c0 100644 --- a/stix2/equivalence/pattern/compare/observation.py +++ b/stix2/equivalence/pattern/compare/observation.py @@ -64,9 +64,12 @@ def observation_expression_cmp(expr1, expr2): the expressions' sub-components. To achieve an order-insensitive comparison, the ASTs must be canonically ordered first. - :param expr1: The first observation expression - :param expr2: The second observation expression - :return: <0, 0, or >0 depending on whether the first arg is less, equal or + Args: + expr1: The first observation expression + expr2: The second observation expression + + Returns: + <0, 0, or >0 depending on whether the first arg is less, equal or greater than the second """ type1 = type(expr1) diff --git a/stix2/equivalence/pattern/transform/comparison.py b/stix2/equivalence/pattern/transform/comparison.py index d0f431b..248766d 100644 --- a/stix2/equivalence/pattern/transform/comparison.py +++ b/stix2/equivalence/pattern/transform/comparison.py @@ -22,13 +22,17 @@ def _dupe_ast(ast): """ Create a duplicate of the given AST. - Note: the comparison expression "leaves", i.e. simple - comparisons are currently not duplicated. I don't think it's necessary as - of this writing; they are never changed. But revisit this if/when - necessary. + Note: + The comparison expression "leaves", i.e. simple + comparisons are currently not duplicated. I don't think it's necessary + as of this writing; they are never changed. But revisit this if/when + necessary. - :param ast: The AST to duplicate - :return: The duplicate AST + Args: + ast: The AST to duplicate + + Returns: + The duplicate AST """ if isinstance(ast, AndBooleanExpression): result = AndBooleanExpression([ @@ -108,8 +112,11 @@ class ComparisonExpressionTransformer(Transformer): Invoke a transformer callback method based on the given ast root node type. - :param ast: The AST - :return: The callback's result + Args: + ast: The AST + + Returns: + The callback's result """ if isinstance(ast, AndBooleanExpression): @@ -137,7 +144,7 @@ class ComparisonExpressionTransformer(Transformer): class OrderDedupeTransformer( - ComparisonExpressionTransformer + ComparisonExpressionTransformer, ): """ Canonically order the children of all nodes in the AST. Because the @@ -153,8 +160,11 @@ class OrderDedupeTransformer( """ Sort/dedupe children. AND and OR can be treated identically. - :param ast: The comparison expression AST - :return: The same AST node, but with sorted children + Args: + ast: The comparison expression AST + + Returns: + The same AST node, but with sorted children """ sorted_children = sorted( ast.operands, key=functools.cmp_to_key(comparison_expression_cmp), @@ -201,8 +211,11 @@ class FlattenTransformer(ComparisonExpressionTransformer): little difference is that we can absorb AND children if we're an AND ourselves; and OR for OR. - :param ast: The comparison expression AST - :return: The same AST node, but with flattened children + Args: + ast: The comparison expression AST + + Returns: + The same AST node, but with flattened children """ changed = False @@ -234,7 +247,7 @@ class FlattenTransformer(ComparisonExpressionTransformer): class AbsorptionTransformer( - ComparisonExpressionTransformer + ComparisonExpressionTransformer, ): """ Applies boolean "absorption" rules for AST simplification. E.g.: diff --git a/stix2/equivalence/pattern/transform/observation.py b/stix2/equivalence/pattern/transform/observation.py index a8982cf..ee698bd 100644 --- a/stix2/equivalence/pattern/transform/observation.py +++ b/stix2/equivalence/pattern/transform/observation.py @@ -38,8 +38,11 @@ def _dupe_ast(ast): observation expressions are currently not duplicated. I don't think it's necessary as of this writing. But revisit this if/when necessary. - :param ast: The AST to duplicate - :return: The duplicate AST + Args: + ast: The AST to duplicate + + Returns: + The duplicate AST """ if isinstance(ast, AndObservationExpression): result = AndObservationExpression([ @@ -149,9 +152,11 @@ class ObservationExpressionTransformer(Transformer): changed = True else: - raise TypeError("Not an observation expression: {}: {}".format( - type(ast).__name__, str(ast), - )) + raise TypeError( + "Not an observation expression: {}: {}".format( + type(ast).__name__, str(ast), + ), + ) return result, changed @@ -160,8 +165,11 @@ class ObservationExpressionTransformer(Transformer): Invoke a transformer callback method based on the given ast root node type. - :param ast: The AST - :return: The callback's result + Args: + ast: The AST + + Returns: + The callback's result """ dispatch_name = self._DISPATCH_NAME_MAP.get(type(ast)) @@ -223,7 +231,7 @@ class FlattenTransformer(ObservationExpressionTransformer): class OrderDedupeTransformer( - ObservationExpressionTransformer + ObservationExpressionTransformer, ): """ Canonically order AND/OR expressions, and dedupe ORs. E.g.: @@ -266,7 +274,7 @@ class OrderDedupeTransformer( class AbsorptionTransformer( - ObservationExpressionTransformer + ObservationExpressionTransformer, ): """ Applies boolean "absorption" rules for observation expressions, for AST @@ -292,10 +300,12 @@ class AbsorptionTransformer( the right does not "contain" the left. You would need two A's on the right. - :param exprs_containee: The expressions we want to check for containment - :param exprs_container: The expressions acting as the "container" - :return: True if the containee is contained in the container; False if - not + Args: + exprs_containee: The expressions we want to check for containment + exprs_container: The expressions acting as the "container" + + Returns: + True if the containee is contained in the container; False if not """ # make our own list we are free to manipulate without affecting the @@ -336,10 +346,12 @@ class AbsorptionTransformer( in the container (rhs), B follows A, so it "contains" the lhs even though there is other stuff mixed in. - :param exprs_containee: The expressions we want to check for containment - :param exprs_container: The expressions acting as the "container" - :return: True if the containee is contained in the container; False if - not + Args: + exprs_containee: The expressions we want to check for containment + exprs_container: The expressions acting as the "container" + + Returns: + True if the containee is contained in the container; False if not """ ee_iter = iter(exprs_containee) @@ -469,7 +481,7 @@ class DNFTransformer(ObservationExpressionTransformer): class CanonicalizeComparisonExpressionsTransformer( - ObservationExpressionTransformer + ObservationExpressionTransformer, ): """ Canonicalize all comparison expressions. diff --git a/stix2/equivalence/pattern/transform/specials.py b/stix2/equivalence/pattern/transform/specials.py index d3611f3..e0b82f5 100644 --- a/stix2/equivalence/pattern/transform/specials.py +++ b/stix2/equivalence/pattern/transform/specials.py @@ -25,9 +25,12 @@ def _path_is(object_path, path_pattern): index path step; _ANY_KEY matches any key path step, and _ANY matches any path step. - :param object_path: An ObjectPath instance - :param path_pattern: An iterable giving the pattern path steps - :return: True if the path matches the pattern; False if not + Args: + object_path: An ObjectPath instance + path_pattern: An iterable giving the pattern path steps + + Returns: + True if the path matches the pattern; False if not """ path_values = object_path_to_raw_values(object_path) @@ -70,8 +73,9 @@ def _mask_bytes(ip_bytes, prefix_size): Retain the high-order 'prefix_size' bits from ip_bytes, and zero out the remaining low-order bits. This side-effects ip_bytes. - :param ip_bytes: A mutable byte sequence (e.g. a bytearray) - :param prefix_size: An integer prefix size + Args: + ip_bytes: A mutable byte sequence (e.g. a bytearray) + prefix_size: An integer prefix size """ addr_size_bytes = len(ip_bytes) addr_size_bits = 8 * addr_size_bytes @@ -99,8 +103,9 @@ def windows_reg_key(comp_expr): being compared. This enables case-insensitive comparisons between two patterns, for those values. This side-effects the given AST. - :param comp_expr: A _ComparisonExpression object whose type is - windows-registry-key + Args: + comp_expr: A _ComparisonExpression object whose type is + windows-registry-key """ if _path_is(comp_expr.lhs, ("key",)) \ or _path_is(comp_expr.lhs, ("values", _ANY_IDX, "name")): @@ -119,7 +124,8 @@ def ipv4_addr(comp_expr): This side-effects the given AST. - :param comp_expr: A _ComparisonExpression object whose type is ipv4-addr. + Args: + comp_expr: A _ComparisonExpression object whose type is ipv4-addr. """ if _path_is(comp_expr.lhs, ("value",)): value = comp_expr.rhs.value @@ -179,7 +185,8 @@ def ipv6_addr(comp_expr): This side-effects the given AST. - :param comp_expr: A _ComparisonExpression object whose type is ipv6-addr. + Args: + comp_expr: A _ComparisonExpression object whose type is ipv6-addr. """ if _path_is(comp_expr.lhs, ("value",)): value = comp_expr.rhs.value diff --git a/stix2/parsing.py b/stix2/parsing.py index 7021c1a..9e90830 100644 --- a/stix2/parsing.py +++ b/stix2/parsing.py @@ -1,17 +1,10 @@ """STIX2 Core parsing methods.""" import copy -import importlib -import pkgutil -import re -import stix2 - -from .base import _DomainObject, _Observable -from .exceptions import DuplicateRegistrationError, ParseError -from .utils import PREFIX_21_REGEX, _get_dict, get_class_hierarchy_names - -STIX2_OBJ_MAPS = {} +from . import registry +from .exceptions import ParseError +from .utils import _get_dict, detect_spec_version def parse(data, allow_custom=False, interoperability=False, version=None): @@ -122,22 +115,19 @@ def dict_to_stix2(stix_dict, allow_custom=False, interoperability=False, version if 'type' not in 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('.', '') - else: - v = _detect_spec_version(stix_dict) + if not version: + version = detect_spec_version(stix_dict) - OBJ_MAP = dict(STIX2_OBJ_MAPS[v]['objects'], **STIX2_OBJ_MAPS[v]['observables']) + obj_type = stix_dict["type"] + obj_class = registry.class_for_type(obj_type, version, "objects") \ + or registry.class_for_type(obj_type, version, "observables") - try: - obj_class = OBJ_MAP[stix_dict['type']] - except KeyError: + if not obj_class: if allow_custom: # flag allows for unknown custom objects too, but will not # be parsed into STIX object, returned as is return stix_dict - raise 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." % obj_type) return obj_class(allow_custom=allow_custom, interoperability=interoperability, **stix_dict) @@ -172,236 +162,19 @@ def parse_observable(data, _valid_refs=None, allow_custom=False, version=None): obj['_valid_refs'] = _valid_refs or [] - if version: - # If the version argument was passed, override other approaches. - v = 'v' + version.replace('.', '') - else: - v = _detect_spec_version(obj) + if not version: + version = detect_spec_version(obj) - try: - OBJ_MAP_OBSERVABLE = STIX2_OBJ_MAPS[v]['observables'] - obj_class = OBJ_MAP_OBSERVABLE[obj['type']] - except KeyError: + obj_type = obj["type"] + obj_class = registry.class_for_type(obj_type, version, "observables") + if not obj_class: 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 ParseError("Can't parse unknown observable type '%s'! For custom observables, " - "use the CustomObservable decorator." % obj['type']) + raise ParseError( + "Can't parse unknown observable type '%s'! For custom observables, " + "use the CustomObservable decorator." % obj['type'], + ) return obj_class(allow_custom=allow_custom, **obj) - - -def _register_object(new_type, version=stix2.DEFAULT_VERSION): - """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. - - Raises: - ValueError: If the class being registered wasn't created with the - @CustomObject decorator. - DuplicateRegistrationError: If the class has already been registered. - - """ - - if not issubclass(new_type, _DomainObject): - raise ValueError( - "'%s' must be created with the @CustomObject decorator." % - new_type.__name__, - ) - - properties = new_type._properties - - if version == "2.1": - for prop_name, prop in properties.items(): - if not re.match(PREFIX_21_REGEX, prop_name): - raise ValueError("Property name '%s' must begin with an alpha character" % prop_name) - - 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'] - if new_type._type in OBJ_MAP.keys(): - raise DuplicateRegistrationError("STIX Object", new_type._type) - OBJ_MAP[new_type._type] = new_type - - -def _register_marking(new_marking, version=stix2.DEFAULT_VERSION): - """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. - - """ - - mark_type = new_marking._type - properties = new_marking._properties - - stix2.properties._validate_type(mark_type, version) - - if version == "2.1": - for prop_name, prop_value in properties.items(): - if not re.match(PREFIX_21_REGEX, prop_name): - raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) - - 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'] - if mark_type in OBJ_MAP_MARKING.keys(): - raise DuplicateRegistrationError("STIX Marking", mark_type) - OBJ_MAP_MARKING[mark_type] = new_marking - - -def _register_observable(new_observable, version=stix2.DEFAULT_VERSION): - """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. - - """ - properties = new_observable._properties - - if version == "2.0": - # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties - for prop_name, prop in properties.items(): - 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, - ) - else: - # If using STIX2.1 (or newer...), check properties ending in "_ref/s" are ReferenceProperties - for prop_name, prop in properties.items(): - if not re.match(PREFIX_21_REGEX, prop_name): - raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) - elif prop_name.endswith('_ref') and ('ReferenceProperty' not in get_class_hierarchy_names(prop)): - raise ValueError( - "'%s' is named like a reference property but " - "is not a ReferenceProperty." % prop_name, - ) - elif (prop_name.endswith('_refs') and ('ListProperty' not in get_class_hierarchy_names(prop) or - 'ReferenceProperty' not in get_class_hierarchy_names(prop.contained))): - raise ValueError( - "'%s' is named like a reference list property but " - "is not a ListProperty containing ReferenceProperty." % prop_name, - ) - - 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'] - if new_observable._type in OBJ_MAP_OBSERVABLE.keys(): - raise DuplicateRegistrationError("Cyber Observable", new_observable._type) - OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable - - -def _register_observable_extension( - observable, new_extension, version=stix2.DEFAULT_VERSION, -): - """Register a custom extension to a STIX Cyber Observable type. - - Args: - observable: An observable class or instance - 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"). - Defaults to the latest supported version. - - """ - obs_class = observable if isinstance(observable, type) else \ - type(observable) - ext_type = new_extension._type - properties = new_extension._properties - - if not issubclass(obs_class, _Observable): - raise ValueError("'observable' must be a valid Observable class!") - - stix2.properties._validate_type(ext_type, version) - - if not new_extension._properties: - raise ValueError( - "Invalid extension: must define at least one property: " + - ext_type, - ) - - if version == "2.1": - if not ext_type.endswith('-ext'): - raise ValueError( - "Invalid extension type name '%s': must end with '-ext'." % - ext_type, - ) - - for prop_name, prop_value in properties.items(): - if not re.match(PREFIX_21_REGEX, prop_name): - raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) - - v = 'v' + 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: - if ext_type in EXT_MAP[observable_type].keys(): - raise DuplicateRegistrationError("Observable Extension", ext_type) - EXT_MAP[observable_type][ext_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] = {ext_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): - 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[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/pattern_visitor.py b/stix2/pattern_visitor.py index c4b2ec2..93eb083 100644 --- a/stix2/pattern_visitor.py +++ b/stix2/pattern_visitor.py @@ -17,10 +17,9 @@ from stix2patterns.v21.grammars.STIXPatternVisitor import \ STIXPatternVisitor as STIXPatternVisitor21 from stix2patterns.v21.pattern import Pattern as Pattern21 -import stix2 - from .patterns import * from .patterns import _BooleanExpression +from .version import DEFAULT_VERSION # flake8: noqa F405 @@ -261,11 +260,13 @@ class STIXPatternVisitorForSTIX2(): property_path.append(self.instantiate("ListObjectPathComponent", current.property_name, next.getText())) i += 2 elif isinstance(next, IntegerConstant): - property_path.append(self.instantiate( - "ListObjectPathComponent", - current.property_name if isinstance(current, BasicObjectPathComponent) else text_type(current), - next.value, - )) + property_path.append( + self.instantiate( + "ListObjectPathComponent", + current.property_name if isinstance(current, BasicObjectPathComponent) else text_type(current), + next.value, + ), + ) i += 2 else: property_path.append(current) @@ -389,7 +390,7 @@ class STIXPatternVisitorForSTIX20(STIXPatternVisitorForSTIX2, STIXPatternVisitor super(STIXPatternVisitor20, self).__init__() -def create_pattern_object(pattern, module_suffix="", module_name="", version=stix2.DEFAULT_VERSION): +def create_pattern_object(pattern, module_suffix="", module_name="", version=DEFAULT_VERSION): """ Create a STIX pattern AST from a pattern string. """ diff --git a/stix2/properties.py b/stix2/properties.py index 74565bc..d999ae4 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -9,15 +9,15 @@ import uuid from six import string_types, text_type -import stix2 - from .base import _STIXBase from .exceptions import ( CustomContentError, DictionaryKeyError, MissingPropertiesError, MutuallyExclusivePropertiesError, STIXError, ) -from .parsing import STIX2_OBJ_MAPS, parse, parse_observable +from .parsing import parse, parse_observable +from .registry import STIX2_OBJ_MAPS from .utils import _get_dict, get_class_hierarchy_names, parse_into_datetime +from .version import DEFAULT_VERSION ID_REGEX_interoperability = re.compile(r"[0-9a-fA-F]{8}-" "[0-9a-fA-F]{4}-" @@ -256,9 +256,11 @@ class ListProperty(Property): valid = self.contained(**item) else: - raise ValueError("Can't create a {} out of {}".format( - self.contained._type, str(item), - )) + raise ValueError( + "Can't create a {} out of {}".format( + self.contained._type, str(item), + ), + ) result.append(valid) @@ -282,7 +284,7 @@ class StringProperty(Property): class TypeProperty(Property): - def __init__(self, type, spec_version=stix2.DEFAULT_VERSION): + def __init__(self, type, spec_version=DEFAULT_VERSION): _validate_type(type, spec_version) self.spec_version = spec_version super(TypeProperty, self).__init__(fixed=type) @@ -290,7 +292,7 @@ class TypeProperty(Property): class IDProperty(Property): - def __init__(self, type, spec_version=stix2.DEFAULT_VERSION): + def __init__(self, type, spec_version=DEFAULT_VERSION): self.required_prefix = type + "--" self.spec_version = spec_version super(IDProperty, self).__init__() @@ -390,7 +392,7 @@ class TimestampProperty(Property): class DictionaryProperty(Property): - def __init__(self, spec_version=stix2.DEFAULT_VERSION, **kwargs): + def __init__(self, spec_version=DEFAULT_VERSION, **kwargs): self.spec_version = spec_version super(DictionaryProperty, self).__init__(**kwargs) @@ -479,7 +481,7 @@ class HexProperty(Property): class ReferenceProperty(Property): - def __init__(self, valid_types=None, invalid_types=None, spec_version=stix2.DEFAULT_VERSION, **kwargs): + def __init__(self, valid_types=None, invalid_types=None, spec_version=DEFAULT_VERSION, **kwargs): """ references sometimes must be to a specific object type """ @@ -511,14 +513,14 @@ class ReferenceProperty(Property): possible_prefix = value[:value.index('--')] if self.valid_types: - ref_valid_types = enumerate_types(self.valid_types, 'v' + self.spec_version.replace(".", "")) + ref_valid_types = enumerate_types(self.valid_types, self.spec_version) if possible_prefix in ref_valid_types or self.allow_custom: required_prefix = possible_prefix + '--' else: raise ValueError("The type-specifying prefix '%s' for this property is not valid" % (possible_prefix)) elif self.invalid_types: - ref_invalid_types = enumerate_types(self.invalid_types, 'v' + self.spec_version.replace(".", "")) + ref_invalid_types = enumerate_types(self.invalid_types, self.spec_version) if possible_prefix not in ref_invalid_types: required_prefix = possible_prefix + '--' @@ -613,7 +615,7 @@ class ObservableProperty(Property): """Property for holding Cyber Observable Objects. """ - def __init__(self, spec_version=stix2.DEFAULT_VERSION, allow_custom=False, *args, **kwargs): + def __init__(self, spec_version=DEFAULT_VERSION, allow_custom=False, *args, **kwargs): self.allow_custom = allow_custom self.spec_version = spec_version super(ObservableProperty, self).__init__(*args, **kwargs) @@ -648,7 +650,7 @@ class ExtensionsProperty(DictionaryProperty): """Property for representing extensions on Observable objects. """ - def __init__(self, spec_version=stix2.DEFAULT_VERSION, allow_custom=False, enclosing_type=None, required=False): + def __init__(self, spec_version=DEFAULT_VERSION, 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) @@ -663,9 +665,7 @@ class ExtensionsProperty(DictionaryProperty): except ValueError: raise ValueError("The extensions property must contain a dictionary") - v = 'v' + self.spec_version.replace('.', '') - - specific_type_map = STIX2_OBJ_MAPS[v]['observable-extensions'].get(self.enclosing_type, {}) + specific_type_map = STIX2_OBJ_MAPS[self.spec_version]['observable-extensions'].get(self.enclosing_type, {}) for key, subvalue in dictified.items(): if key in specific_type_map: cls = specific_type_map[key] @@ -690,7 +690,7 @@ class ExtensionsProperty(DictionaryProperty): class STIXObjectProperty(Property): - def __init__(self, spec_version=stix2.DEFAULT_VERSION, allow_custom=False, interoperability=False, *args, **kwargs): + def __init__(self, spec_version=DEFAULT_VERSION, allow_custom=False, interoperability=False, *args, **kwargs): self.allow_custom = allow_custom self.spec_version = spec_version self.interoperability = interoperability @@ -699,8 +699,10 @@ class STIXObjectProperty(Property): 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 ('_DomainObject', '_RelationshipObject', 'MarkingDefinition') - for x in get_class_hierarchy_names(value)): + if any( + x in ('_DomainObject', '_RelationshipObject', '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 diff --git a/stix2/registration.py b/stix2/registration.py new file mode 100644 index 0000000..4ec019a --- /dev/null +++ b/stix2/registration.py @@ -0,0 +1,199 @@ +import re + +from . import registry +from .base import _DomainObject, _Observable +from .exceptions import DuplicateRegistrationError +from .properties import _validate_type +from .utils import PREFIX_21_REGEX, get_class_hierarchy_names +from .version import DEFAULT_VERSION + + +def _register_object(new_type, version=DEFAULT_VERSION): + """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. + + Raises: + ValueError: If the class being registered wasn't created with the + @CustomObject decorator. + DuplicateRegistrationError: If the class has already been registered. + + """ + + if not issubclass(new_type, _DomainObject): + raise ValueError( + "'%s' must be created with the @CustomObject decorator." % + new_type.__name__, + ) + + properties = new_type._properties + + if not version: + version = DEFAULT_VERSION + + if version == "2.1": + for prop_name, prop in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character" % prop_name) + + OBJ_MAP = registry.STIX2_OBJ_MAPS[version]['objects'] + if new_type._type in OBJ_MAP.keys(): + raise DuplicateRegistrationError("STIX Object", new_type._type) + OBJ_MAP[new_type._type] = new_type + + +def _register_marking(new_marking, version=DEFAULT_VERSION): + """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. + + """ + + mark_type = new_marking._type + properties = new_marking._properties + + if not version: + version = DEFAULT_VERSION + + _validate_type(mark_type, version) + + if version == "2.1": + for prop_name, prop_value in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) + + OBJ_MAP_MARKING = registry.STIX2_OBJ_MAPS[version]['markings'] + if mark_type in OBJ_MAP_MARKING.keys(): + raise DuplicateRegistrationError("STIX Marking", mark_type) + OBJ_MAP_MARKING[mark_type] = new_marking + + +def _register_observable(new_observable, version=DEFAULT_VERSION): + """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. + + """ + properties = new_observable._properties + + if not version: + version = DEFAULT_VERSION + + if version == "2.0": + # If using STIX2.0, check properties ending in "_ref/s" are ObjectReferenceProperties + for prop_name, prop in properties.items(): + 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, + ) + else: + # If using STIX2.1 (or newer...), check properties ending in "_ref/s" are ReferenceProperties + for prop_name, prop in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) + elif prop_name.endswith('_ref') and ('ReferenceProperty' not in get_class_hierarchy_names(prop)): + raise ValueError( + "'%s' is named like a reference property but " + "is not a ReferenceProperty." % prop_name, + ) + elif ( + prop_name.endswith('_refs') and ( + 'ListProperty' not in get_class_hierarchy_names(prop) or + 'ReferenceProperty' not in get_class_hierarchy_names(prop.contained) + ) + ): + raise ValueError( + "'%s' is named like a reference list property but " + "is not a ListProperty containing ReferenceProperty." % prop_name, + ) + + OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[version]['observables'] + if new_observable._type in OBJ_MAP_OBSERVABLE.keys(): + raise DuplicateRegistrationError("Cyber Observable", new_observable._type) + OBJ_MAP_OBSERVABLE[new_observable._type] = new_observable + + +def _register_observable_extension( + observable, new_extension, version=DEFAULT_VERSION, +): + """Register a custom extension to a STIX Cyber Observable type. + + Args: + observable: An observable class or instance + 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"). + Defaults to the latest supported version. + + """ + obs_class = observable if isinstance(observable, type) else \ + type(observable) + ext_type = new_extension._type + properties = new_extension._properties + + if not issubclass(obs_class, _Observable): + raise ValueError("'observable' must be a valid Observable class!") + + _validate_type(ext_type, version) + + if not new_extension._properties: + raise ValueError( + "Invalid extension: must define at least one property: " + + ext_type, + ) + + if version == "2.1": + if not ext_type.endswith('-ext'): + raise ValueError( + "Invalid extension type name '%s': must end with '-ext'." % + ext_type, + ) + + for prop_name, prop_value in properties.items(): + if not re.match(PREFIX_21_REGEX, prop_name): + raise ValueError("Property name '%s' must begin with an alpha character." % prop_name) + + try: + observable_type = observable._type + except AttributeError: + raise ValueError( + "Unknown observable type. Custom observables must be " + "created with the @CustomObservable decorator.", + ) + + OBJ_MAP_OBSERVABLE = registry.STIX2_OBJ_MAPS[version]['observables'] + EXT_MAP = registry.STIX2_OBJ_MAPS[version]['observable-extensions'] + + try: + if ext_type in EXT_MAP[observable_type].keys(): + raise DuplicateRegistrationError("Observable Extension", ext_type) + EXT_MAP[observable_type][ext_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] = {ext_type: new_extension} diff --git a/stix2/registry.py b/stix2/registry.py new file mode 100644 index 0000000..3dcc3a5 --- /dev/null +++ b/stix2/registry.py @@ -0,0 +1,80 @@ +import importlib +import pkgutil +import re + +# Collects information on which classes implement which STIX types, for the +# various STIX spec versions. +STIX2_OBJ_MAPS = {} + + +def _stix_vid_to_version(stix_vid): + """ + Convert a python package name representing a STIX version in the form "vXX" + to the dotted style used in the public APIs of this library, "X.X". + + :param stix_vid: A package name in the form "vXX" + :return: A STIX version in dotted style + """ + assert len(stix_vid) >= 3 + + stix_version = stix_vid[1] + "." + stix_vid[2:] + return stix_version + + +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): + stix_vid = name.split('.')[1] + if re.match(r'^stix2\.v2[0-9]$', name) and is_pkg: + ver = _stix_vid_to_version(stix_vid) + mod = importlib.import_module(name, str(top_level_module.__name__)) + 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: + ver = _stix_vid_to_version(stix_vid) + mod = importlib.import_module(name, str(top_level_module.__name__)) + STIX2_OBJ_MAPS[ver]['markings'] = mod.OBJ_MAP_MARKING + + +def class_for_type(stix_type, stix_version, category=None): + """ + Get the registered class which implements a particular STIX type for a + particular STIX version. + + :param stix_type: A STIX type as a string + :param stix_version: A STIX version as a string, e.g. "2.1" + :param category: An optional "category" value, which is just used directly + as a second key after the STIX version, and depends on how the types + are internally categorized. This would be useful if the same STIX type + is used to mean two different things within the same STIX version. So + it's unlikely to be necessary. Pass None to just search all the + categories and return the first class found. + :return: A registered python class which implements the given STIX type, or + None if one is not found. + """ + cls = None + + cat_map = STIX2_OBJ_MAPS.get(stix_version) + if cat_map: + if category: + class_map = cat_map.get(category) + if class_map: + cls = class_map.get(stix_type) + else: + cls = cat_map["objects"].get(stix_type) \ + or cat_map["observables"].get(stix_type) \ + or cat_map["markings"].get(stix_type) + + # Left "observable-extensions" out; it has a different + # substructure. A version->category->type lookup would result + # in another map, not a class. So it doesn't fit the pattern. + + return cls diff --git a/stix2/test/test_spec_version_detect.py b/stix2/test/test_spec_version_detect.py index 7039024..570cc8e 100644 --- a/stix2/test/test_spec_version_detect.py +++ b/stix2/test/test_spec_version_detect.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import pytest -from stix2.parsing import _detect_spec_version +from stix2.utils import detect_spec_version @pytest.mark.parametrize( @@ -17,7 +17,7 @@ from stix2.parsing import _detect_spec_version "name": "alice", "identity_class": "individual", }, - "v20", + "2.0", ), ( { @@ -29,14 +29,14 @@ from stix2.parsing import _detect_spec_version "target_ref": "identity--ba18dde2-56d3-4a34-aa0b-fc56f5be568f", "relationship_type": "targets", }, - "v20", + "2.0", ), ( { "type": "file", "name": "notes.txt", }, - "v20", + "2.0", ), ( { @@ -48,7 +48,7 @@ from stix2.parsing import _detect_spec_version "statement": "Copyright (c) ACME Corp.", }, }, - "v20", + "2.0", ), ( { @@ -75,7 +75,7 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v20", + "2.0", ), # STIX 2.1 examples ( @@ -87,7 +87,7 @@ from stix2.parsing import _detect_spec_version "modified": "2001-07-01T09:33:17.000Z", "name": "alice", }, - "v21", + "2.1", ), ( { @@ -100,7 +100,7 @@ from stix2.parsing import _detect_spec_version "target_ref": "identity--ba18dde2-56d3-4a34-aa0b-fc56f5be568f", "relationship_type": "targets", }, - "v21", + "2.1", ), ( { @@ -109,7 +109,7 @@ from stix2.parsing import _detect_spec_version "spec_version": "2.1", "name": "notes.txt", }, - "v21", + "2.1", ), ( { @@ -117,7 +117,7 @@ from stix2.parsing import _detect_spec_version "id": "file--5eef3404-6a94-4db3-9a1a-5684cbea0dfe", "name": "notes.txt", }, - "v21", + "2.1", ), ( { @@ -131,7 +131,7 @@ from stix2.parsing import _detect_spec_version "tlp": "green", }, }, - "v21", + "2.1", ), ( { @@ -153,7 +153,7 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v21", + "2.1", ), # Mixed spec examples ( @@ -180,7 +180,7 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v21", + "2.1", ), ( { @@ -202,11 +202,11 @@ from stix2.parsing import _detect_spec_version }, ], }, - "v21", + "2.1", ), ], ) def test_spec_version_detect(obj_dict, expected_ver): - detected_ver = _detect_spec_version(obj_dict) + detected_ver = detect_spec_version(obj_dict) assert detected_ver == expected_ver diff --git a/stix2/test/test_utils_type_checks.py b/stix2/test/test_utils_type_checks.py new file mode 100644 index 0000000..2144653 --- /dev/null +++ b/stix2/test/test_utils_type_checks.py @@ -0,0 +1,262 @@ +import pytest + +import stix2.utils + +### +# Tests using types/behaviors common to STIX 2.0 and 2.1. +### + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "attack-pattern", + "campaign", + "course-of-action", + "identity", + "indicator", + "intrusion-set", + "malware", + "observed-data", + "report", + "threat-actor", + "tool", + "vulnerability", + ], +) +def test_is_sdo(type_, stix_version): + assert stix2.utils.is_sdo(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_sdo(id_, stix_version) + + assert stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SDO, + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "relationship", + "sighting", + "marking-definition", + "bundle", + "language-content", + "ipv4-addr", + "foo", + ], +) +def test_is_not_sdo(type_, stix_version): + assert not stix2.utils.is_sdo(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_sdo(id_, stix_version) + + d = { + "type": type_, + } + assert not stix2.utils.is_sdo(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SDO, + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "artifact", + "autonomous-system", + "directory", + "domain-name", + "email-addr", + "email-message", + "file", + "ipv4-addr", + "ipv6-addr", + "mac-addr", + "mutex", + "network-traffic", + "process", + "software", + "url", + "user-account", + "windows-registry-key", + "x509-certificate", + ], +) +def test_is_sco(type_, stix_version): + assert stix2.utils.is_sco(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_sco(id_, stix_version) + + assert stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SCO, + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "sighting", + "marking-definition", + "bundle", + "language-content", + "foo", + ], +) +def test_is_not_sco(type_, stix_version): + assert not stix2.utils.is_sco(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_sco(id_, stix_version) + + d = { + "type": type_, + } + assert not stix2.utils.is_sco(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SCO, + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "relationship", + "sighting", + ], +) +def test_is_sro(type_, stix_version): + assert stix2.utils.is_sro(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_sro(id_, stix_version) + + assert stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SRO, + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "marking-definition", + "bundle", + "language-content", + "ipv4-addr", + "foo", + ], +) +def test_is_not_sro(type_, stix_version): + assert not stix2.utils.is_sro(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_sro(id_, stix_version) + + d = { + "type": type_, + } + assert not stix2.utils.is_sro(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, stix2.utils.STIXTypeClass.SRO, + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +def test_is_marking(stix_version): + assert stix2.utils.is_marking("marking-definition", stix_version) + + id_ = "marking-definition--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_marking(id_, stix_version) + + assert stix2.utils.is_stix_type( + "marking-definition", stix_version, "marking-definition", + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "bundle", + "language-content", + "ipv4-addr", + "foo", + ], +) +def test_is_not_marking(type_, stix_version): + assert not stix2.utils.is_marking(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_marking(id_, stix_version) + + d = { + "type": type_, + } + assert not stix2.utils.is_marking(d, stix_version) + + assert not stix2.utils.is_stix_type( + type_, stix_version, "marking-definition", + ) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +@pytest.mark.parametrize( + "type_", [ + "identity", + "relationship", + "sighting", + "marking-definition", + "bundle", + "ipv4-addr", + ], +) +def test_is_object(type_, stix_version): + assert stix2.utils.is_object(type_, stix_version) + + id_ = type_ + "--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert stix2.utils.is_object(id_, stix_version) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +def test_is_not_object(stix_version): + assert not stix2.utils.is_object("foo", stix_version) + + id_ = "foo--a12fa04c-6586-4128-8d1a-cfe0d1c081f5" + assert not stix2.utils.is_object(id_, stix_version) + + d = { + "type": "foo", + } + assert not stix2.utils.is_object(d, stix_version) + + +@pytest.mark.parametrize("stix_version", ["2.0", "2.1"]) +def test_is_stix_type(stix_version): + + assert not stix2.utils.is_stix_type( + "foo", stix_version, stix2.utils.STIXTypeClass.SDO, "foo", + ) + + assert stix2.utils.is_stix_type( + "bundle", stix_version, "foo", "bundle", + ) + + assert stix2.utils.is_stix_type( + "identity", stix_version, + stix2.utils.STIXTypeClass.SDO, + stix2.utils.STIXTypeClass.SRO, + ) + + assert stix2.utils.is_stix_type( + "software", stix_version, + stix2.utils.STIXTypeClass.SDO, + stix2.utils.STIXTypeClass.SCO, + ) diff --git a/stix2/test/v20/test_custom.py b/stix2/test/v20/test_custom.py index 70835c1..a83bf24 100644 --- a/stix2/test/v20/test_custom.py +++ b/stix2/test/v20/test_custom.py @@ -1,7 +1,9 @@ import pytest import stix2 -from stix2 import parsing +import stix2.parsing +import stix2.registration +import stix2.registry import stix2.v20 from ...exceptions import DuplicateRegistrationError, InvalidValueError @@ -981,7 +983,7 @@ def test_register_custom_object(): _type = 'awesome-object' with pytest.raises(ValueError): - stix2.parsing._register_object(CustomObject2, version="2.0") + stix2.registration._register_object(CustomObject2, version="2.0") def test_extension_property_location(): @@ -1041,10 +1043,9 @@ def test_register_custom_object_with_version(): "id": "x-new-type-2--00000000-0000-4000-8000-000000000007", } - cust_obj_1 = parsing.dict_to_stix2(custom_obj_1, version='2.0') - v = 'v20' + cust_obj_1 = stix2.parsing.dict_to_stix2(custom_obj_1, version='2.0') - assert cust_obj_1.type in parsing.STIX2_OBJ_MAPS[v]['objects'] + assert cust_obj_1.type in stix2.registry.STIX2_OBJ_MAPS['2.0']['objects'] # spec_version is not in STIX 2.0, and is required in 2.1, so this # suffices as a test for a STIX 2.0 object. assert "spec_version" not in cust_obj_1 @@ -1074,9 +1075,8 @@ class NewObservable2(object): def test_register_observable_with_version(): custom_obs = NewObservable2(property1="Test Observable") - v = 'v20' - assert custom_obs.type in parsing.STIX2_OBJ_MAPS[v]['observables'] + assert custom_obs.type in stix2.registry.STIX2_OBJ_MAPS['2.0']['observables'] def test_register_duplicate_observable_with_version(): @@ -1099,10 +1099,9 @@ def test_register_marking_with_version(): ) class NewObj2(): pass - v = 'v20' no = NewObj2(property1='something') - assert no._type in parsing.STIX2_OBJ_MAPS[v]['markings'] + assert no._type in stix2.registry.STIX2_OBJ_MAPS['2.0']['markings'] def test_register_observable_extension_with_version(): @@ -1114,10 +1113,9 @@ def test_register_observable_extension_with_version(): class SomeCustomExtension2: pass - v = 'v20' example = SomeCustomExtension2(keys='test123') - assert example._type in parsing.STIX2_OBJ_MAPS[v]['observable-extensions']['user-account'] + assert example._type in stix2.registry.STIX2_OBJ_MAPS['2.0']['observable-extensions']['user-account'] def test_register_duplicate_observable_extension(): diff --git a/stix2/test/v20/test_datastore_memory.py b/stix2/test/v20/test_datastore_memory.py index 28d8e52..3357e2c 100644 --- a/stix2/test/v20/test_datastore_memory.py +++ b/stix2/test/v20/test_datastore_memory.py @@ -175,12 +175,14 @@ 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 diff --git a/stix2/test/v20/test_environment.py b/stix2/test/v20/test_environment.py index 34ce596..e572aee 100644 --- a/stix2/test/v20/test_environment.py +++ b/stix2/test/v20/test_environment.py @@ -39,15 +39,19 @@ def ds2(): 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, created_by_ref=idy.id, **INDICATOR_KWARGS) - indv2 = ind.new_version(external_references=[{ - "source_name": "unknown", - "url": "https://examplewebsite.com/", - }]) + indv2 = ind.new_version( + external_references=[{ + "source_name": "unknown", + "url": "https://examplewebsite.com/", + }], + ) mal = stix2.v20.Malware(id=MALWARE_ID, created_by_ref=idy.id, **MALWARE_KWARGS) - malv2 = mal.new_version(external_references=[{ - "source_name": "unknown", - "url": "https://examplewebsite2.com/", - }]) + malv2 = mal.new_version( + external_references=[{ + "source_name": "unknown", + "url": "https://examplewebsite2.com/", + }], + ) 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]) diff --git a/stix2/test/v20/test_indicator.py b/stix2/test/v20/test_indicator.py index 1ae33ec..6f3bfea 100644 --- a/stix2/test/v20/test_indicator.py +++ b/stix2/test/v20/test_indicator.py @@ -20,7 +20,8 @@ EXPECTED_INDICATOR = """{ ] }""" -EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" +EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join( + """ type='indicator', id='indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7', created='2017-01-01T00:00:01.000Z', @@ -28,7 +29,8 @@ EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", valid_from='1970-01-01T00:00:01Z', labels=['malicious-activity'] -""".split()) + ")" +""".split(), +) + ")" def test_indicator_with_all_required_properties(): diff --git a/stix2/test/v20/test_observed_data.py b/stix2/test/v20/test_observed_data.py index 354d70c..bd60383 100644 --- a/stix2/test/v20/test_observed_data.py +++ b/stix2/test/v20/test_observed_data.py @@ -1180,50 +1180,56 @@ def test_process_example_extensions_empty(): def test_process_example_with_WindowsProcessExt_Object(): - 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 - }) + 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.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", + 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" assert p.extensions["windows-service-ext"].service_type == "SERVICE_WIN32_OWN_PROCESS" def test_process_example_with_WindowsProcessServiceExt(): - 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", + 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", + }, + "windows-process-ext": { + "aslr_enabled": True, + "dep_enabled": True, + "priority": "HIGH_PRIORITY_CLASS", + "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309", + }, }, - "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" diff --git a/stix2/test/v20/test_parsing.py b/stix2/test/v20/test_parsing.py index 6eb554e..6317e5a 100644 --- a/stix2/test/v20/test_parsing.py +++ b/stix2/test/v20/test_parsing.py @@ -2,8 +2,7 @@ from collections import OrderedDict import pytest -import stix2 -from stix2 import exceptions, parsing +from stix2 import DEFAULT_VERSION, exceptions, parsing, registration, registry BUNDLE = { "type": "bundle", @@ -59,7 +58,7 @@ def test_parse_observable_with_version(): 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") +@pytest.mark.xfail(reason="The default version is no longer 2.0", condition=DEFAULT_VERSION != "2.0") def test_parse_observable_with_no_version(): observable = {"type": "file", "name": "foo.exe"} obs_obj = parsing.parse_observable(observable) @@ -73,8 +72,7 @@ def test_register_marking_with_version(): _type = 'x-new-marking1' _properties = OrderedDict() - parsing._register_marking(NewMarking1, version='2.0') - v = 'v20' + registration._register_marking(NewMarking1, version='2.0') - assert NewMarking1._type in parsing.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(parsing.STIX2_OBJ_MAPS[v]['markings'][NewMarking1._type]) + assert NewMarking1._type in registry.STIX2_OBJ_MAPS['2.0']['markings'] + assert 'v20' in str(registry.STIX2_OBJ_MAPS['2.0']['markings'][NewMarking1._type]) diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py index 526fe97..4d0073a 100644 --- a/stix2/test/v20/test_pattern_expressions.py +++ b/stix2/test/v20/test_pattern_expressions.py @@ -306,10 +306,12 @@ def test_multiple_qualifiers(): def test_set_op(): - exp = stix2.ObservationExpression(stix2.IsSubsetComparisonExpression( - "network-traffic:dst_ref.value", - "2001:0db8:dead:beef:0000:0000:0000:0000/64", - )) + 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']" diff --git a/stix2/test/v20/test_utils.py b/stix2/test/v20/test_utils.py index a66f3e8..0443933 100644 --- a/stix2/test/v20/test_utils.py +++ b/stix2/test/v20/test_utils.py @@ -71,7 +71,7 @@ def test_parse_datetime_invalid(ts): {"a": 1}, '{"a": 1}', StringIO(u'{"a": 1}'), - [("a", 1,)], + [("a", 1)], ], ) def test_get_dict(data): @@ -237,3 +237,146 @@ def test_find_property_index(object, tuple_to_find, expected_index): ) def test_iterate_over_values(dict_value, tuple_to_find, expected_index): assert stix2.serialization._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index + + +@pytest.mark.parametrize( + "type_", [ + "attack-pattern", + "campaign", + "course-of-action", + "identity", + "indicator", + "intrusion-set", + "malware", + "observed-data", + "report", + "threat-actor", + "tool", + "vulnerability", + ], +) +def test_is_sdo_dict(type_): + d = { + "type": type_, + } + assert stix2.utils.is_sdo(d, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "identity", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ], +) +def test_is_not_sdo_dict(dict_): + assert not stix2.utils.is_sdo(dict_, "2.0") + + +def test_is_sco_dict(): + d = { + "type": "file", + } + + assert stix2.utils.is_sco(d, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ], +) +def test_is_not_sco_dict(dict_): + assert not stix2.utils.is_sco(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "relationship"}, + {"type": "sighting"}, + ], +) +def test_is_sro_dict(dict_): + assert stix2.utils.is_sro(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "identity"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "sighting", "spec_version": "2.1"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ], +) +def test_is_not_sro_dict(dict_): + assert not stix2.utils.is_sro(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "software"}, + {"type": "marking-definition"}, + { + "type": "bundle", + "id": "bundle--8f431680-6278-4767-ba43-5edb682d7086", + "spec_version": "2.0", + "objects": [ + {"type": "identity"}, + {"type": "software"}, + {"type": "marking-definition"}, + ], + }, + ], +) +def test_is_object_dict(dict_): + assert stix2.utils.is_object(dict_, "2.0") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "sighting", "spec_version": "2.1"}, + {"type": "foo", "spec_version": "2.1"}, + {"type": "foo"}, + ], +) +def test_is_not_object_dict(dict_): + assert not stix2.utils.is_object(dict_, "2.0") diff --git a/stix2/test/v20/test_versioning.py b/stix2/test/v20/test_versioning.py index 03d43cc..e2525e4 100644 --- a/stix2/test/v20/test_versioning.py +++ b/stix2/test/v20/test_versioning.py @@ -46,10 +46,12 @@ def test_making_new_version_with_embedded_object(): **CAMPAIGN_MORE_KWARGS ) - campaign_v2 = campaign_v1.new_version(external_references=[{ - "source_name": "capec", - "external_id": "CAPEC-164", - }]) + 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.created_by_ref == campaign_v2.created_by_ref @@ -237,8 +239,10 @@ def test_remove_custom_stix_property(): mal_nc = stix2.versioning.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(): diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py index ea6d3a8..36e3548 100644 --- a/stix2/test/v21/test_custom.py +++ b/stix2/test/v21/test_custom.py @@ -4,6 +4,8 @@ import pytest import stix2 import stix2.base +import stix2.registration +import stix2.registry import stix2.v21 from ...exceptions import DuplicateRegistrationError, InvalidValueError @@ -1199,7 +1201,7 @@ def test_register_custom_object(): _type = 'awesome-object' with pytest.raises(ValueError) as excinfo: - stix2.parsing._register_object(CustomObject2, version="2.1") + stix2.registration._register_object(CustomObject2, version="2.1") assert '@CustomObject decorator' in str(excinfo) @@ -1263,9 +1265,8 @@ def test_register_custom_object_with_version(): } cust_obj_1 = stix2.parsing.dict_to_stix2(custom_obj_1, version='2.1') - v = 'v21' - assert cust_obj_1.type in stix2.parsing.STIX2_OBJ_MAPS[v]['objects'] + assert cust_obj_1.type in stix2.registry.STIX2_OBJ_MAPS['2.1']['objects'] assert cust_obj_1.spec_version == "2.1" @@ -1293,9 +1294,8 @@ class NewObservable3(object): def test_register_observable(): custom_obs = NewObservable3(property1="Test Observable") - v = 'v21' - assert custom_obs.type in stix2.parsing.STIX2_OBJ_MAPS[v]['observables'] + assert custom_obs.type in stix2.registry.STIX2_OBJ_MAPS['2.1']['observables'] def test_register_duplicate_observable(): @@ -1321,10 +1321,9 @@ def test_register_observable_custom_extension(): pass example = NewExtension2(property1="Hi there") - v = 'v21' - assert 'domain-name' in stix2.parsing.STIX2_OBJ_MAPS[v]['observables'] - assert example._type in stix2.parsing.STIX2_OBJ_MAPS[v]['observable-extensions']['domain-name'] + assert 'domain-name' in stix2.registry.STIX2_OBJ_MAPS['2.1']['observables'] + assert example._type in stix2.registry.STIX2_OBJ_MAPS['2.1']['observable-extensions']['domain-name'] def test_register_duplicate_observable_extension(): diff --git a/stix2/test/v21/test_datastore_memory.py b/stix2/test/v21/test_datastore_memory.py index 60f577e..870f82e 100644 --- a/stix2/test/v21/test_datastore_memory.py +++ b/stix2/test/v21/test_datastore_memory.py @@ -191,11 +191,13 @@ 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, - type="bundle", - )) + 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 diff --git a/stix2/test/v21/test_environment.py b/stix2/test/v21/test_environment.py index 95094fe..0da01d1 100644 --- a/stix2/test/v21/test_environment.py +++ b/stix2/test/v21/test_environment.py @@ -11,9 +11,9 @@ import stix2.exceptions from .constants import ( ATTACK_PATTERN_ID, ATTACK_PATTERN_KWARGS, CAMPAIGN_ID, CAMPAIGN_KWARGS, FAKE_TIME, IDENTITY_ID, IDENTITY_KWARGS, INDICATOR_ID, INDICATOR_KWARGS, - LOCATION_ID, MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS, REPORT_ID, - REPORT_KWARGS, THREAT_ACTOR_ID, THREAT_ACTOR_KWARGS, TOOL_ID, TOOL_KWARGS, - VULNERABILITY_ID, VULNERABILITY_KWARGS, + LOCATION_ID, LOCATION_KWARGS, MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS, + REPORT_ID, REPORT_KWARGS, THREAT_ACTOR_ID, THREAT_ACTOR_KWARGS, TOOL_ID, + TOOL_KWARGS, VULNERABILITY_ID, VULNERABILITY_KWARGS, ) FS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") @@ -495,26 +495,34 @@ def test_semantic_equivalence_on_same_indicator(): def test_semantic_equivalence_on_same_location1(): - LOCATION_KWARGS = dict(latitude=45, longitude=179) - loc1 = stix2.v21.Location(id=LOCATION_ID, **LOCATION_KWARGS) - loc2 = stix2.v21.Location(id=LOCATION_ID, **LOCATION_KWARGS) + location_kwargs = dict(latitude=45, longitude=179) + loc1 = stix2.v21.Location(id=LOCATION_ID, **location_kwargs) + loc2 = stix2.v21.Location(id=LOCATION_ID, **location_kwargs) env = stix2.Environment().semantically_equivalent(loc1, loc2) assert round(env) == 100 def test_semantic_equivalence_on_same_location2(): - LOCATION_KWARGS = dict( + location_kwargs = dict( latitude=38.889, longitude=-77.023, region="northern-america", country="us", ) - loc1 = stix2.v21.Location(id=LOCATION_ID, **LOCATION_KWARGS) - loc2 = stix2.v21.Location(id=LOCATION_ID, **LOCATION_KWARGS) + loc1 = stix2.v21.Location(id=LOCATION_ID, **location_kwargs) + loc2 = stix2.v21.Location(id=LOCATION_ID, **location_kwargs) env = stix2.Environment().semantically_equivalent(loc1, loc2) assert round(env) == 100 +def test_semantic_equivalence_location_with_no_latlong(): + loc_kwargs = dict(country="US", administrative_area="US-DC") + loc1 = stix2.v21.Location(id=LOCATION_ID, **LOCATION_KWARGS) + loc2 = stix2.v21.Location(id=LOCATION_ID, **loc_kwargs) + env = stix2.Environment().semantically_equivalent(loc1, loc2) + assert round(env) != 100 + + def test_semantic_equivalence_on_same_malware(): malw1 = stix2.v21.Malware(id=MALWARE_ID, **MALWARE_KWARGS) malw2 = stix2.v21.Malware(id=MALWARE_ID, **MALWARE_KWARGS) diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py index 8452f70..2b22418 100644 --- a/stix2/test/v21/test_indicator.py +++ b/stix2/test/v21/test_indicator.py @@ -20,7 +20,8 @@ EXPECTED_INDICATOR = """{ "valid_from": "1970-01-01T00:00:01Z" }""" -EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" +EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join( + """ type='indicator', spec_version='2.1', id='indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7', @@ -30,7 +31,8 @@ EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" pattern_type='stix', pattern_version='2.1', valid_from='1970-01-01T00:00:01Z' -""".split()) + ")" +""".split(), +) + ")" def test_indicator_with_all_required_properties(): diff --git a/stix2/test/v21/test_location.py b/stix2/test/v21/test_location.py index 7517fdf..cf9c101 100644 --- a/stix2/test/v21/test_location.py +++ b/stix2/test/v21/test_location.py @@ -19,14 +19,16 @@ EXPECTED_LOCATION_1 = """{ "longitude": 2.3522 }""" -EXPECTED_LOCATION_1_REPR = "Location(" + " ".join(""" +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()) + ")" + longitude=2.3522""".split(), +) + ")" EXPECTED_LOCATION_2 = """{ "type": "location", @@ -38,13 +40,15 @@ EXPECTED_LOCATION_2 = """{ } """ -EXPECTED_LOCATION_2_REPR = "Location(" + " ".join(""" +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()) + ")" + region='north-america'""".split(), +) + ")" def test_location_with_some_required_properties(): diff --git a/stix2/test/v21/test_malware_analysis.py b/stix2/test/v21/test_malware_analysis.py index 22f4171..bf4bbe6 100644 --- a/stix2/test/v21/test_malware_analysis.py +++ b/stix2/test/v21/test_malware_analysis.py @@ -10,13 +10,9 @@ MALWARE_ANALYSIS_JSON = """{ "type": "malware-analysis", "spec_version": "2.1", "id": "malware-analysis--f8afc020-f92f-4906-a971-88ee5882eb46", + "created_by_ref": "identity--e0353ed3-991e-4f71-a332-114c2f10b84f", "created": "2017-11-28T09:44:58.418Z", "modified": "2017-12-31T21:27:49.754Z", - "created_by_ref": "identity--e0353ed3-991e-4f71-a332-114c2f10b84f", - "labels": [ - "label1", - "label2" - ], "product": "Acme Malware Analyzer", "version": "2.5", "host_vm_ref": "software--1bda7336-fe67-469f-a8ca-ab6268b0449b", @@ -40,7 +36,11 @@ MALWARE_ANALYSIS_JSON = """{ "file--fc27e371-6c88-4c5c-868a-4dda0e60b167", "url--6f7a74cd-8eb2-4b88-a4da-aa878e50ac2e" ], - "sample_ref": "email-addr--499a32d7-74c1-4276-ace9-725ac933e243" + "sample_ref": "email-addr--499a32d7-74c1-4276-ace9-725ac933e243", + "labels": [ + "label1", + "label2" + ] }""" diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py index 00ee055..c1cb38e 100644 --- a/stix2/test/v21/test_observed_data.py +++ b/stix2/test/v21/test_observed_data.py @@ -496,12 +496,14 @@ 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, version="2.1") - assert all(x in odata.objects["3"].extensions['archive-ext'].contains_refs - for x in [ - "file--ecd47d73-15e4-5250-afda-ef8897b22340", - "file--65f2873d-38c2-56b4-bfa5-e3ef21e8a3c3", - "file--ef2d6dca-ec7d-5ab7-8dd9-ec9c0dee0eac", - ]) + assert all( + x in odata.objects["3"].extensions['archive-ext'].contains_refs + for x in [ + "file--ecd47d73-15e4-5250-afda-ef8897b22340", + "file--65f2873d-38c2-56b4-bfa5-e3ef21e8a3c3", + "file--ef2d6dca-ec7d-5ab7-8dd9-ec9c0dee0eac", + ] + ) @pytest.mark.parametrize( @@ -904,14 +906,14 @@ def test_file_with_archive_ext_object(): f_obj = stix2.v21.File( name="foo", extensions={ "archive-ext": { - "contains_refs": [ad, ], + "contains_refs": [ad], }, }, ) f_ref = stix2.v21.File( name="foo", extensions={ "archive-ext": { - "contains_refs": [ad.id, ], + "contains_refs": [ad.id], }, }, ) @@ -1229,9 +1231,11 @@ def test_process_example_empty_error(): def test_process_example_empty_with_extensions(): with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: - stix2.v21.Process(extensions={ - "windows-process-ext": {}, - }) + stix2.v21.Process( + extensions={ + "windows-process-ext": {}, + }, + ) assert excinfo.value.cls == stix2.v21.Process @@ -1276,50 +1280,56 @@ def test_process_example_extensions_empty(): 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 - }) + 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", + 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", + 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", + }, }, - "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" diff --git a/stix2/test/v21/test_parsing.py b/stix2/test/v21/test_parsing.py index 53b53b2..f23eb7d 100644 --- a/stix2/test/v21/test_parsing.py +++ b/stix2/test/v21/test_parsing.py @@ -2,8 +2,7 @@ from collections import OrderedDict import pytest -import stix2 -from stix2 import exceptions, parsing +from stix2 import DEFAULT_VERSION, exceptions, parsing, registration, registry BUNDLE = { "type": "bundle", @@ -64,7 +63,7 @@ def test_parse_observable_with_version(): assert v in str(obs_obj.__class__) -@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") +@pytest.mark.xfail(reason="The default version is not 2.1", condition=DEFAULT_VERSION != "2.1") def test_parse_observable_with_no_version(): observable = {"type": "file", "name": "foo.exe", "spec_version": "2.1"} obs_obj = parsing.parse_observable(observable) @@ -78,22 +77,20 @@ def test_register_marking_with_version(): _type = 'x-new-marking1' _properties = OrderedDict() - parsing._register_marking(NewMarking1, version='2.1') - v = 'v21' + registration._register_marking(NewMarking1, version='2.1') - assert NewMarking1._type in parsing.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(parsing.STIX2_OBJ_MAPS[v]['markings'][NewMarking1._type]) + assert NewMarking1._type in registry.STIX2_OBJ_MAPS['2.1']['markings'] + assert 'v21' in str(registry.STIX2_OBJ_MAPS['2.1']['markings'][NewMarking1._type]) -@pytest.mark.xfail(reason="The default version is not 2.1", condition=stix2.DEFAULT_VERSION != "2.1") +@pytest.mark.xfail(reason="The default version is not 2.1", condition=DEFAULT_VERSION != "2.1") def test_register_marking_with_no_version(): # Uses default version (2.1 in this case) class NewMarking2: _type = 'x-new-marking2' _properties = OrderedDict() - parsing._register_marking(NewMarking2) - v = 'v21' + registration._register_marking(NewMarking2) - assert NewMarking2._type in parsing.STIX2_OBJ_MAPS[v]['markings'] - assert v in str(parsing.STIX2_OBJ_MAPS[v]['markings'][NewMarking2._type]) + assert NewMarking2._type in registry.STIX2_OBJ_MAPS['2.1']['markings'] + assert 'v21' in str(registry.STIX2_OBJ_MAPS['2.1']['markings'][NewMarking2._type]) diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py index 4f365d7..d7afe5c 100644 --- a/stix2/test/v21/test_pattern_expressions.py +++ b/stix2/test/v21/test_pattern_expressions.py @@ -444,10 +444,12 @@ def test_multiple_qualifiers(): def test_set_op(): - exp = stix2.ObservationExpression(stix2.IsSubsetComparisonExpression( - "network-traffic:dst_ref.value", - "2001:0db8:dead:beef:0000:0000:0000:0000/64", - )) + 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']" @@ -712,12 +714,12 @@ def test_parsing_boolean(): def test_parsing_mixed_boolean_expression_1(): - patt_obj = create_pattern_object("[a:b = 1 AND a:b = 2 OR a:b = 3]",) + patt_obj = create_pattern_object("[a:b = 1 AND a:b = 2 OR a:b = 3]") assert str(patt_obj) == "[a:b = 1 AND a:b = 2 OR a:b = 3]" def test_parsing_mixed_boolean_expression_2(): - patt_obj = create_pattern_object("[a:b = 1 OR a:b = 2 AND a:b = 3]",) + patt_obj = create_pattern_object("[a:b = 1 OR a:b = 2 AND a:b = 3]") assert str(patt_obj) == "[a:b = 1 OR a:b = 2 AND a:b = 3]" diff --git a/stix2/test/v21/test_sighting.py b/stix2/test/v21/test_sighting.py index 0493b71..0ef5faa 100644 --- a/stix2/test/v21/test_sighting.py +++ b/stix2/test/v21/test_sighting.py @@ -5,7 +5,9 @@ import pytz import stix2 -from .constants import IDENTITY_ID, INDICATOR_ID, SIGHTING_ID, SIGHTING_KWARGS +from .constants import ( + IDENTITY_ID, INDICATOR_ID, LOCATION_ID, SIGHTING_ID, SIGHTING_KWARGS, +) EXPECTED_SIGHTING = """{ "type": "sighting", @@ -15,7 +17,8 @@ EXPECTED_SIGHTING = """{ "modified": "2016-04-06T20:06:37.000Z", "sighting_of_ref": "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7", "where_sighted_refs": [ - "identity--311b2d2d-f010-4473-83ec-1edf84858f4c" + "identity--311b2d2d-f010-4473-83ec-1edf84858f4c", + "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64" ] }""" @@ -41,7 +44,7 @@ def test_sighting_all_required_properties(): created=now, modified=now, sighting_of_ref=INDICATOR_ID, - where_sighted_refs=[IDENTITY_ID], + where_sighted_refs=[IDENTITY_ID, LOCATION_ID], ) assert str(s) == EXPECTED_SIGHTING @@ -101,6 +104,7 @@ def test_create_sighting_from_objects_rather_than_ids(malware): # noqa: F811 "type": "sighting", "where_sighted_refs": [ IDENTITY_ID, + LOCATION_ID, ], }, ], @@ -114,4 +118,4 @@ def test_parse_sighting(data): 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_ID - assert sighting.where_sighted_refs == [IDENTITY_ID] + assert sighting.where_sighted_refs == [IDENTITY_ID, LOCATION_ID] diff --git a/stix2/test/v21/test_utils.py b/stix2/test/v21/test_utils.py index f64cec2..6d108d4 100644 --- a/stix2/test/v21/test_utils.py +++ b/stix2/test/v21/test_utils.py @@ -71,7 +71,7 @@ def test_parse_datetime_invalid(ts): {"a": 1}, '{"a": 1}', StringIO(u'{"a": 1}'), - [("a", 1,)], + [("a", 1)], ], ) def test_get_dict(data): @@ -241,3 +241,153 @@ def test_find_property_index(object, tuple_to_find, expected_index): ) def test_iterate_over_values(dict_value, tuple_to_find, expected_index): assert stix2.serialization._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index + + +@pytest.mark.parametrize( + "type_", [ + "attack-pattern", + "campaign", + "course-of-action", + "identity", + "indicator", + "intrusion-set", + "malware", + "observed-data", + "report", + "threat-actor", + "tool", + "vulnerability", + + # New in 2.1 + "grouping", + "infrastructure", + "location", + "malware-analysis", + "note", + "opinion", + ], +) +def test_is_sdo_dict(type_): + d = { + "type": type_, + "spec_version": "2.1", + } + assert stix2.utils.is_sdo(d, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "identity"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + ], +) +def test_is_not_sdo_dict(dict_): + assert not stix2.utils.is_sdo(dict_, "2.1") + + +def test_is_sco_dict(): + d = { + "type": "file", + "spec_version": "2.1", + } + + assert stix2.utils.is_sco(d, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "identity", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship", "spec_version": "2.1"}, + {"type": "relationship"}, + {"type": "foo", "spec_version": "2.1"}, + ], +) +def test_is_not_sco_dict(dict_): + assert not stix2.utils.is_sco(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "relationship", "spec_version": "2.1"}, + {"type": "sighting", "spec_version": "2.1"}, + ], +) +def test_is_sro_dict(dict_): + assert stix2.utils.is_sro(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "identity"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "software"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "marking-definition"}, + {"type": "bundle", "spec_version": "2.1"}, + {"type": "bundle"}, + {"type": "language-content", "spec_version": "2.1"}, + {"type": "language-content"}, + {"type": "relationship"}, + {"type": "sighting"}, + {"type": "foo", "spec_version": "2.1"}, + ], +) +def test_is_not_sro_dict(dict_): + assert not stix2.utils.is_sro(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "language-content", "spec_version": "2.1"}, + { + "type": "bundle", + "id": "bundle--8f431680-6278-4767-ba43-5edb682d7086", + "objects": [ + {"type": "identity", "spec_version": "2.1"}, + {"type": "software", "spec_version": "2.1"}, + {"type": "marking-definition", "spec_version": "2.1"}, + {"type": "language-content", "spec_version": "2.1"}, + ], + }, + ], +) +def test_is_object_dict(dict_): + assert stix2.utils.is_object(dict_, "2.1") + + +@pytest.mark.parametrize( + "dict_", [ + {"type": "identity"}, + {"type": "software"}, + {"type": "marking-definition"}, + {"type": "bundle"}, + {"type": "language-content"}, + {"type": "relationship"}, + {"type": "sighting"}, + {"type": "foo"}, + ], +) +def test_is_not_object_dict(dict_): + assert not stix2.utils.is_object(dict_, "2.1") diff --git a/stix2/test/v21/test_versioning.py b/stix2/test/v21/test_versioning.py index adfa7a0..051fb2e 100644 --- a/stix2/test/v21/test_versioning.py +++ b/stix2/test/v21/test_versioning.py @@ -50,10 +50,12 @@ def test_making_new_version_with_embedded_object(): **CAMPAIGN_MORE_KWARGS ) - campaign_v2 = campaign_v1.new_version(external_references=[{ - "source_name": "capec", - "external_id": "CAPEC-164", - }]) + 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 @@ -344,6 +346,38 @@ def test_version_sco_with_custom(): assert revoked_obj.revoked +def test_version_sco_id_contributing_properties(): + file_sco_obj = stix2.v21.File( + name="data.txt", + created="1973-11-23T02:31:37Z", + modified="1991-05-13T19:24:57Z", + revoked=False, + allow_custom=True, + ) + + with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as e: + stix2.versioning.new_version(file_sco_obj, name="foo.dat") + + assert e.value.unchangable_properties == {"name"} + + +def test_version_sco_id_contributing_properties_dict(): + file_sco_dict = { + "type": "file", + "id": "file--c27c572c-2e17-5ce1-817e-67bb97629a56", + "spec_version": "2.1", + "name": "data.txt", + "created": "1973-11-23T02:31:37Z", + "modified": "1991-05-13T19:24:57Z", + "revoked": False, + } + + with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as e: + stix2.versioning.new_version(file_sco_dict, name="foo.dat") + + assert e.value.unchangable_properties == {"name"} + + def test_version_disable_custom(): m = stix2.v21.Malware( name="foo", description="Steals your identity!", is_family=False, diff --git a/stix2/utils.py b/stix2/utils.py index 1b88f72..3e272f8 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,5 +1,6 @@ """Utility functions and classes for the STIX2 library.""" +import collections.abc import datetime as dt import enum import json @@ -8,7 +9,8 @@ import re import pytz import six -import stix2 +import stix2.registry as mappings +import stix2.version # 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 @@ -71,9 +73,11 @@ def _to_enum(value, enum_type, enum_default=None): elif isinstance(value, six.string_types): value = enum_type[value.upper()] else: - raise TypeError("Not a valid {}: {}".format( - enum_type.__name__, value, - )) + raise TypeError( + "Not a valid {}: {}".format( + enum_type.__name__, value, + ), + ) return value @@ -311,18 +315,262 @@ def get_type_from_id(stix_id): return stix_id.split('--', 1)[0] -def is_marking(obj_or_id): - """Determines whether the given object or object ID is/is for a marking - definition. +def detect_spec_version(stix_dict): + """ + Given a dict representing a STIX object, try to detect what spec version + it is likely to comply with. - :param obj_or_id: A STIX object or object ID as a string. - :return: True if a marking definition, False otherwise. + :param stix_dict: A dict with some STIX content. Must at least have a + "type" property. + :return: A STIX version in "X.Y" format """ - if isinstance(obj_or_id, (stix2.base._STIXBase, dict)): - result = obj_or_id["type"] == "marking-definition" + obj_type = stix_dict["type"] + + if 'spec_version' in stix_dict: + # For STIX 2.0, applies to bundles only. + # For STIX 2.1+, applies to SCOs, SDOs, SROs, and markings only. + v = stix_dict['spec_version'] + elif "id" not in stix_dict: + # Only 2.0 SCOs don't have ID properties + v = "2.0" + elif obj_type == 'bundle': + # Bundle without a spec_version property: must be 2.1. But to + # future-proof, use max version over all contained SCOs, with 2.1 + # minimum. + v = max( + "2.1", + max( + detect_spec_version(obj) for obj in stix_dict["objects"] + ), + ) + elif obj_type in mappings.STIX2_OBJ_MAPS["2.1"]["observables"]: + # Non-bundle object with an ID and without spec_version. Could be a + # 2.1 SCO or 2.0 SDO/SRO/marking. Check for 2.1 SCO... + v = "2.1" else: - # it's a string ID - result = obj_or_id.startswith("marking-definition--") + # Not a 2.1 SCO; must be a 2.0 object. + v = "2.0" + + return v + + +def _stix_type_of(value): + """ + Get a STIX type from the given value: if a STIX ID is passed, the type + prefix is extracted; if string which is not a STIX ID is passed, it is + assumed to be a STIX type and is returned; otherwise it is assumed to be a + mapping with a "type" property, and the value of that property is returned. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :return: A STIX type + """ + if isinstance(value, str): + if "--" in value: + type_ = get_type_from_id(value) + else: + type_ = value + else: + type_ = value["type"] + + return type_ + + +def is_sdo(value, stix_version=stix2.version.DEFAULT_VERSION): + """ + Determine whether the given object, type, or ID is/is for an SDO of the + given STIX version. If value is a type or ID, this just checks whether + the type was registered as an SDO in the given STIX version. If a mapping, + *simple* STIX version inference is additionally done on the value, and the + result is checked against stix_version. It does not attempt to fully + validate the value. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is an SDO type of the given + version; False if not + """ + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] + type_ = _stix_type_of(value) + result = type_ in cls_maps["objects"] and type_ not in { + "relationship", "sighting", "marking-definition", "bundle", + "language-content", + } + + return result + + +def is_sco(value, stix_version=stix2.version.DEFAULT_VERSION): + """ + Determine whether the given object, type, or ID is/is for an SCO of the + given STIX version. If value is a type or ID, this just checks whether + the type was registered as an SCO in the given STIX version. If a mapping, + *simple* STIX version inference is additionally done on the value, and the + result is checked against stix_version. It does not attempt to fully + validate the value. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is an SCO type of the given + version; False if not + """ + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] + type_ = _stix_type_of(value) + result = type_ in cls_maps["observables"] + + return result + + +def is_sro(value, stix_version=stix2.version.DEFAULT_VERSION): + """ + Determine whether the given object, type, or ID is/is for an SRO of the + given STIX version. If value is a type or ID, this just checks whether + the type is "sighting" or "relationship". If a mapping, *simple* STIX + version inference is additionally done on the value, and the result is + checked against stix_version. It does not attempt to fully validate the + value. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is an SRO type of the given + version; False if not + """ + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + # No need to check registration in this case + type_ = _stix_type_of(value) + result = type_ in ("sighting", "relationship") + + return result + + +def is_object(value, stix_version=stix2.version.DEFAULT_VERSION): + """ + Determine whether an object, type, or ID is/is for any STIX object. This + includes all SDOs, SCOs, meta-objects, and bundle. If value is a type or + ID, this just checks whether the type was registered in the given STIX + version. If a mapping, *simple* STIX version inference is additionally + done on the value, and the result is checked against stix_version. It does + not attempt to fully validate the value. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :return: True if the type of the given value is a valid STIX type with + respect to the given STIX version; False if not + """ + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + cls_maps = mappings.STIX2_OBJ_MAPS[stix_version] + type_ = _stix_type_of(value) + result = type_ in cls_maps["observables"] \ + or type_ in cls_maps["objects"] + + return result + + +def is_marking(value, stix_version=stix2.version.DEFAULT_VERSION): + """ + Determine whether the given object, type, or ID is/is for an marking + definition of the given STIX version. If value is a type or ID, this just + checks whether the type is "marking-definition". If a mapping, *simple* + STIX version inference is additionally done on the value, and the result + is checked against stix_version. It does not attempt to fully validate the + value. + + :param value: A STIX object, object ID, or type as a string. + :param stix_version: A STIX version as a string + :return: True if the value is/is for a marking definition, False otherwise. + """ + + result = True + if isinstance(value, collections.abc.Mapping): + value_stix_version = detect_spec_version(value) + if value_stix_version != stix_version: + result = False + + if result: + # No need to check registration in this case + type_ = _stix_type_of(value) + result = type_ == "marking-definition" + + return result + + +class STIXTypeClass(enum.Enum): + """ + Represents different classes of STIX type. + """ + SDO = 0 + SCO = 1 + SRO = 2 + + +def is_stix_type(value, stix_version=stix2.version.DEFAULT_VERSION, *types): + """ + Determine whether the type of the given value satisfies the given + constraints. 'types' must contain STIX types as strings, and/or the + STIXTypeClass enum values. STIX types imply an exact match constraint; + STIXTypeClass enum values imply a more general constraint, that the object + or type be in that class of STIX type. These constraints are implicitly + OR'd together. + + :param value: A mapping with a "type" property, or a STIX ID or type + as a string + :param stix_version: A STIX version as a string + :param types: A sequence of STIX type strings or STIXTypeClass enum values + :return: True if the object or type satisfies the constraints; False if not + """ + + for type_ in types: + if type_ is STIXTypeClass.SDO: + result = is_sdo(value, stix_version) + elif type_ is STIXTypeClass.SCO: + result = is_sco(value, stix_version) + elif type_ is STIXTypeClass.SRO: + result = is_sro(value, stix_version) + else: + # Assume a string STIX type is given instead of a class enum, + # and just check for exact match. + obj_type = _stix_type_of(value) + result = obj_type == type_ and is_object(value, stix_version) + + if result: + break + + else: + result = False return result diff --git a/stix2/v20/__init__.py b/stix2/v20/__init__.py index 36d09be..002343b 100644 --- a/stix2/v20/__init__.py +++ b/stix2/v20/__init__.py @@ -32,7 +32,7 @@ from .observables import ( UNIXAccountExt, UserAccount, WindowsPEBinaryExt, WindowsPEOptionalHeaderType, WindowsPESection, WindowsProcessExt, WindowsRegistryKey, WindowsRegistryValueType, WindowsServiceExt, - X509Certificate, X509V3ExtenstionsType, + X509Certificate, X509V3ExtensionsType, ) from .sdo import ( AttackPattern, Campaign, CourseOfAction, CustomObject, Identity, Indicator, @@ -123,7 +123,7 @@ __all__ = """ UNIXAccountExt, UserAccount, WindowsPEBinaryExt, WindowsPEOptionalHeaderType, WindowsPESection, WindowsProcessExt, WindowsRegistryKey, WindowsRegistryValueType, WindowsServiceExt, - X509Certificate, X509V3ExtenstionsType, + X509Certificate, X509V3ExtensionsType, AttackPattern, Campaign, CourseOfAction, CustomObject, Identity, Indicator, IntrusionSet, Malware, ObservedData, Report, ThreatActor, Tool, diff --git a/stix2/v20/common.py b/stix2/v20/common.py index 97291c5..ab8a8e3 100644 --- a/stix2/v20/common.py +++ b/stix2/v20/common.py @@ -124,11 +124,11 @@ class MarkingDefinition(_STIXBase20, _MarkingsMixin): ('id', IDProperty(_type, spec_version='2.0')), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), ('created', TimestampProperty(default=lambda: NOW)), + ('definition_type', StringProperty(required=True)), + ('definition', MarkingProperty(required=True)), ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), ('granular_markings', ListProperty(GranularMarking)), - ('definition_type', StringProperty(required=True)), - ('definition', MarkingProperty(required=True)), ]) def __init__(self, **kwargs): diff --git a/stix2/v20/observables.py b/stix2/v20/observables.py index 79d4e79..56262b0 100644 --- a/stix2/v20/observables.py +++ b/stix2/v20/observables.py @@ -440,24 +440,28 @@ class SocketExt(_Extension): ('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(spec_version="2.0")), ( - '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()), @@ -537,33 +541,39 @@ class WindowsServiceExt(_Extension): ('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_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_status', EnumProperty( + allowed=[ + "SERVICE_CONTINUE_PENDING", + "SERVICE_PAUSE_PENDING", + "SERVICE_PAUSED", + "SERVICE_RUNNING", + "SERVICE_START_PENDING", + "SERVICE_STOP_PENDING", + "SERVICE_STOPPED", + ], + ), ), ]) @@ -687,21 +697,23 @@ class WindowsRegistryValueType(_STIXBase20): ('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", + ], + ), ), ]) @@ -724,7 +736,7 @@ class WindowsRegistryKey(_Observable): ]) -class X509V3ExtenstionsType(_STIXBase20): +class X509V3ExtensionsType(_STIXBase20): """For more detailed information on this object's properties, see `the STIX 2.0 specification `__. """ # noqa @@ -770,7 +782,7 @@ class X509Certificate(_Observable): ('subject_public_key_algorithm', StringProperty()), ('subject_public_key_modulus', StringProperty()), ('subject_public_key_exponent', IntegerProperty()), - ('x509_v3_extensions', EmbeddedObjectProperty(type=X509V3ExtenstionsType)), + ('x509_v3_extensions', EmbeddedObjectProperty(type=X509V3ExtensionsType)), ('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=_type)), ]) @@ -790,11 +802,13 @@ def CustomObservable(type='x-custom-observable', properties=None): """ def wrapper(cls): - _properties = list(itertools.chain.from_iterable([ - [('type', TypeProperty(type, spec_version='2.0'))], - properties, - [('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=type))], - ])) + _properties = list( + itertools.chain.from_iterable([ + [('type', TypeProperty(type, spec_version='2.0'))], + properties, + [('extensions', ExtensionsProperty(spec_version="2.0", enclosing_type=type))], + ]), + ) return _custom_observable_builder(cls, type, _properties, '2.0', _Observable) return wrapper diff --git a/stix2/v20/sdo.py b/stix2/v20/sdo.py index bb69f26..44ffa75 100644 --- a/stix2/v20/sdo.py +++ b/stix2/v20/sdo.py @@ -362,23 +362,25 @@ def CustomObject(type='x-custom-type', properties=None): """ def wrapper(cls): - _properties = list(itertools.chain.from_iterable([ - [ - ('type', TypeProperty(type, spec_version='2.0')), - ('id', IDProperty(type, spec_version='2.0')), - ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), - ('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)), - ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), - ('granular_markings', ListProperty(GranularMarking)), - ], - sorted([x for x in properties if x[0].startswith('x_')], key=lambda x: x[0]), - ])) + _properties = list( + itertools.chain.from_iterable([ + [ + ('type', TypeProperty(type, spec_version='2.0')), + ('id', IDProperty(type, spec_version='2.0')), + ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.0')), + ('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)), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.0'))), + ('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.0', _DomainObject) return wrapper diff --git a/stix2/v21/__init__.py b/stix2/v21/__init__.py index 77a28ee..15b2fb9 100644 --- a/stix2/v21/__init__.py +++ b/stix2/v21/__init__.py @@ -32,7 +32,7 @@ from .observables import ( UNIXAccountExt, UserAccount, WindowsPEBinaryExt, WindowsPEOptionalHeaderType, WindowsPESection, WindowsProcessExt, WindowsRegistryKey, WindowsRegistryValueType, WindowsServiceExt, - X509Certificate, X509V3ExtenstionsType, + X509Certificate, X509V3ExtensionsType, ) from .sdo import ( AttackPattern, Campaign, CourseOfAction, CustomObject, Grouping, Identity, @@ -131,7 +131,7 @@ __all__ = """ UNIXAccountExt, UserAccount, WindowsPEBinaryExt, WindowsPEOptionalHeaderType, WindowsPESection, WindowsProcessExt, WindowsRegistryKey, WindowsRegistryValueType, WindowsServiceExt, - X509Certificate, X509V3ExtenstionsType, + X509Certificate, X509V3ExtensionsType, AttackPattern, Campaign, CourseOfAction, CustomObject, Grouping, Identity, Indicator, Infrastructure, IntrusionSet, Location, Malware, diff --git a/stix2/v21/common.py b/stix2/v21/common.py index c7a9441..311525c 100644 --- a/stix2/v21/common.py +++ b/stix2/v21/common.py @@ -94,7 +94,7 @@ class LanguageContent(_STIXBase21): ('object_modified', TimestampProperty(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()), + ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), ('external_references', ListProperty(ExternalReference)), @@ -156,12 +156,12 @@ class MarkingDefinition(_STIXBase21, _MarkingsMixin): ('id', IDProperty(_type)), ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), - ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), - ('granular_markings', ListProperty(GranularMarking)), ('definition_type', StringProperty(required=True)), ('name', StringProperty()), ('definition', MarkingProperty(required=True)), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), + ('granular_markings', ListProperty(GranularMarking)), ]) def __init__(self, **kwargs): diff --git a/stix2/v21/observables.py b/stix2/v21/observables.py index 55224cd..430afea 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -28,6 +28,7 @@ class Artifact(_Observable): _type = 'artifact' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('mime_type', StringProperty()), ('payload_bin', BinaryProperty()), @@ -35,11 +36,10 @@ class Artifact(_Observable): ('hashes', HashesProperty(spec_version='2.1')), ('encryption_algorithm', StringProperty()), ('decryption_key', StringProperty()), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["hashes", "payload_bin"] @@ -57,15 +57,15 @@ class AutonomousSystem(_Observable): _type = 'autonomous-system' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('number', IntegerProperty(required=True)), ('name', StringProperty()), ('rir', StringProperty()), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["number"] @@ -78,6 +78,7 @@ class Directory(_Observable): _type = 'directory' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('path', StringProperty(required=True)), ('path_enc', StringProperty()), @@ -86,11 +87,10 @@ class Directory(_Observable): ('mtime', TimestampProperty()), ('atime', TimestampProperty()), ('contains_refs', ListProperty(ReferenceProperty(valid_types=['file', 'directory'], spec_version='2.1'))), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["path"] @@ -103,14 +103,14 @@ class DomainName(_Observable): _type = 'domain-name' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ReferenceProperty(valid_types=['ipv4-addr', 'ipv6-addr', 'domain-name'], spec_version='2.1'))), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["value"] @@ -123,15 +123,15 @@ class EmailAddress(_Observable): _type = 'email-addr' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('display_name', StringProperty()), ('belongs_to_ref', ReferenceProperty(valid_types='user-account', spec_version='2.1')), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["value"] @@ -161,6 +161,7 @@ class EmailMessage(_Observable): _type = 'email-message' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('is_multipart', BooleanProperty(required=True)), ('date', TimestampProperty()), @@ -177,11 +178,10 @@ class EmailMessage(_Observable): ('body', StringProperty()), ('body_multipart', ListProperty(EmbeddedObjectProperty(type=EmailMIMEComponent))), ('raw_email_ref', ReferenceProperty(valid_types='artifact', spec_version='2.1')), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["from_ref", "subject", "body"] @@ -345,6 +345,7 @@ class File(_Observable): _type = 'file' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('hashes', HashesProperty(spec_version='2.1')), ('size', IntegerProperty(min=0)), @@ -358,11 +359,10 @@ class File(_Observable): ('parent_directory_ref', ReferenceProperty(valid_types='directory', spec_version='2.1')), ('contains_refs', ListProperty(ReferenceProperty(valid_types=["SCO"], spec_version='2.1'))), ('content_ref', ReferenceProperty(valid_types='artifact', spec_version='2.1')), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["hashes", "name", "parent_directory_ref", "extensions"] @@ -379,15 +379,15 @@ class IPv4Address(_Observable): _type = 'ipv4-addr' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ReferenceProperty(valid_types='mac-addr', spec_version='2.1'))), ('belongs_to_refs', ListProperty(ReferenceProperty(valid_types='autonomous-system', spec_version='2.1'))), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["value"] @@ -400,15 +400,15 @@ class IPv6Address(_Observable): _type = 'ipv6-addr' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), ('resolves_to_refs', ListProperty(ReferenceProperty(valid_types='mac-addr', spec_version='2.1'))), ('belongs_to_refs', ListProperty(ReferenceProperty(valid_types='autonomous-system', spec_version='2.1'))), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["value"] @@ -421,13 +421,13 @@ class MACAddress(_Observable): _type = 'mac-addr' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["value"] @@ -440,13 +440,13 @@ class Mutex(_Observable): _type = 'mutex' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('name', StringProperty(required=True)), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["name"] @@ -505,13 +505,15 @@ class SocketExt(_Extension): ('is_listening', BooleanProperty()), ('options', DictionaryProperty(spec_version='2.1')), ( - '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(min=0)), ('socket_handle', IntegerProperty()), @@ -551,6 +553,7 @@ class NetworkTraffic(_Observable): _type = 'network-traffic' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('start', TimestampProperty()), ('end', TimestampProperty()), @@ -569,11 +572,10 @@ class NetworkTraffic(_Observable): ('dst_payload_ref', ReferenceProperty(valid_types='artifact', spec_version='2.1')), ('encapsulates_refs', ListProperty(ReferenceProperty(valid_types='network-traffic', spec_version='2.1'))), ('encapsulated_by_ref', ReferenceProperty(valid_types='network-traffic', spec_version='2.1')), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["start", "src_ref", "dst_ref", "src_port", "dst_port", "protocols"] @@ -612,12 +614,14 @@ class WindowsProcessExt(_Extension): ('window_title', StringProperty()), ('startup_info', DictionaryProperty(spec_version='2.1')), ( - 'integrity_level', EnumProperty(allowed=[ - "low", - "medium", - "high", - "system", - ]), + 'integrity_level', EnumProperty( + allowed=[ + "low", + "medium", + "high", + "system", + ], + ), ), ]) @@ -634,33 +638,39 @@ class WindowsServiceExt(_Extension): ('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(ReferenceProperty(valid_types='file', spec_version="2.1"))), ( - 'service_type', EnumProperty(allowed=[ - "SERVICE_KERNEL_DRIVER", - "SERVICE_FILE_SYSTEM_DRIVER", - "SERVICE_WIN32_OWN_PROCESS", - "SERVICE_WIN32_SHARE_PROCESS", - ]), + '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_status', EnumProperty( + allowed=[ + "SERVICE_CONTINUE_PENDING", + "SERVICE_PAUSE_PENDING", + "SERVICE_PAUSED", + "SERVICE_RUNNING", + "SERVICE_START_PENDING", + "SERVICE_STOP_PENDING", + "SERVICE_STOPPED", + ], + ), ), ]) @@ -673,6 +683,7 @@ class Process(_Observable): _type = 'process' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('is_hidden', BooleanProperty()), ('pid', IntegerProperty()), @@ -686,11 +697,10 @@ class Process(_Observable): ('image_ref', ReferenceProperty(valid_types='file', spec_version='2.1')), ('parent_ref', ReferenceProperty(valid_types='process', spec_version='2.1')), ('child_refs', ListProperty(ReferenceProperty(valid_types='process', spec_version='2.1'))), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = [] @@ -717,6 +727,7 @@ class Software(_Observable): _type = 'software' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('name', StringProperty(required=True)), ('cpe', StringProperty()), @@ -724,11 +735,10 @@ class Software(_Observable): ('languages', ListProperty(StringProperty)), ('vendor', StringProperty()), ('version', StringProperty()), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["name", "cpe", "swid", "vendor", "version"] @@ -741,13 +751,13 @@ class URL(_Observable): _type = 'url' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('value', StringProperty(required=True)), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["value"] @@ -774,6 +784,7 @@ class UserAccount(_Observable): _type = 'user-account' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('user_id', StringProperty()), ('credential', StringProperty()), @@ -789,11 +800,10 @@ class UserAccount(_Observable): ('credential_last_changed', TimestampProperty()), ('account_first_login', TimestampProperty()), ('account_last_login', TimestampProperty()), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["account_type", "user_id", "account_login"] @@ -808,21 +818,23 @@ class WindowsRegistryValueType(_STIXBase21): ('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", - ]), + '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", + ], + ), ), ]) @@ -835,6 +847,7 @@ class WindowsRegistryKey(_Observable): _type = 'windows-registry-key' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('key', StringProperty()), ('values', ListProperty(EmbeddedObjectProperty(type=WindowsRegistryValueType))), @@ -842,16 +855,15 @@ class WindowsRegistryKey(_Observable): ('modified_time', TimestampProperty()), ('creator_user_ref', ReferenceProperty(valid_types='user-account', spec_version='2.1')), ('number_of_subkeys', IntegerProperty()), - ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), - ('spec_version', StringProperty(fixed='2.1')), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["key", "values"] -class X509V3ExtenstionsType(_STIXBase21): +class X509V3ExtensionsType(_STIXBase21): """For more detailed information on this object's properties, see `the STIX 2.1 specification `__. """ @@ -885,6 +897,7 @@ class X509Certificate(_Observable): _type = 'x509-certificate' _properties = OrderedDict([ ('type', TypeProperty(_type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), ('is_self_signed', BooleanProperty()), ('hashes', HashesProperty(spec_version='2.1')), @@ -898,12 +911,11 @@ class X509Certificate(_Observable): ('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)), - ('spec_version', StringProperty(fixed='2.1')), + ('x509_v3_extensions', EmbeddedObjectProperty(type=X509V3ExtensionsType)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), ('defanged', BooleanProperty(default=lambda: False)), + ('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=_type)), ]) _id_contributing_properties = ["hashes", "serial_number"] @@ -935,12 +947,18 @@ def CustomObservable(type='x-custom-observable', properties=None, id_contrib_pro """ def wrapper(cls): - _properties = list(itertools.chain.from_iterable([ - [('type', TypeProperty(type, spec_version='2.1'))], - [('id', IDProperty(type, spec_version='2.1'))], - properties, - [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))], - ])) + _properties = list( + itertools.chain.from_iterable([ + [('type', TypeProperty(type, spec_version='2.1'))], + [('spec_version', StringProperty(fixed='2.1'))], + [('id', IDProperty(type, spec_version='2.1'))], + properties, + [('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1')))], + [('granular_markings', ListProperty(GranularMarking))], + [('defanged', BooleanProperty(default=lambda: False))], + [('extensions', ExtensionsProperty(spec_version='2.1', enclosing_type=type))], + ]), + ) return _custom_observable_builder(cls, type, _properties, '2.1', _Observable, id_contrib_props) return wrapper diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py index cae8dca..7e0e123 100644 --- a/stix2/v21/sdo.py +++ b/stix2/v21/sdo.py @@ -122,9 +122,13 @@ class Grouping(_DomainObject): ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), + ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), - ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), + ('name', StringProperty()), + ('description', StringProperty()), + ('context', StringProperty(required=True)), + ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), ('confidence', IntegerProperty()), @@ -132,10 +136,6 @@ class Grouping(_DomainObject): ('external_references', ListProperty(ExternalReference)), ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), ('granular_markings', ListProperty(GranularMarking)), - ('name', StringProperty()), - ('description', StringProperty()), - ('context', StringProperty(required=True)), - ('object_refs', ListProperty(ReferenceProperty(valid_types=["SCO", "SDO", "SRO"], spec_version='2.1'), required=True)), ]) @@ -240,13 +240,6 @@ class Infrastructure(_DomainObject): ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), - ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty)), - ('confidence', IntegerProperty()), - ('lang', StringProperty()), - ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), - ('granular_markings', ListProperty(GranularMarking)), ('name', StringProperty(required=True)), ('description', StringProperty()), ('infrastructure_types', ListProperty(StringProperty)), @@ -254,6 +247,13 @@ class Infrastructure(_DomainObject): ('kill_chain_phases', ListProperty(KillChainPhase)), ('first_seen', TimestampProperty()), ('last_seen', TimestampProperty()), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), + ('granular_markings', ListProperty(GranularMarking)), ]) def _check_object_constraints(self): @@ -478,16 +478,9 @@ class MalwareAnalysis(_DomainObject): ('type', TypeProperty(_type, spec_version='2.1')), ('spec_version', StringProperty(fixed='2.1')), ('id', IDProperty(_type, spec_version='2.1')), + ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), - ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('revoked', BooleanProperty(default=lambda: False)), - ('labels', ListProperty(StringProperty)), - ('confidence', IntegerProperty()), - ('lang', StringProperty()), - ('external_references', ListProperty(ExternalReference)), - ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), - ('granular_markings', ListProperty(GranularMarking)), ('product', StringProperty(required=True)), ('version', StringProperty()), ('host_vm_ref', ReferenceProperty(valid_types='software', spec_version='2.1')), @@ -503,7 +496,14 @@ class MalwareAnalysis(_DomainObject): ('result_name', StringProperty()), ('result', StringProperty()), ('analysis_sco_refs', ListProperty(ReferenceProperty(valid_types="SCO", spec_version='2.1'))), - ('sample_ref', ReferenceProperty(valid_types="SCO", spec_version="2.1")), + ('sample_ref', ReferenceProperty(valid_types="SCO", spec_version='2.1')), + ('revoked', BooleanProperty(default=lambda: False)), + ('labels', ListProperty(StringProperty)), + ('confidence', IntegerProperty()), + ('lang', StringProperty()), + ('external_references', ListProperty(ExternalReference)), + ('object_marking_refs', ListProperty(ReferenceProperty(valid_types='marking-definition', spec_version='2.1'))), + ('granular_markings', ListProperty(GranularMarking)), ]) def _check_object_constraints(self): @@ -794,27 +794,29 @@ def CustomObject(type='x-custom-type', properties=None): """ def wrapper(cls): - _properties = list(itertools.chain.from_iterable([ - [ - ('type', TypeProperty(type, spec_version='2.1')), - ('spec_version', StringProperty(fixed='2.1')), - ('id', IDProperty(type, spec_version='2.1')), - ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), - ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), - ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), - ], - [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(valid_types='marking-definition', spec_version='2.1'))), - ('granular_markings', ListProperty(GranularMarking)), - ], - sorted([x for x in properties if x[0].startswith('x_')], key=lambda x: x[0]), - ])) + _properties = list( + itertools.chain.from_iterable([ + [ + ('type', TypeProperty(type, spec_version='2.1')), + ('spec_version', StringProperty(fixed='2.1')), + ('id', IDProperty(type, spec_version='2.1')), + ('created_by_ref', ReferenceProperty(valid_types='identity', spec_version='2.1')), + ('created', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond', precision_constraint='min')), + ], + [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(valid_types='marking-definition', spec_version='2.1'))), + ('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', _DomainObject) return wrapper diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py index 2047623..ffd2de2 100644 --- a/stix2/v21/sro.py +++ b/stix2/v21/sro.py @@ -89,7 +89,7 @@ class Sighting(_RelationshipObject): ('count', IntegerProperty(min=0, max=999999999)), ('sighting_of_ref', ReferenceProperty(valid_types="SDO", spec_version='2.1', required=True)), ('observed_data_refs', ListProperty(ReferenceProperty(valid_types='observed-data', spec_version='2.1'))), - ('where_sighted_refs', ListProperty(ReferenceProperty(valid_types='identity', spec_version='2.1'))), + ('where_sighted_refs', ListProperty(ReferenceProperty(valid_types=['identity', 'location'], spec_version='2.1'))), ('summary', BooleanProperty()), ('revoked', BooleanProperty(default=lambda: False)), ('labels', ListProperty(StringProperty)), diff --git a/stix2/version.py b/stix2/version.py index 0309ae2..9ddc874 100644 --- a/stix2/version.py +++ b/stix2/version.py @@ -1 +1,3 @@ -__version__ = "2.0.2" +__version__ = "2.1.0" + +DEFAULT_VERSION = '2.1' # Default version will always be the latest STIX 2.X version diff --git a/stix2/versioning.py b/stix2/versioning.py index 90d1ac9..01affe9 100644 --- a/stix2/versioning.py +++ b/stix2/versioning.py @@ -1,15 +1,17 @@ """STIX2 core versioning methods.""" +from collections.abc import Mapping import copy import datetime as dt import itertools import uuid -import six -from six.moves.collections_abc import Mapping - import stix2.base -from stix2.utils import get_timestamp, parse_into_datetime +import stix2.registry +from stix2.utils import ( + detect_spec_version, get_timestamp, is_sco, is_sdo, is_sro, + parse_into_datetime, +) import stix2.v20 from .exceptions import ( @@ -73,58 +75,47 @@ def _is_versionable(data): """ is_versionable = False - is_21 = False - stix_vid = None + stix_version = None if isinstance(data, Mapping): # First, determine spec version. It's easy for our stix2 objects; more # work for dicts. - is_21 = False - if isinstance(data, stix2.base._STIXBase) and \ - not isinstance(data, stix2.v20._STIXBase20): - # (is_21 means 2.1 or later; try not to be 2.1-specific) - is_21 = True + if isinstance(data, stix2.v20._STIXBase20): + stix_version = "2.0" + elif isinstance(data, stix2.v21._STIXBase21): + stix_version = "2.1" elif isinstance(data, dict): - stix_vid = stix2.parsing._detect_spec_version(data) - is_21 = stix_vid != "v20" + stix_version = detect_spec_version(data) # Then, determine versionability. - if six.PY2: - # dumb python2 compatibility: map.keys() returns a list, not a set! - # six.viewkeys() compatibility function uses dict.viewkeys() on - # python2, which is not a Mapping mixin method, so that doesn't - # work either (for our stix2 objects). - keys = set(data) - else: - keys = data.keys() - # This should be sufficient for STIX objects; maybe we get lucky with # dicts here but probably not. - if keys >= _VERSIONING_PROPERTIES: + if data.keys() >= _VERSIONING_PROPERTIES: is_versionable = True # Tougher to handle dicts. We need to consider STIX version, map to a # registered class, and from that get a more complete picture of its # properties. elif isinstance(data, dict): - class_maps = stix2.parsing.STIX2_OBJ_MAPS[stix_vid] obj_type = data["type"] - if obj_type in class_maps["objects"]: + if is_sdo(obj_type, stix_version) or is_sro(obj_type, stix_version): # Should we bother checking properties for SDOs/SROs? # They were designed to be versionable. is_versionable = True - elif obj_type in class_maps["observables"]: + elif is_sco(obj_type, stix_version): # but do check SCOs - cls = class_maps["observables"][obj_type] + cls = stix2.registry.class_for_type( + obj_type, stix_version, "observables", + ) is_versionable = _VERSIONING_PROPERTIES.issubset( cls._properties, ) - return is_versionable, is_21 + return is_versionable, stix_version def new_version(data, allow_custom=None, **kwargs): @@ -143,7 +134,7 @@ def new_version(data, allow_custom=None, **kwargs): :return: The new object. """ - is_versionable, is_21 = _is_versionable(data) + is_versionable, stix_version = _is_versionable(data) if not is_versionable: raise ValueError( @@ -164,10 +155,17 @@ def new_version(data, allow_custom=None, **kwargs): # probably were). That would imply an ID change, which is not allowed # across versions. sco_locked_props = [] - if is_21 and isinstance(data, stix2.base._Observable): + if is_sco(data, "2.1"): uuid_ = uuid.UUID(data["id"][-36:]) if uuid_.variant == uuid.RFC_4122 and uuid_.version == 5: - sco_locked_props = data._id_contributing_properties + if isinstance(data, stix2.base._Observable): + cls = data.__class__ + else: + cls = stix2.registry.class_for_type( + data["type"], stix_version, "observables", + ) + + sco_locked_props = cls._id_contributing_properties unchangable_properties = set() for prop in itertools.chain(STIX_UNMOD_PROPERTIES, sco_locked_props): @@ -178,7 +176,7 @@ def new_version(data, allow_custom=None, **kwargs): # Different versioning precision rules in STIX 2.0 vs 2.1, so we need # to know which rules to apply. - precision_constraint = "min" if is_21 else "exact" + precision_constraint = "min" if stix_version == "2.1" else "exact" cls = type(data) if 'modified' not in kwargs: @@ -188,7 +186,9 @@ def new_version(data, allow_custom=None, **kwargs): ) new_modified = get_timestamp() - new_modified = _fudge_modified(old_modified, new_modified, is_21) + new_modified = _fudge_modified( + old_modified, new_modified, stix_version == "2.1", + ) kwargs['modified'] = new_modified diff --git a/stix2/workbench.py b/stix2/workbench.py index 3724bdb..8be546f 100644 --- a/stix2/workbench.py +++ b/stix2/workbench.py @@ -21,7 +21,7 @@ """ import functools -import stix2 + from . import AttackPattern as _AttackPattern from . import Campaign as _Campaign from . import CourseOfAction as _CourseOfAction @@ -34,34 +34,37 @@ from . import Location as _Location from . import Malware as _Malware from . import MalwareAnalysis as _MalwareAnalysis from . import Note as _Note +from . import OBJ_MAP from . import ObservedData as _ObservedData from . import Opinion as _Opinion from . import Report as _Report from . import ThreatActor as _ThreatActor from . import Tool as _Tool from . import Vulnerability as _Vulnerability -from . import ( # noqa: F401 +from .version import DEFAULT_VERSION + +from . import ( # noqa: F401 isort:skip AlternateDataStream, ArchiveExt, Artifact, AutonomousSystem, Bundle, CustomExtension, CustomMarking, CustomObservable, Directory, DomainName, EmailAddress, EmailMessage, EmailMIMEComponent, Environment, ExternalReference, File, FileSystemSource, Filter, GranularMarking, HTTPRequestExt, - ICMPExt, IPv4Address, IPv6Address, KillChainPhase, LanguageContent, MACAddress, - MarkingDefinition, MemoryStore, Mutex, NetworkTraffic, NTFSExt, - parse_observable, PDFExt, Process, RasterImageExt, Relationship, + ICMPExt, IPv4Address, IPv6Address, KillChainPhase, LanguageContent, + 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 + X509V3ExtensionsType, ) -from .datastore.filters import FilterSet +from .datastore.filters import FilterSet # isort:skip # Enable some adaptation to the current default supported STIX version. -_STIX_VID = "v" + stix2.DEFAULT_VERSION.replace(".", "") +_STIX_VID = "v" + DEFAULT_VERSION.replace(".", "") # Use an implicit MemoryStore @@ -161,7 +164,7 @@ def _setup_workbench(): # Add our new "class" to this module's globals and to the library-wide # mapping. This allows parse() to use the wrapped classes. globals()[obj_type.__name__] = factory_func - stix2.OBJ_MAP[obj_type._type] = factory_func + OBJ_MAP[obj_type._type] = factory_func _setup_workbench() diff --git a/tox.ini b/tox.ini index f938f8d..90f5cfc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py36,py37,py38,style,isort-check,packaging +envlist = py36,py37,py38,py39,packaging,pre-commit-check [testenv] deps = @@ -15,33 +15,24 @@ deps = commands = python -m pytest --cov=stix2 stix2/test/ --cov-report term-missing -W ignore::stix2.exceptions.STIXDeprecationWarning -passenv = CI TRAVIS TRAVIS_* - -[testenv:style] -deps = - flake8 -commands = - flake8 - -[flake8] -max-line-length = 160 - -[testenv:isort-check] -deps = isort -commands = - isort stix2 examples --df - isort stix2 examples -c +passenv = GITHUB_* [testenv:packaging] deps = twine commands = - python setup.py bdist_wheel --universal + python setup.py sdist bdist_wheel --universal twine check dist/* -[travis] +[testenv:pre-commit-check] +deps = + pre-commit +commands = + pre-commit run --all-files + +[gh-actions] python = - 3.5: py35 3.6: py36 3.7: py37 - 3.8: py38, style, packaging + 3.8: py38 + 3.9: py39, packaging, pre-commit-check