diff --git a/docs/api/equivalence/patterns/compare/stix2.equivalence.patterns.compare.comparison.rst b/docs/api/equivalence/patterns/compare/stix2.equivalence.patterns.compare.comparison.rst new file mode 100644 index 0000000..8e53da7 --- /dev/null +++ b/docs/api/equivalence/patterns/compare/stix2.equivalence.patterns.compare.comparison.rst @@ -0,0 +1,5 @@ +comparison +============== + +.. automodule:: stix2.equivalence.patterns.compare.comparison + :members: diff --git a/docs/api/equivalence/patterns/compare/stix2.equivalence.patterns.compare.observation.rst b/docs/api/equivalence/patterns/compare/stix2.equivalence.patterns.compare.observation.rst new file mode 100644 index 0000000..1abd64e --- /dev/null +++ b/docs/api/equivalence/patterns/compare/stix2.equivalence.patterns.compare.observation.rst @@ -0,0 +1,5 @@ +observation +============== + +.. automodule:: stix2.equivalence.patterns.compare.observation + :members: diff --git a/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.comparison.rst b/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.comparison.rst new file mode 100644 index 0000000..2cf8388 --- /dev/null +++ b/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.comparison.rst @@ -0,0 +1,5 @@ +comparison +============== + +.. automodule:: stix2.equivalence.patterns.transform.comparison + :members: diff --git a/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.observation.rst b/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.observation.rst new file mode 100644 index 0000000..1815e7e --- /dev/null +++ b/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.observation.rst @@ -0,0 +1,5 @@ +observation +============== + +.. automodule:: stix2.equivalence.patterns.transform.observation + :members: diff --git a/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.specials.rst b/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.specials.rst new file mode 100644 index 0000000..7930ae2 --- /dev/null +++ b/docs/api/equivalence/patterns/transform/stix2.equivalence.patterns.transform.specials.rst @@ -0,0 +1,5 @@ +specials +============== + +.. automodule:: stix2.equivalence.patterns.transform.specials + :members: diff --git a/docs/api/equivalence/stix2.equivalence.patterns.rst b/docs/api/equivalence/stix2.equivalence.patterns.rst new file mode 100644 index 0000000..32377f1 --- /dev/null +++ b/docs/api/equivalence/stix2.equivalence.patterns.rst @@ -0,0 +1,5 @@ +patterns +============== + +.. automodule:: stix2.equivalence.patterns + :members: diff --git a/docs/api/stix2.equivalence.rst b/docs/api/stix2.equivalence.rst new file mode 100644 index 0000000..b886fc7 --- /dev/null +++ b/docs/api/stix2.equivalence.rst @@ -0,0 +1,5 @@ +equivalence +============== + +.. automodule:: stix2.equivalence + :members: diff --git a/stix2/__init__.py b/stix2/__init__.py index 97790aa..72fb29b 100644 --- a/stix2/__init__.py +++ b/stix2/__init__.py @@ -6,6 +6,7 @@ confidence datastore environment + equivalence exceptions markings parsing diff --git a/stix2/datastore/taxii.py b/stix2/datastore/taxii.py index 063fabd..41d1e54 100644 --- a/stix2/datastore/taxii.py +++ b/stix2/datastore/taxii.py @@ -210,7 +210,7 @@ class TAXIICollectionSource(DataSource): if len(stix_obj): stix_obj = parse(stix_obj[0], allow_custom=self.allow_custom, version=version) - if stix_obj.id != stix_id: + if stix_obj['id'] != stix_id: # check - was added to handle erroneous TAXII servers stix_obj = None else: @@ -246,7 +246,7 @@ class TAXIICollectionSource(DataSource): all_data = [parse(stix_obj, allow_custom=self.allow_custom, version=version) for stix_obj in all_data] # check - was added to handle erroneous TAXII servers - all_data_clean = [stix_obj for stix_obj in all_data if stix_obj.id == stix_id] + all_data_clean = [stix_obj for stix_obj in all_data if stix_obj['id'] == stix_id] return all_data_clean diff --git a/stix2/equivalence/__init__.py b/stix2/equivalence/__init__.py index e69de29..c67e649 100644 --- a/stix2/equivalence/__init__.py +++ b/stix2/equivalence/__init__.py @@ -0,0 +1,9 @@ +"""Python APIs for STIX 2 Semantic Equivalence. + +.. autosummary:: + :toctree: equivalence + + patterns + +| +""" diff --git a/stix2/equivalence/patterns/__init__.py b/stix2/equivalence/patterns/__init__.py index c792574..85ec9ab 100644 --- a/stix2/equivalence/patterns/__init__.py +++ b/stix2/equivalence/patterns/__init__.py @@ -1,3 +1,14 @@ +"""Python APIs for STIX 2 Pattern Semantic Equivalence. + +.. autosummary:: + :toctree: patterns + + compare + transform + +| +""" + import stix2 from stix2.equivalence.patterns.compare.observation import ( observation_expression_cmp, diff --git a/stix2/equivalence/patterns/compare/__init__.py b/stix2/equivalence/patterns/compare/__init__.py index e4bcc8f..e9d7ec9 100644 --- a/stix2/equivalence/patterns/compare/__init__.py +++ b/stix2/equivalence/patterns/compare/__init__.py @@ -1,5 +1,13 @@ """ Some generic comparison utility functions. + +.. autosummary:: + :toctree: compare + + comparison + observation + +| """ diff --git a/stix2/equivalence/patterns/transform/__init__.py b/stix2/equivalence/patterns/transform/__init__.py index 84a993c..6e2b116 100644 --- a/stix2/equivalence/patterns/transform/__init__.py +++ b/stix2/equivalence/patterns/transform/__init__.py @@ -1,5 +1,14 @@ """ Generic AST transformation classes. + +.. autosummary:: + :toctree: transform + + comparison + observation + specials + +| """ diff --git a/stix2/patterns.py b/stix2/patterns.py index bbee7ac..ce07637 100644 --- a/stix2/patterns.py +++ b/stix2/patterns.py @@ -227,7 +227,7 @@ def make_constant(value): return value try: - return parse_into_datetime(value) + return TimestampConstant(value) except (ValueError, TypeError): pass @@ -366,7 +366,7 @@ class _ComparisonExpression(_PatternExpression): else: self.rhs = make_constant(rhs) self.negated = negated - self.root_type = self.lhs.object_type_name + self.root_types = {self.lhs.object_type_name} def __str__(self): if self.negated: @@ -506,15 +506,17 @@ class _BooleanExpression(_PatternExpression): """ def __init__(self, operator, operands): self.operator = operator - self.operands = [] + self.operands = list(operands) for arg in operands: - if not hasattr(self, "root_type"): - self.root_type = arg.root_type - elif self.root_type and (self.root_type != arg.root_type) and operator == "AND": - raise ValueError("All operands to an 'AND' expression must have the same object type") - elif self.root_type and (self.root_type != arg.root_type): - self.root_type = None - self.operands.append(arg) + if not hasattr(self, "root_types"): + self.root_types = arg.root_types + elif operator == "AND": + self.root_types &= arg.root_types + else: + self.root_types |= arg.root_types + + if not self.root_types: + raise ValueError("All operands to an 'AND' expression must be satisfiable with the same object type") def __str__(self): sub_exprs = [] @@ -613,8 +615,8 @@ class ParentheticalExpression(_PatternExpression): """ def __init__(self, exp): self.expression = exp - if hasattr(exp, "root_type"): - self.root_type = exp.root_type + if hasattr(exp, "root_types"): + self.root_types = exp.root_types def __str__(self): return "(%s)" % self.expression diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py index fa9000e..526fe97 100644 --- a/stix2/test/v20/test_pattern_expressions.py +++ b/stix2/test/v20/test_pattern_expressions.py @@ -1,9 +1,11 @@ import datetime import pytest +import pytz import stix2 from stix2.pattern_visitor import create_pattern_object +import stix2.utils def test_create_comparison_expression(): @@ -482,6 +484,44 @@ def test_invalid_startstop_qualifier(): ) +@pytest.mark.parametrize( + "input_, expected_class, expected_value", [ + (1, stix2.patterns.IntegerConstant, 1), + (1.5, stix2.patterns.FloatConstant, 1.5), + ("abc", stix2.patterns.StringConstant, "abc"), + (True, stix2.patterns.BooleanConstant, True), + ( + "2001-02-10T21:36:15Z", stix2.patterns.TimestampConstant, + stix2.utils.STIXdatetime(2001, 2, 10, 21, 36, 15, tzinfo=pytz.utc), + ), + ( + datetime.datetime(2001, 2, 10, 21, 36, 15, tzinfo=pytz.utc), + stix2.patterns.TimestampConstant, + stix2.utils.STIXdatetime(2001, 2, 10, 21, 36, 15, tzinfo=pytz.utc), + ), + ], +) +def test_make_constant_simple(input_, expected_class, expected_value): + const = stix2.patterns.make_constant(input_) + + assert isinstance(const, expected_class) + assert const.value == expected_value + + +def test_make_constant_list(): + list_const = stix2.patterns.make_constant([1, 2, 3]) + + assert isinstance(list_const, stix2.patterns.ListConstant) + assert all( + isinstance(elt, stix2.patterns.IntegerConstant) + for elt in list_const.value + ) + assert all( + int_const.value == test_elt + for int_const, test_elt in zip(list_const.value, [1, 2, 3]) + ) + + def test_make_constant_already_a_constant(): str_const = stix2.StringConstant('Foo') result = stix2.patterns.make_constant(str_const) diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py index ac6a439..4f365d7 100644 --- a/stix2/test/v21/test_pattern_expressions.py +++ b/stix2/test/v21/test_pattern_expressions.py @@ -1,10 +1,12 @@ import datetime import pytest +import pytz from stix2patterns.exceptions import ParseException import stix2 from stix2.pattern_visitor import create_pattern_object +import stix2.utils def test_create_comparison_expression(): @@ -362,7 +364,7 @@ def test_parsing_or_observable_expression(): assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] OR [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul']" # noqa -def test_invalid_and_observable_expression(): +def test_invalid_and_comparison_expression(): with pytest.raises(ValueError): stix2.AndBooleanExpression([ stix2.EqualityComparisonExpression( @@ -376,6 +378,33 @@ def test_invalid_and_observable_expression(): ]) +@pytest.mark.parametrize( + "pattern, root_types", [ + ("[a:a=1 AND a:b=1]", {"a"}), + ("[a:a=1 AND a:b=1 OR c:d=1]", {"a", "c"}), + ("[a:a=1 AND (a:b=1 OR c:d=1)]", {"a"}), + ("[(a:a=1 OR b:a=1) AND (b:a=1 OR c:c=1)]", {"b"}), + ("[(a:a=1 AND a:b=1) OR (b:a=1 AND b:c=1)]", {"a", "b"}), + ], +) +def test_comparison_expression_root_types(pattern, root_types): + ast = create_pattern_object(pattern) + assert ast.operand.root_types == root_types + + +@pytest.mark.parametrize( + "pattern", [ + "[a:b=1 AND b:c=1]", + "[a:b=1 AND (b:c=1 OR c:d=1)]", + "[(a:b=1 OR b:c=1) AND (c:d=1 OR d:e=1)]", + "[(a:b=1 AND b:c=1) OR (b:c=1 AND c:d=1)]", + ], +) +def test_comparison_expression_root_types_error(pattern): + with pytest.raises(ValueError): + create_pattern_object(pattern) + + def test_hex(): exp_and = stix2.AndBooleanExpression([ stix2.EqualityComparisonExpression( @@ -603,6 +632,44 @@ def test_invalid_startstop_qualifier(): ) +@pytest.mark.parametrize( + "input_, expected_class, expected_value", [ + (1, stix2.patterns.IntegerConstant, 1), + (1.5, stix2.patterns.FloatConstant, 1.5), + ("abc", stix2.patterns.StringConstant, "abc"), + (True, stix2.patterns.BooleanConstant, True), + ( + "2001-02-10T21:36:15Z", stix2.patterns.TimestampConstant, + stix2.utils.STIXdatetime(2001, 2, 10, 21, 36, 15, tzinfo=pytz.utc), + ), + ( + datetime.datetime(2001, 2, 10, 21, 36, 15, tzinfo=pytz.utc), + stix2.patterns.TimestampConstant, + stix2.utils.STIXdatetime(2001, 2, 10, 21, 36, 15, tzinfo=pytz.utc), + ), + ], +) +def test_make_constant_simple(input_, expected_class, expected_value): + const = stix2.patterns.make_constant(input_) + + assert isinstance(const, expected_class) + assert const.value == expected_value + + +def test_make_constant_list(): + list_const = stix2.patterns.make_constant([1, 2, 3]) + + assert isinstance(list_const, stix2.patterns.ListConstant) + assert all( + isinstance(elt, stix2.patterns.IntegerConstant) + for elt in list_const.value + ) + assert all( + int_const.value == test_elt + for int_const, test_elt in zip(list_const.value, [1, 2, 3]) + ) + + def test_make_constant_already_a_constant(): str_const = stix2.StringConstant('Foo') result = stix2.patterns.make_constant(str_const)