Add ObservableProperty, DictionaryProperty, HashesProperty,
BinaryProperty, and HexPropertystix2.1
parent
7a8e6341b2
commit
c63ba8e447
|
@ -99,3 +99,7 @@ class _STIXBase(collections.Mapping):
|
||||||
props = [(k, self[k]) for k in sorted(self._properties) if self.get(k)]
|
props = [(k, self[k]) for k in sorted(self._properties) if self.get(k)]
|
||||||
return "{0}({1})".format(self.__class__.__name__,
|
return "{0}({1})".format(self.__class__.__name__,
|
||||||
", ".join(["{0!s}={1!r}".format(k, v) for k, v in props]))
|
", ".join(["{0!s}={1!r}".format(k, v) for k, v in props]))
|
||||||
|
|
||||||
|
|
||||||
|
class Observable(_STIXBase):
|
||||||
|
pass
|
||||||
|
|
|
@ -49,3 +49,16 @@ class ImmutableError(STIXError, ValueError):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ImmutableError, self).__init__("Cannot modify properties after creation.")
|
super(ImmutableError, self).__init__("Cannot modify properties after creation.")
|
||||||
|
|
||||||
|
|
||||||
|
class DictionaryKeyError(STIXError, ValueError):
|
||||||
|
"""Dictionary key does not conform to the correct format."""
|
||||||
|
|
||||||
|
def __init__(self, key, reason):
|
||||||
|
super(DictionaryKeyError, self).__init__()
|
||||||
|
self.key = key
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
msg = "Invliad dictionary key {0.key}: ({0.reason})."
|
||||||
|
return msg.format(self)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
import collections
|
import collections
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -8,7 +10,8 @@ from dateutil import parser
|
||||||
import pytz
|
import pytz
|
||||||
from six import text_type
|
from six import text_type
|
||||||
|
|
||||||
from .base import _STIXBase
|
from .base import _STIXBase, Observable
|
||||||
|
from .exceptions import DictionaryKeyError
|
||||||
|
|
||||||
|
|
||||||
class Property(object):
|
class Property(object):
|
||||||
|
@ -213,6 +216,86 @@ class TimestampProperty(Property):
|
||||||
return pytz.utc.localize(parsed)
|
return pytz.utc.localize(parsed)
|
||||||
|
|
||||||
|
|
||||||
|
class ObservableProperty(Property):
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
dictified = dict(value)
|
||||||
|
for obj in dictified:
|
||||||
|
if not issubclass(type(obj), Observable):
|
||||||
|
raise ValueError("Objects in an observable property must be "
|
||||||
|
"Cyber Observable Objects")
|
||||||
|
return dictified
|
||||||
|
|
||||||
|
|
||||||
|
class DictionaryProperty(Property):
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
dictified = dict(value)
|
||||||
|
for k in dictified.keys():
|
||||||
|
if len(k) < 3:
|
||||||
|
raise DictionaryKeyError(k, "shorter than 3 characters")
|
||||||
|
elif len(k) > 256:
|
||||||
|
raise DictionaryKeyError(k, "longer than 256 characters")
|
||||||
|
if not re.match('^[a-zA-Z0-9_-]+$', k):
|
||||||
|
raise DictionaryKeyError(k, "contains characters other than"
|
||||||
|
"lowercase a-z, uppercase A-Z, "
|
||||||
|
"numerals 0-9, hyphen (-), or "
|
||||||
|
"underscore (_)")
|
||||||
|
return dictified
|
||||||
|
|
||||||
|
|
||||||
|
HASHES_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 HashesProperty(DictionaryProperty):
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
clean_dict = super(HashesProperty, self).clean(value)
|
||||||
|
for k, v in clean_dict.items():
|
||||||
|
key = k.upper().replace('-', '')
|
||||||
|
if key in HASHES_REGEX:
|
||||||
|
vocab_key = HASHES_REGEX[key][1]
|
||||||
|
if not re.match(HASHES_REGEX[key][0], v):
|
||||||
|
raise ValueError("'%s' is not a valid %s hash" % (v, vocab_key))
|
||||||
|
if k != vocab_key:
|
||||||
|
clean_dict[vocab_key] = clean_dict[k]
|
||||||
|
del clean_dict[k]
|
||||||
|
return clean_dict
|
||||||
|
|
||||||
|
|
||||||
|
class BinaryProperty(Property):
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
try:
|
||||||
|
base64.b64decode(value)
|
||||||
|
except (binascii.Error, TypeError):
|
||||||
|
raise ValueError("must contain a base64 encoded string")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class HexProperty(Property):
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
if not re.match('^([a-fA-F0-9]{2})+$', value):
|
||||||
|
raise ValueError("must contain an even number of hexadecimal characters")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}"
|
REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}"
|
||||||
"-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
"-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from stix2.properties import (BooleanProperty, IDProperty, IntegerProperty,
|
from stix2.exceptions import DictionaryKeyError
|
||||||
ListProperty, Property, ReferenceProperty,
|
from stix2.properties import (BinaryProperty, BooleanProperty,
|
||||||
StringProperty, TimestampProperty, TypeProperty)
|
DictionaryProperty, HashesProperty, HexProperty,
|
||||||
|
IDProperty, IntegerProperty, ListProperty,
|
||||||
|
Property, ReferenceProperty, StringProperty,
|
||||||
|
TimestampProperty, TypeProperty)
|
||||||
|
|
||||||
from .constants import FAKE_TIME
|
from .constants import FAKE_TIME
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,3 +175,60 @@ def test_timestamp_property_invalid():
|
||||||
ts_prop.clean(1)
|
ts_prop.clean(1)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ts_prop.clean("someday sometime")
|
ts_prop.clean("someday sometime")
|
||||||
|
|
||||||
|
|
||||||
|
def test_binary_property():
|
||||||
|
bin_prop = BinaryProperty()
|
||||||
|
|
||||||
|
assert bin_prop.clean("TG9yZW0gSXBzdW0=")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
bin_prop.clean("foobar")
|
||||||
|
|
||||||
|
|
||||||
|
def test_hex_property():
|
||||||
|
hex_prop = HexProperty()
|
||||||
|
|
||||||
|
assert hex_prop.clean("4c6f72656d20497073756d")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
hex_prop.clean("foobar")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("d", [
|
||||||
|
{'description': 'something'},
|
||||||
|
[('abc', 1), ('bcd', 2), ('cde', 3)],
|
||||||
|
])
|
||||||
|
def test_dictionary_property_valid(d):
|
||||||
|
dict_prop = DictionaryProperty()
|
||||||
|
assert dict_prop.clean(d)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("d", [
|
||||||
|
{'a': 'something'},
|
||||||
|
{'a'*300: 'something'},
|
||||||
|
{'Hey!': 'something'},
|
||||||
|
])
|
||||||
|
def test_dictionary_property_invalid(d):
|
||||||
|
dict_prop = DictionaryProperty()
|
||||||
|
|
||||||
|
with pytest.raises(DictionaryKeyError):
|
||||||
|
dict_prop.clean(d)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", [
|
||||||
|
{"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"},
|
||||||
|
[('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')],
|
||||||
|
])
|
||||||
|
def test_hashes_property_valid(value):
|
||||||
|
hash_prop = HashesProperty()
|
||||||
|
assert hash_prop.clean(value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", [
|
||||||
|
{"MD5": "a"},
|
||||||
|
{"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"},
|
||||||
|
])
|
||||||
|
def test_hashes_property_invalid(value):
|
||||||
|
hash_prop = HashesProperty()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
hash_prop.clean(value)
|
||||||
|
|
Loading…
Reference in New Issue