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/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/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..4b80dc2 100644 --- a/stix2/equivalence/pattern/__init__.py +++ b/stix2/equivalence/pattern/__init__.py @@ -30,7 +30,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 @@ -64,11 +65,14 @@ def equivalent_patterns(pattern1, pattern2, stix_version=stix2.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( pattern1, version=stix_version, @@ -96,12 +100,14 @@ 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, version=stix_version, 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..6db1055 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): @@ -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 diff --git a/stix2/equivalence/pattern/transform/observation.py b/stix2/equivalence/pattern/transform/observation.py index a8982cf..8e2a4d2 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([ @@ -160,8 +163,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)) @@ -292,10 +298,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 +344,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) 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/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_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/v20/common.py b/stix2/v20/common.py index f2a371e..720f14f 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/v21/observables.py b/stix2/v21/observables.py index cf23cfd..84ac136 100644 --- a/stix2/v21/observables.py +++ b/stix2/v21/observables.py @@ -937,6 +937,7 @@ 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'))], + [('spec_version', StringProperty(fixed='2.1'))], [('id', IDProperty(type, spec_version='2.1'))], [('spec_version', StringProperty(fixed='2.1'))], properties, diff --git a/stix2/v21/sro.py b/stix2/v21/sro.py index a30e319..9a0b9c6 100644 --- a/stix2/v21/sro.py +++ b/stix2/v21/sro.py @@ -88,7 +88,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..9aa3f90 100644 --- a/stix2/version.py +++ b/stix2/version.py @@ -1 +1 @@ -__version__ = "2.0.2" +__version__ = "2.1.0"