Merge pull request #154 from oasis-open/code-coverage
Increase code coverage, fix patterning bugsstix2.0
commit
f61bc5e5ee
|
@ -31,11 +31,12 @@ from .environment import Environment, ObjectFactory
|
|||
from .markings import (add_markings, clear_markings, get_markings, is_marked,
|
||||
remove_markings, set_markings)
|
||||
from .patterns import (AndBooleanExpression, AndObservationExpression,
|
||||
BasicObjectPathComponent, EqualityComparisonExpression,
|
||||
BasicObjectPathComponent, BinaryConstant,
|
||||
BooleanConstant, EqualityComparisonExpression,
|
||||
FloatConstant, FollowedByObservationExpression,
|
||||
GreaterThanComparisonExpression,
|
||||
GreaterThanEqualComparisonExpression, HashConstant,
|
||||
HexConstant, IntegerConstant,
|
||||
HexConstant, InComparisonExpression, IntegerConstant,
|
||||
IsSubsetComparisonExpression,
|
||||
IsSupersetComparisonExpression,
|
||||
LessThanComparisonExpression,
|
||||
|
|
|
@ -114,16 +114,10 @@ class Environment(DataStoreMixin):
|
|||
create.__doc__ = ObjectFactory.create.__doc__
|
||||
|
||||
def add_filters(self, *args, **kwargs):
|
||||
try:
|
||||
return self.source.filters.update(*args, **kwargs)
|
||||
except AttributeError:
|
||||
raise AttributeError('Environment has no data source')
|
||||
return self.source.filters.update(*args, **kwargs)
|
||||
|
||||
def add_filter(self, *args, **kwargs):
|
||||
try:
|
||||
return self.source.filters.add(*args, **kwargs)
|
||||
except AttributeError:
|
||||
raise AttributeError('Environment has no data source')
|
||||
return self.source.filters.add(*args, **kwargs)
|
||||
|
||||
def parse(self, *args, **kwargs):
|
||||
return _parse(*args, **kwargs)
|
||||
|
|
|
@ -3,8 +3,11 @@
|
|||
|
||||
import base64
|
||||
import binascii
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from .utils import parse_into_datetime
|
||||
|
||||
|
||||
def escape_quotes_and_backslashes(s):
|
||||
return s.replace(u'\\', u'\\\\').replace(u"'", u"\\'")
|
||||
|
@ -24,10 +27,13 @@ class StringConstant(_Constant):
|
|||
|
||||
class TimestampConstant(_Constant):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
try:
|
||||
self.value = parse_into_datetime(value)
|
||||
except Exception:
|
||||
raise ValueError("must be a datetime object or timestamp string.")
|
||||
|
||||
def __str__(self):
|
||||
return "t'%s'" % escape_quotes_and_backslashes(self.value)
|
||||
return "t%s" % repr(self.value)
|
||||
|
||||
|
||||
class IntegerConstant(_Constant):
|
||||
|
@ -46,7 +52,7 @@ class FloatConstant(_Constant):
|
|||
try:
|
||||
self.value = float(value)
|
||||
except Exception:
|
||||
raise ValueError("must be an float.")
|
||||
raise ValueError("must be a float.")
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.value
|
||||
|
@ -56,24 +62,29 @@ class BooleanConstant(_Constant):
|
|||
def __init__(self, value):
|
||||
if isinstance(value, bool):
|
||||
self.value = value
|
||||
return
|
||||
|
||||
trues = ['true', 't']
|
||||
falses = ['false', 'f']
|
||||
try:
|
||||
if value.lower() in trues:
|
||||
self.value = True
|
||||
if value.lower() in falses:
|
||||
return
|
||||
elif value.lower() in falses:
|
||||
self.value = False
|
||||
return
|
||||
except AttributeError:
|
||||
if value == 1:
|
||||
self.value = True
|
||||
if value == 0:
|
||||
return
|
||||
elif value == 0:
|
||||
self.value = False
|
||||
return
|
||||
|
||||
raise ValueError("must be a boolean value.")
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % self.value
|
||||
return str(self.value).lower()
|
||||
|
||||
|
||||
_HASH_REGEX = {
|
||||
|
@ -132,20 +143,25 @@ class ListConstant(_Constant):
|
|||
self.value = values
|
||||
|
||||
def __str__(self):
|
||||
return "(" + ", ".join([("%s" % x) for x in self.value]) + ")"
|
||||
return "(" + ", ".join([("%s" % make_constant(x)) for x in self.value]) + ")"
|
||||
|
||||
|
||||
def make_constant(value):
|
||||
try:
|
||||
return parse_into_datetime(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if isinstance(value, str):
|
||||
return StringConstant(value)
|
||||
elif isinstance(value, bool):
|
||||
return BooleanConstant(value)
|
||||
elif isinstance(value, int):
|
||||
return IntegerConstant(value)
|
||||
elif isinstance(value, float):
|
||||
return FloatConstant(value)
|
||||
elif isinstance(value, list):
|
||||
return ListConstant(value)
|
||||
elif isinstance(value, bool):
|
||||
return BooleanConstant(value)
|
||||
else:
|
||||
raise ValueError("Unable to create a constant from %s" % value)
|
||||
|
||||
|
@ -210,15 +226,12 @@ class ObjectPath(object):
|
|||
|
||||
|
||||
class _PatternExpression(object):
|
||||
|
||||
@staticmethod
|
||||
def escape_quotes_and_backslashes(s):
|
||||
return s.replace(u'\\', u'\\\\').replace(u"'", u"\\'")
|
||||
pass
|
||||
|
||||
|
||||
class _ComparisonExpression(_PatternExpression):
|
||||
def __init__(self, operator, lhs, rhs, negated=False):
|
||||
if operator == "=" and isinstance(rhs, ListConstant):
|
||||
if operator == "=" and isinstance(rhs, (ListConstant, list)):
|
||||
self.operator = "IN"
|
||||
else:
|
||||
self.operator = operator
|
||||
|
@ -234,13 +247,6 @@ class _ComparisonExpression(_PatternExpression):
|
|||
self.root_type = self.lhs.object_type_name
|
||||
|
||||
def __str__(self):
|
||||
# if isinstance(self.rhs, list):
|
||||
# final_rhs = []
|
||||
# for r in self.rhs:
|
||||
# final_rhs.append("'" + self.escape_quotes_and_backslashes("%s" % r) + "'")
|
||||
# rhs_string = "(" + ", ".join(final_rhs) + ")"
|
||||
# else:
|
||||
# rhs_string = self.rhs
|
||||
if self.negated:
|
||||
return "%s NOT %s %s" % (self.lhs, self.operator, self.rhs)
|
||||
else:
|
||||
|
@ -383,7 +389,7 @@ class RepeatQualifier(_ExpressionQualifier):
|
|||
elif isinstance(times_to_repeat, int):
|
||||
self.times_to_repeat = IntegerConstant(times_to_repeat)
|
||||
else:
|
||||
raise ValueError("%s is not a valid argument for a Within Qualifier" % times_to_repeat)
|
||||
raise ValueError("%s is not a valid argument for a Repeat Qualifier" % times_to_repeat)
|
||||
|
||||
def __str__(self):
|
||||
return "REPEATS %s TIMES" % self.times_to_repeat
|
||||
|
@ -404,18 +410,18 @@ class WithinQualifier(_ExpressionQualifier):
|
|||
|
||||
class StartStopQualifier(_ExpressionQualifier):
|
||||
def __init__(self, start_time, stop_time):
|
||||
if isinstance(start_time, IntegerConstant):
|
||||
if isinstance(start_time, TimestampConstant):
|
||||
self.start_time = start_time
|
||||
elif isinstance(start_time, int):
|
||||
self.start_time = IntegerConstant(start_time)
|
||||
elif isinstance(start_time, datetime.date):
|
||||
self.start_time = TimestampConstant(start_time)
|
||||
else:
|
||||
raise ValueError("%s is not a valid argument for a Within Qualifier" % start_time)
|
||||
if isinstance(stop_time, IntegerConstant):
|
||||
raise ValueError("%s is not a valid argument for a Start/Stop Qualifier" % start_time)
|
||||
if isinstance(stop_time, TimestampConstant):
|
||||
self.stop_time = stop_time
|
||||
elif isinstance(stop_time, int):
|
||||
self.stop_time = IntegerConstant(stop_time)
|
||||
elif isinstance(stop_time, datetime.date):
|
||||
self.stop_time = TimestampConstant(stop_time)
|
||||
else:
|
||||
raise ValueError("%s is not a valid argument for a Within Qualifier" % stop_time)
|
||||
raise ValueError("%s is not a valid argument for a Start/Stop Qualifier" % stop_time)
|
||||
|
||||
def __str__(self):
|
||||
return "START %s STOP %s" % (self.start_time, self.stop_time)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import pytest
|
||||
|
||||
from stix2 import TLP_RED, Malware, markings
|
||||
from stix2.exceptions import MarkingNotFoundError
|
||||
|
||||
from .constants import MALWARE_MORE_KWARGS as MALWARE_KWARGS_CONST
|
||||
from .constants import MARKING_IDS
|
||||
|
@ -546,6 +547,20 @@ def test_remove_marking_bad_selector():
|
|||
markings.remove_markings(before, ["marking-definition--1", "marking-definition--2"], ["title"])
|
||||
|
||||
|
||||
def test_remove_marking_not_present():
|
||||
before = Malware(
|
||||
granular_markings=[
|
||||
{
|
||||
"selectors": ["description"],
|
||||
"marking_ref": MARKING_IDS[0]
|
||||
}
|
||||
],
|
||||
**MALWARE_KWARGS
|
||||
)
|
||||
with pytest.raises(MarkingNotFoundError):
|
||||
markings.remove_markings(before, [MARKING_IDS[1]], ["description"])
|
||||
|
||||
|
||||
IS_MARKED_TEST_DATA = [
|
||||
Malware(
|
||||
granular_markings=[
|
||||
|
@ -1044,3 +1059,10 @@ def test_clear_marking_bad_selector(data, selector):
|
|||
"""Test bad selector raises exception."""
|
||||
with pytest.raises(AssertionError):
|
||||
markings.clear_markings(data, selector)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA)
|
||||
def test_clear_marking_not_present(data):
|
||||
"""Test clearing markings for a selector that has no associated markings."""
|
||||
with pytest.raises(MarkingNotFoundError):
|
||||
data = markings.clear_markings(data, ["labels"])
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
import stix2
|
||||
|
||||
|
||||
|
@ -67,7 +71,11 @@ def test_file_observable_expression():
|
|||
assert str(exp) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f' AND file:mime_type = 'application/x-pdf']" # noqa
|
||||
|
||||
|
||||
def test_multiple_file_observable_expression():
|
||||
@pytest.mark.parametrize("observation_class, op", [
|
||||
(stix2.AndObservationExpression, 'AND'),
|
||||
(stix2.OrObservationExpression, 'OR'),
|
||||
])
|
||||
def test_multiple_file_observable_expression(observation_class, op):
|
||||
exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
||||
stix2.HashConstant(
|
||||
"bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c",
|
||||
|
@ -81,8 +89,8 @@ def test_multiple_file_observable_expression():
|
|||
'SHA-256'))
|
||||
op1_exp = stix2.ObservationExpression(bool1_exp)
|
||||
op2_exp = stix2.ObservationExpression(exp3)
|
||||
exp = stix2.AndObservationExpression([op1_exp, op2_exp])
|
||||
assert str(exp) == "[file:hashes.'SHA-256' = 'bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c' OR file:hashes.MD5 = 'cead3f77f6cda6ec00f57d76c9a6879f'] AND [file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']" # noqa
|
||||
exp = observation_class([op1_exp, op2_exp])
|
||||
assert str(exp) == "[file:hashes.'SHA-256' = 'bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c' OR file:hashes.MD5 = 'cead3f77f6cda6ec00f57d76c9a6879f'] {} [file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']".format(op) # noqa
|
||||
|
||||
|
||||
def test_root_types():
|
||||
|
@ -120,6 +128,31 @@ def test_greater_than():
|
|||
assert str(exp) == "[file:extensions.windows-pebinary-ext.sections[*].entropy > 7.0]"
|
||||
|
||||
|
||||
def test_less_than():
|
||||
exp = stix2.LessThanComparisonExpression("file:size",
|
||||
1024)
|
||||
assert str(exp) == "file:size < 1024"
|
||||
|
||||
|
||||
def test_greater_than_or_equal():
|
||||
exp = stix2.GreaterThanEqualComparisonExpression("file:size",
|
||||
1024)
|
||||
assert str(exp) == "file:size >= 1024"
|
||||
|
||||
|
||||
def test_less_than_or_equal():
|
||||
exp = stix2.LessThanEqualComparisonExpression("file:size",
|
||||
1024)
|
||||
assert str(exp) == "file:size <= 1024"
|
||||
|
||||
|
||||
def test_not():
|
||||
exp = stix2.LessThanComparisonExpression("file:size",
|
||||
1024,
|
||||
negated=True)
|
||||
assert str(exp) == "file:size NOT < 1024"
|
||||
|
||||
|
||||
def test_and_observable_expression():
|
||||
exp1 = stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("user-account:account_type",
|
||||
"unix"),
|
||||
|
@ -145,6 +178,15 @@ def test_and_observable_expression():
|
|||
assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1009' AND user-account:account_login = 'Mary']" # noqa
|
||||
|
||||
|
||||
def test_invalid_and_observable_expression():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("user-account:display_name",
|
||||
"admin"),
|
||||
stix2.EqualityComparisonExpression("email-addr:display_name",
|
||||
stix2.StringConstant("admin"))])
|
||||
assert "All operands to an 'AND' expression must have the same object type" in str(excinfo)
|
||||
|
||||
|
||||
def test_hex():
|
||||
exp_and = stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("file:mime_type",
|
||||
"image/bmp"),
|
||||
|
@ -175,3 +217,158 @@ def test_set_op():
|
|||
def test_timestamp():
|
||||
ts = stix2.TimestampConstant('2014-01-13T07:03:17Z')
|
||||
assert str(ts) == "t'2014-01-13T07:03:17Z'"
|
||||
|
||||
|
||||
def test_boolean():
|
||||
exp = stix2.EqualityComparisonExpression("email-message:is_multipart",
|
||||
True)
|
||||
assert str(exp) == "email-message:is_multipart = true"
|
||||
|
||||
|
||||
def test_binary():
|
||||
const = stix2.BinaryConstant("dGhpcyBpcyBhIHRlc3Q=")
|
||||
exp = stix2.EqualityComparisonExpression("artifact:payload_bin",
|
||||
const)
|
||||
assert str(exp) == "artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q='"
|
||||
|
||||
|
||||
def test_list():
|
||||
exp = stix2.InComparisonExpression("process:name",
|
||||
['proccy', 'proximus', 'badproc'])
|
||||
assert str(exp) == "process:name IN ('proccy', 'proximus', 'badproc')"
|
||||
|
||||
|
||||
def test_list2():
|
||||
# alternate way to construct an "IN" Comparison Expression
|
||||
exp = stix2.EqualityComparisonExpression("process:name",
|
||||
['proccy', 'proximus', 'badproc'])
|
||||
assert str(exp) == "process:name IN ('proccy', 'proximus', 'badproc')"
|
||||
|
||||
|
||||
def test_invalid_constant_type():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.EqualityComparisonExpression("artifact:payload_bin",
|
||||
{'foo': 'bar'})
|
||||
assert 'Unable to create a constant' in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_integer_constant():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.IntegerConstant('foo')
|
||||
assert 'must be an integer' in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_timestamp_constant():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.TimestampConstant('foo')
|
||||
assert 'must be a datetime object or timestamp string' in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_float_constant():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.FloatConstant('foo')
|
||||
assert 'must be a float' in str(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data, result", [
|
||||
(True, True),
|
||||
(False, False),
|
||||
('True', True),
|
||||
('False', False),
|
||||
('true', True),
|
||||
('false', False),
|
||||
('t', True),
|
||||
('f', False),
|
||||
('T', True),
|
||||
('F', False),
|
||||
(1, True),
|
||||
(0, False),
|
||||
])
|
||||
def test_boolean_constant(data, result):
|
||||
boolean = stix2.BooleanConstant(data)
|
||||
assert boolean.value == result
|
||||
|
||||
|
||||
def test_invalid_boolean_constant():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.BooleanConstant('foo')
|
||||
assert 'must be a boolean' in str(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hashtype, data", [
|
||||
('MD5', 'zzz'),
|
||||
('ssdeep', 'zzz=='),
|
||||
])
|
||||
def test_invalid_hash_constant(hashtype, data):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.HashConstant(data, hashtype)
|
||||
assert 'is not a valid {} hash'.format(hashtype) in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_hex_constant():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.HexConstant('mm')
|
||||
assert "must contain an even number of hexadecimal characters" in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_binary_constant():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.BinaryConstant('foo')
|
||||
assert 'must contain a base64' in str(excinfo)
|
||||
|
||||
|
||||
def test_escape_quotes_and_backslashes():
|
||||
exp = stix2.MatchesComparisonExpression("file:name",
|
||||
"^Final Report.+\.exe$")
|
||||
assert str(exp) == "file:name MATCHES '^Final Report.+\\\\.exe$'"
|
||||
|
||||
|
||||
def test_like():
|
||||
exp = stix2.LikeComparisonExpression("directory:path",
|
||||
"C:\Windows\%\\foo")
|
||||
assert str(exp) == "directory:path LIKE 'C:\\\\Windows\\\\%\\\\foo'"
|
||||
|
||||
|
||||
def test_issuperset():
|
||||
exp = stix2.IsSupersetComparisonExpression("ipv4-addr:value",
|
||||
"198.51.100.0/24")
|
||||
assert str(exp) == "ipv4-addr:value ISSUPERSET '198.51.100.0/24'"
|
||||
|
||||
|
||||
def test_repeat_qualifier():
|
||||
qual = stix2.RepeatQualifier(stix2.IntegerConstant(5))
|
||||
assert str(qual) == 'REPEATS 5 TIMES'
|
||||
|
||||
|
||||
def test_invalid_repeat_qualifier():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.RepeatQualifier('foo')
|
||||
assert 'is not a valid argument for a Repeat Qualifier' in str(excinfo)
|
||||
|
||||
|
||||
def test_invalid_within_qualifier():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.WithinQualifier('foo')
|
||||
assert 'is not a valid argument for a Within Qualifier' in str(excinfo)
|
||||
|
||||
|
||||
def test_startstop_qualifier():
|
||||
qual = stix2.StartStopQualifier(stix2.TimestampConstant('2016-06-01T00:00:00Z'),
|
||||
datetime.datetime(2017, 3, 12, 8, 30, 0))
|
||||
assert str(qual) == "START t'2016-06-01T00:00:00Z' STOP t'2017-03-12T08:30:00Z'"
|
||||
|
||||
qual2 = stix2.StartStopQualifier(datetime.date(2016, 6, 1),
|
||||
stix2.TimestampConstant('2016-07-01T00:00:00Z'))
|
||||
assert str(qual2) == "START t'2016-06-01T00:00:00Z' STOP t'2016-07-01T00:00:00Z'"
|
||||
|
||||
|
||||
def test_invalid_startstop_qualifier():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.StartStopQualifier('foo',
|
||||
stix2.TimestampConstant('2016-06-01T00:00:00Z'))
|
||||
assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
stix2.StartStopQualifier(datetime.date(2016, 6, 1),
|
||||
'foo')
|
||||
assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo)
|
||||
|
|
|
@ -16,6 +16,8 @@ def test_property():
|
|||
p = Property()
|
||||
|
||||
assert p.required is False
|
||||
assert p.clean('foo') == 'foo'
|
||||
assert p.clean(3) == 3
|
||||
|
||||
|
||||
def test_basic_clean():
|
||||
|
|
|
@ -233,12 +233,19 @@ def test_remove_custom_stix_object():
|
|||
("animal_class", stix2.properties.StringProperty()),
|
||||
])
|
||||
class Animal(object):
|
||||
def __init__(self, animal_class=None, **kwargs):
|
||||
if animal_class and animal_class not in ["mammal", "bird"]:
|
||||
raise ValueError("Not a recognized class of animal")
|
||||
pass
|
||||
|
||||
animal = Animal(species="lion", animal_class="mammal")
|
||||
|
||||
nc = stix2.utils.remove_custom_stix(animal)
|
||||
|
||||
assert nc is None
|
||||
|
||||
|
||||
def test_remove_custom_stix_no_custom():
|
||||
campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS)
|
||||
campaign_v2 = stix2.utils.remove_custom_stix(campaign_v1)
|
||||
|
||||
assert len(campaign_v1.keys()) == len(campaign_v2.keys())
|
||||
assert campaign_v1.id == campaign_v2.id
|
||||
assert campaign_v1.description == campaign_v2.description
|
||||
|
|
Loading…
Reference in New Issue