Introduce constant objects for literals in pattern expressions

fixed idioms
stix2.1
Richard Piazza 2017-07-07 16:27:39 -04:00 committed by Greg Back
parent c8bcece6f6
commit c1b07ef505
4 changed files with 195 additions and 27 deletions

View File

@ -4,6 +4,8 @@
from . import exceptions
from .bundle import Bundle
from .constants import (FloatConstant, HashConstant, IntegerConstant,
StringConstant)
from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
AutonomousSystem, CustomObservable, Directory,
DomainName, EmailAddress, EmailMessage,
@ -25,12 +27,17 @@ from .pattern_expressions import (AndBooleanExpression,
ComparisonExpression,
EqualityComparisonExpression,
FollowedByObservableExpression,
GreaterThanComparisonExpression,
GreaterThanEqualComparisonExpression,
LessThanComparisonExpression,
LessThanEqualComparisonExpression,
LikeComparisonExpression,
MatchesComparisonExpression,
ObservableExpression,
OrBooleanExpression,
ObservableExpression, OrBooleanExpression,
OrObservableExpression,
ParentheticalExpression,
QualifiedObservationExpression,
RepeatQualifier, StartStopQualifier,
WithinQualifier)
from .sdo import (AttackPattern, Campaign, CourseOfAction, CustomObject,
Identity, Indicator, IntrusionSet, Malware, ObservedData,

125
stix2/constants.py Normal file
View File

@ -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

View File

@ -1,3 +1,5 @@
from .constants import ListConstant, make_constant
class PatternExpression(object):
@ -13,24 +15,30 @@ class PatternExpression(object):
class ComparisonExpression(PatternExpression):
def __init__(self, operator, lhs, rhs, negated=False):
if operator == "=" and isinstance(rhs, list):
if operator == "=" and isinstance(rhs, ListConstant):
self.operator = "IN"
else:
self.operator = operator
self.lhs = lhs
self.rhs = rhs
if isinstance(rhs, str):
self.rhs = make_constant(rhs)
else:
self.rhs = rhs
self.negated = negated
self.root_type = self.get_root_from_object_path(lhs)
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) + ")"
# 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:
rhs_string = "'" + self.escape_quotes_and_backslashes("%s" % self.rhs) + "'"
return self.lhs + (" NOT" if self.negated else "") + " " + self.operator + " " + rhs_string
return "%s %s %s" % (self.lhs, self.operator, self.rhs)
class EqualityComparisonExpression(ComparisonExpression):

View File

@ -3,42 +3,49 @@ import stix2
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'"
def test_boolean_expression():
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value", ".+\\@example\\.com$")
exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name", "^Final Report.+\\.exe$")
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value",
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])
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():
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value", ".+\\@example\\.com$")
exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name", "^Final Report.+\\.exe$")
exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value",
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]))
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():
hash_exp = stix2.EqualityComparisonExpression("file:hashes.MD5",
"79054025255fb1a26e4bc422aef54eb4")
stix2.HashConstant("79054025255fb1a26e4bc422aef54eb4", "MD5"))
o_exp1 = stix2.ObservableExpression(hash_exp)
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)
fb_exp = stix2.FollowedByObservableExpression([o_exp1, o_exp2])
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)
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():
exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f")
exp2 = stix2.EqualityComparisonExpression("file:mime_type", "application/x-pdf")
stix2.HashConstant(
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f",
'SHA-256'))
exp2 = stix2.EqualityComparisonExpression("file:mime_type", stix2.StringConstant("application/x-pdf"))
bool_exp = stix2.AndBooleanExpression([exp1, exp2])
exp = stix2.ObservableExpression(bool_exp)
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():
exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
"bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c")
stix2.HashConstant(
"bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c",
'SHA-256'))
exp2 = stix2.EqualityComparisonExpression("file:hashes.MD5",
"cead3f77f6cda6ec00f57d76c9a6879f")
stix2.HashConstant("cead3f77f6cda6ec00f57d76c9a6879f", "MD5"))
bool1_exp = stix2.OrBooleanExpression([exp1, exp2])
exp3 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'",
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f")
stix2.HashConstant(
"aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f",
'SHA-256'))
op1_exp = stix2.ObservableExpression(bool1_exp)
op2_exp = stix2.ObservableExpression(exp3)
exp = stix2.AndObservableExpression([op1_exp, op2_exp])
@ -63,7 +74,24 @@ def test_root_types():
stix2.AndBooleanExpression(
[stix2.ParentheticalExpression(
stix2.OrBooleanExpression([
stix2.EqualityComparisonExpression(u"a:b", u"1"),
stix2.EqualityComparisonExpression(u"b:c", u"2")])),
stix2.EqualityComparisonExpression(u"b:d", u"3")]))
stix2.EqualityComparisonExpression("a:b", stix2.StringConstant("1")),
stix2.EqualityComparisonExpression("b:c", stix2.StringConstant("2"))])),
stix2.EqualityComparisonExpression(u"b:d", stix2.StringConstant("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]"