parent
c8bcece6f6
commit
c1b07ef505
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
from .bundle import Bundle
|
from .bundle import Bundle
|
||||||
|
from .constants import (FloatConstant, HashConstant, IntegerConstant,
|
||||||
|
StringConstant)
|
||||||
from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
|
from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
|
||||||
AutonomousSystem, CustomObservable, Directory,
|
AutonomousSystem, CustomObservable, Directory,
|
||||||
DomainName, EmailAddress, EmailMessage,
|
DomainName, EmailAddress, EmailMessage,
|
||||||
|
@ -25,12 +27,17 @@ from .pattern_expressions import (AndBooleanExpression,
|
||||||
ComparisonExpression,
|
ComparisonExpression,
|
||||||
EqualityComparisonExpression,
|
EqualityComparisonExpression,
|
||||||
FollowedByObservableExpression,
|
FollowedByObservableExpression,
|
||||||
|
GreaterThanComparisonExpression,
|
||||||
|
GreaterThanEqualComparisonExpression,
|
||||||
|
LessThanComparisonExpression,
|
||||||
|
LessThanEqualComparisonExpression,
|
||||||
|
LikeComparisonExpression,
|
||||||
MatchesComparisonExpression,
|
MatchesComparisonExpression,
|
||||||
ObservableExpression,
|
ObservableExpression, OrBooleanExpression,
|
||||||
OrBooleanExpression,
|
|
||||||
OrObservableExpression,
|
OrObservableExpression,
|
||||||
ParentheticalExpression,
|
ParentheticalExpression,
|
||||||
QualifiedObservationExpression,
|
QualifiedObservationExpression,
|
||||||
|
RepeatQualifier, StartStopQualifier,
|
||||||
WithinQualifier)
|
WithinQualifier)
|
||||||
from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject,
|
from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject,
|
||||||
Identity, Indicator, IntrusionSet, Malware, ObservedData,
|
Identity, Indicator, IntrusionSet, Malware, ObservedData,
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import re
|
||||||
|
|
||||||
|
# TODO: REConstant?
|
||||||
|
# TODO: Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
class Constant(object):
|
||||||
|
def __str__(self):
|
||||||
|
return "%s" % self.value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def escape_quotes_and_backslashes(s):
|
||||||
|
return s.replace(u'\\', u'\\\\').replace(u"'", u"\\'")
|
||||||
|
|
||||||
|
|
||||||
|
class StringConstant(Constant):
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "'%s'" % StringConstant.escape_quotes_and_backslashes(self.value)
|
||||||
|
|
||||||
|
|
||||||
|
class IntegerConstant(Constant):
|
||||||
|
def __init__(self, value):
|
||||||
|
try:
|
||||||
|
self.value = int(value)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("must be an integer.")
|
||||||
|
|
||||||
|
|
||||||
|
class FloatConstant(Constant):
|
||||||
|
def __init__(self, value):
|
||||||
|
try:
|
||||||
|
self.value = float(value)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError("must be an float.")
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanConstant(Constant):
|
||||||
|
def __init__(self, value):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
trues = ['true', 't']
|
||||||
|
falses = ['false', 'f']
|
||||||
|
try:
|
||||||
|
if value.lower() in trues:
|
||||||
|
self.value = True
|
||||||
|
if value.lower() in falses:
|
||||||
|
self.value = False
|
||||||
|
except AttributeError:
|
||||||
|
if value == 1:
|
||||||
|
self.value = True
|
||||||
|
if value == 0:
|
||||||
|
self.value = False
|
||||||
|
|
||||||
|
raise ValueError("must be a boolean value.")
|
||||||
|
|
||||||
|
|
||||||
|
_HASH_REGEX = {
|
||||||
|
"MD5": ("^[a-fA-F0-9]{32}$", "MD5"),
|
||||||
|
"MD6": ("^[a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{56}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128}$", "MD6"),
|
||||||
|
"RIPEMD160": ("^[a-fA-F0-9]{40}$", "RIPEMD-160"),
|
||||||
|
"SHA1": ("^[a-fA-F0-9]{40}$", "SHA-1"),
|
||||||
|
"SHA224": ("^[a-fA-F0-9]{56}$", "SHA-224"),
|
||||||
|
"SHA256": ("^[a-fA-F0-9]{64}$", "SHA-256"),
|
||||||
|
"SHA384": ("^[a-fA-F0-9]{96}$", "SHA-384"),
|
||||||
|
"SHA512": ("^[a-fA-F0-9]{128}$", "SHA-512"),
|
||||||
|
"SHA3224": ("^[a-fA-F0-9]{56}$", "SHA3-224"),
|
||||||
|
"SHA3256": ("^[a-fA-F0-9]{64}$", "SHA3-256"),
|
||||||
|
"SHA3384": ("^[a-fA-F0-9]{96}$", "SHA3-384"),
|
||||||
|
"SHA3512": ("^[a-fA-F0-9]{128}$", "SHA3-512"),
|
||||||
|
"SSDEEP": ("^[a-zA-Z0-9/+:.]{1,128}$", "ssdeep"),
|
||||||
|
"WHIRLPOOL": ("^[a-fA-F0-9]{128}$", "WHIRLPOOL"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HashConstant(StringConstant):
|
||||||
|
def __init__(self, value, type):
|
||||||
|
key = type.upper().replace('-', '')
|
||||||
|
if key in _HASH_REGEX:
|
||||||
|
vocab_key = _HASH_REGEX[key][1]
|
||||||
|
if not re.match(_HASH_REGEX[key][0], value):
|
||||||
|
raise ValueError("'%s' is not a valid %s hash" % (value, vocab_key))
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryConstant(Constant):
|
||||||
|
|
||||||
|
def __init__(self, value):
|
||||||
|
try:
|
||||||
|
base64.b64decode(value)
|
||||||
|
self.value = value
|
||||||
|
except (binascii.Error, TypeError):
|
||||||
|
raise ValueError("must contain a base64 encoded string")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "b'%s'" % self.value
|
||||||
|
|
||||||
|
|
||||||
|
class HexConstant(Constant):
|
||||||
|
|
||||||
|
def __init__(self, value):
|
||||||
|
if not re.match('^([a-fA-F0-9]{2})+$', value):
|
||||||
|
raise ValueError("must contain an even number of hexadecimal characters")
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "h'%s'" % self.value
|
||||||
|
|
||||||
|
|
||||||
|
class ListConstant(Constant):
|
||||||
|
def __init__(self, values):
|
||||||
|
self.value = values
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "(" + ", ".join([("%s" % x) for x in self.value]) + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def make_constant(value):
|
||||||
|
# TODO: Stub
|
||||||
|
pass
|
|
@ -1,3 +1,5 @@
|
||||||
|
from .constants import ListConstant, make_constant
|
||||||
|
|
||||||
|
|
||||||
class PatternExpression(object):
|
class PatternExpression(object):
|
||||||
|
|
||||||
|
@ -13,24 +15,30 @@ class PatternExpression(object):
|
||||||
|
|
||||||
class ComparisonExpression(PatternExpression):
|
class ComparisonExpression(PatternExpression):
|
||||||
def __init__(self, operator, lhs, rhs, negated=False):
|
def __init__(self, operator, lhs, rhs, negated=False):
|
||||||
if operator == "=" and isinstance(rhs, list):
|
if operator == "=" and isinstance(rhs, ListConstant):
|
||||||
self.operator = "IN"
|
self.operator = "IN"
|
||||||
else:
|
else:
|
||||||
self.operator = operator
|
self.operator = operator
|
||||||
self.lhs = lhs
|
self.lhs = lhs
|
||||||
self.rhs = rhs
|
if isinstance(rhs, str):
|
||||||
|
self.rhs = make_constant(rhs)
|
||||||
|
else:
|
||||||
|
self.rhs = rhs
|
||||||
self.negated = negated
|
self.negated = negated
|
||||||
self.root_type = self.get_root_from_object_path(lhs)
|
self.root_type = self.get_root_from_object_path(lhs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if isinstance(self.rhs, list):
|
# if isinstance(self.rhs, list):
|
||||||
final_rhs = []
|
# final_rhs = []
|
||||||
for r in self.rhs:
|
# for r in self.rhs:
|
||||||
final_rhs.append("'" + self.escape_quotes_and_backslashes("%s" % r) + "'")
|
# final_rhs.append("'" + self.escape_quotes_and_backslashes("%s" % r) + "'")
|
||||||
rhs_string = "(" + ", ".join(final_rhs) + ")"
|
# 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:
|
else:
|
||||||
rhs_string = "'" + self.escape_quotes_and_backslashes("%s" % self.rhs) + "'"
|
return "%s %s %s" % (self.lhs, self.operator, self.rhs)
|
||||||
return self.lhs + (" NOT" if self.negated else "") + " " + self.operator + " " + rhs_string
|
|
||||||
|
|
||||||
|
|
||||||
class EqualityComparisonExpression(ComparisonExpression):
|
class EqualityComparisonExpression(ComparisonExpression):
|
||||||
|
|
|
@ -3,42 +3,49 @@ import stix2
|
||||||
|
|
||||||
def test_create_comparison_expression():
|
def test_create_comparison_expression():
|
||||||
|
|
||||||
exp = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'", "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f")
|
exp = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
||||||
|
stix2.HashConstant("aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", "SHA-256")) # noqa
|
||||||
assert str(exp) == "file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f'"
|
assert str(exp) == "file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f'"
|
||||||
|
|
||||||
|
|
||||||
def test_boolean_expression():
|
def test_boolean_expression():
|
||||||
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value", ".+\\@example\\.com$")
|
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value",
|
||||||
exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name", "^Final Report.+\\.exe$")
|
stix2.StringConstant(".+\\@example\\.com$"))
|
||||||
|
exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name",
|
||||||
|
stix2.StringConstant("^Final Report.+\\.exe$"))
|
||||||
exp = stix2.AndBooleanExpression([exp1, exp2])
|
exp = stix2.AndBooleanExpression([exp1, exp2])
|
||||||
assert str(exp) == "email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$'" # noqa
|
assert str(exp) == "email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$'" # noqa
|
||||||
|
|
||||||
|
|
||||||
def test_boolean_expression_with_parentheses():
|
def test_boolean_expression_with_parentheses():
|
||||||
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value", ".+\\@example\\.com$")
|
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value",
|
||||||
exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name", "^Final Report.+\\.exe$")
|
stix2.StringConstant(".+\\@example\\.com$"))
|
||||||
|
exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name",
|
||||||
|
stix2.StringConstant("^Final Report.+\\.exe$"))
|
||||||
exp = stix2.ParentheticalExpression(stix2.AndBooleanExpression([exp1, exp2]))
|
exp = stix2.ParentheticalExpression(stix2.AndBooleanExpression([exp1, exp2]))
|
||||||
assert str(exp) == "(email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$')" # noqa
|
assert str(exp) == "(email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$')" # noqa
|
||||||
|
|
||||||
|
|
||||||
def test_hash_followed_by_registryKey_expression():
|
def test_hash_followed_by_registryKey_expression():
|
||||||
hash_exp = stix2.EqualityComparisonExpression("file:hashes.MD5",
|
hash_exp = stix2.EqualityComparisonExpression("file:hashes.MD5",
|
||||||
"79054025255fb1a26e4bc422aef54eb4")
|
stix2.HashConstant("79054025255fb1a26e4bc422aef54eb4", "MD5"))
|
||||||
o_exp1 = stix2.ObservableExpression(hash_exp)
|
o_exp1 = stix2.ObservableExpression(hash_exp)
|
||||||
reg_exp = stix2.EqualityComparisonExpression("win-registry-key:key",
|
reg_exp = stix2.EqualityComparisonExpression("win-registry-key:key",
|
||||||
"HKEY_LOCAL_MACHINE\\foo\\bar")
|
stix2.StringConstant("HKEY_LOCAL_MACHINE\\foo\\bar"))
|
||||||
o_exp2 = stix2.ObservableExpression(reg_exp)
|
o_exp2 = stix2.ObservableExpression(reg_exp)
|
||||||
fb_exp = stix2.FollowedByObservableExpression([o_exp1, o_exp2])
|
fb_exp = stix2.FollowedByObservableExpression([o_exp1, o_exp2])
|
||||||
para_exp = stix2.ParentheticalExpression(fb_exp)
|
para_exp = stix2.ParentheticalExpression(fb_exp)
|
||||||
qual_exp = stix2.WithinQualifier(300)
|
qual_exp = stix2.WithinQualifier(stix2.IntegerConstant(300))
|
||||||
exp = stix2.QualifiedObservationExpression(para_exp, qual_exp)
|
exp = stix2.QualifiedObservationExpression(para_exp, qual_exp)
|
||||||
assert str(exp) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [win-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS" # noqa
|
assert str(exp) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [win-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS" # noqa
|
||||||
|
|
||||||
|
|
||||||
def test_file_observable_expression():
|
def test_file_observable_expression():
|
||||||
exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
||||||
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f")
|
stix2.HashConstant(
|
||||||
exp2 = stix2.EqualityComparisonExpression("file:mime_type", "application/x-pdf")
|
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f",
|
||||||
|
'SHA-256'))
|
||||||
|
exp2 = stix2.EqualityComparisonExpression("file:mime_type", stix2.StringConstant("application/x-pdf"))
|
||||||
bool_exp = stix2.AndBooleanExpression([exp1, exp2])
|
bool_exp = stix2.AndBooleanExpression([exp1, exp2])
|
||||||
exp = stix2.ObservableExpression(bool_exp)
|
exp = stix2.ObservableExpression(bool_exp)
|
||||||
assert str(exp) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f' AND file:mime_type = 'application/x-pdf']" # noqa
|
assert str(exp) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f' AND file:mime_type = 'application/x-pdf']" # noqa
|
||||||
|
@ -46,12 +53,16 @@ def test_file_observable_expression():
|
||||||
|
|
||||||
def test_multiple_file_observable_expression():
|
def test_multiple_file_observable_expression():
|
||||||
exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
||||||
"bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c")
|
stix2.HashConstant(
|
||||||
|
"bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c",
|
||||||
|
'SHA-256'))
|
||||||
exp2 = stix2.EqualityComparisonExpression("file:hashes.MD5",
|
exp2 = stix2.EqualityComparisonExpression("file:hashes.MD5",
|
||||||
"cead3f77f6cda6ec00f57d76c9a6879f")
|
stix2.HashConstant("cead3f77f6cda6ec00f57d76c9a6879f", "MD5"))
|
||||||
bool1_exp = stix2.OrBooleanExpression([exp1, exp2])
|
bool1_exp = stix2.OrBooleanExpression([exp1, exp2])
|
||||||
exp3 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
exp3 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
|
||||||
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f")
|
stix2.HashConstant(
|
||||||
|
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f",
|
||||||
|
'SHA-256'))
|
||||||
op1_exp = stix2.ObservableExpression(bool1_exp)
|
op1_exp = stix2.ObservableExpression(bool1_exp)
|
||||||
op2_exp = stix2.ObservableExpression(exp3)
|
op2_exp = stix2.ObservableExpression(exp3)
|
||||||
exp = stix2.AndObservableExpression([op1_exp, op2_exp])
|
exp = stix2.AndObservableExpression([op1_exp, op2_exp])
|
||||||
|
@ -63,7 +74,24 @@ def test_root_types():
|
||||||
stix2.AndBooleanExpression(
|
stix2.AndBooleanExpression(
|
||||||
[stix2.ParentheticalExpression(
|
[stix2.ParentheticalExpression(
|
||||||
stix2.OrBooleanExpression([
|
stix2.OrBooleanExpression([
|
||||||
stix2.EqualityComparisonExpression(u"a:b", u"1"),
|
stix2.EqualityComparisonExpression("a:b", stix2.StringConstant("1")),
|
||||||
stix2.EqualityComparisonExpression(u"b:c", u"2")])),
|
stix2.EqualityComparisonExpression("b:c", stix2.StringConstant("2"))])),
|
||||||
stix2.EqualityComparisonExpression(u"b:d", u"3")]))
|
stix2.EqualityComparisonExpression(u"b:d", stix2.StringConstant("3"))]))
|
||||||
assert str(ast) == "[(a:b = '1' OR b:c = '2') AND b:d = '3']"
|
assert str(ast) == "[(a:b = '1' OR b:c = '2') AND b:d = '3']"
|
||||||
|
|
||||||
|
|
||||||
|
def test_artifact_payload():
|
||||||
|
exp1 = stix2.EqualityComparisonExpression("artifact:mime_type",
|
||||||
|
stix2.StringConstant("application/vnd.tcpdump.pcap"))
|
||||||
|
exp2 = stix2.MatchesComparisonExpression("artifact:payload_bin",
|
||||||
|
stix2.StringConstant("\\xd4\\xc3\\xb2\\xa1\\x02\\x00\\x04\\x00"))
|
||||||
|
and_exp = stix2.AndBooleanExpression([exp1, exp2])
|
||||||
|
exp = stix2.ObservableExpression(and_exp)
|
||||||
|
assert str(exp) == "[artifact:mime_type = 'application/vnd.tcpdump.pcap' AND artifact:payload_bin MATCHES '\\\\xd4\\\\xc3\\\\xb2\\\\xa1\\\\x02\\\\x00\\\\x04\\\\x00']" # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def test_greater_than():
|
||||||
|
exp1 = stix2.GreaterThanComparisonExpression("file:extensions.windows-pebinary-ext.sections[*].entropy",
|
||||||
|
stix2.FloatConstant(7.0))
|
||||||
|
exp = stix2.ObservableExpression(exp1)
|
||||||
|
assert str(exp) == "[file:extensions.windows-pebinary-ext.sections[*].entropy > 7.0]"
|
||||||
|
|
Loading…
Reference in New Issue