WIP: Start transforming properties.
parent
e5610ff81b
commit
cf688c3851
|
@ -23,6 +23,16 @@ class STIXJSONEncoder(json.JSONEncoder):
|
|||
return super(STIXJSONEncoder, self).default(obj)
|
||||
|
||||
|
||||
def get_required_properties(properties):
|
||||
for k, v in properties.items():
|
||||
if isinstance(v, dict):
|
||||
if v.get('required'):
|
||||
yield k
|
||||
else: # This is a Property subclass
|
||||
if v.required:
|
||||
yield k
|
||||
|
||||
|
||||
class _STIXBase(collections.Mapping):
|
||||
"""Base class for STIX object types"""
|
||||
|
||||
|
@ -30,31 +40,16 @@ class _STIXBase(collections.Mapping):
|
|||
def _make_id(cls):
|
||||
return cls._type + "--" + str(uuid.uuid4())
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# TODO: remove this
|
||||
def _handle_old_style_property(self, prop_name, prop_metadata, kwargs):
|
||||
cls = self.__class__
|
||||
class_name = cls.__name__
|
||||
|
||||
# Use the same timestamp for any auto-generated datetimes
|
||||
now = get_timestamp()
|
||||
|
||||
# Detect any keyword arguments not allowed for a specific type
|
||||
extra_kwargs = list(set(kwargs) - set(cls._properties))
|
||||
if extra_kwargs:
|
||||
raise TypeError("unexpected keyword arguments: " + str(extra_kwargs))
|
||||
|
||||
required_fields = [k for k, v in cls._properties.items() if v.get('required')]
|
||||
missing_kwargs = set(required_fields) - set(kwargs)
|
||||
if missing_kwargs:
|
||||
msg = "Missing required field(s) for {type}: ({fields})."
|
||||
field_list = ", ".join(x for x in sorted(list(missing_kwargs)))
|
||||
raise ValueError(msg.format(type=class_name, fields=field_list))
|
||||
|
||||
for prop_name, prop_metadata in cls._properties.items():
|
||||
if prop_name not in kwargs:
|
||||
if prop_metadata.get('default'):
|
||||
default = prop_metadata['default']
|
||||
if default == NOW:
|
||||
kwargs[prop_name] = now
|
||||
kwargs[prop_name] = self.__now
|
||||
else:
|
||||
kwargs[prop_name] = default(cls)
|
||||
elif prop_metadata.get('fixed'):
|
||||
|
@ -79,6 +74,33 @@ class _STIXBase(collections.Mapping):
|
|||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
cls = self.__class__
|
||||
class_name = cls.__name__
|
||||
|
||||
# Use the same timestamp for any auto-generated datetimes
|
||||
self.__now = get_timestamp()
|
||||
|
||||
# Detect any keyword arguments not allowed for a specific type
|
||||
extra_kwargs = list(set(kwargs) - set(cls._properties))
|
||||
if extra_kwargs:
|
||||
raise TypeError("unexpected keyword arguments: " + str(extra_kwargs))
|
||||
|
||||
required_fields = get_required_properties(cls._properties)
|
||||
missing_kwargs = set(required_fields) - set(kwargs)
|
||||
if missing_kwargs:
|
||||
msg = "Missing required field(s) for {type}: ({fields})."
|
||||
field_list = ", ".join(x for x in sorted(list(missing_kwargs)))
|
||||
raise ValueError(msg.format(type=class_name, fields=field_list))
|
||||
|
||||
for prop_name, prop_metadata in cls._properties.items():
|
||||
|
||||
if isinstance(prop_metadata, dict):
|
||||
self._handle_old_style_property(prop_name, prop_metadata, kwargs)
|
||||
else: # This is a Property Subclasses
|
||||
# self.check_property(prop_name, prop_metadata, kwargs)
|
||||
pass
|
||||
|
||||
self._inner = kwargs
|
||||
|
||||
def __getitem__(self, key):
|
||||
|
@ -92,10 +114,13 @@ class _STIXBase(collections.Mapping):
|
|||
|
||||
# Handle attribute access just like key access
|
||||
def __getattr__(self, name):
|
||||
if name.startswith('_'):
|
||||
return super(_STIXBase, self).__getattr__(name)
|
||||
return self.get(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name != '_inner':
|
||||
if name != '_inner' and not name.startswith("_STIXBase__"):
|
||||
print(name)
|
||||
raise ValueError("Cannot modify properties after creation.")
|
||||
super(_STIXBase, self).__setattr__(name, value)
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
import uuid
|
||||
|
||||
|
||||
class Property(object):
|
||||
"""Represent a property of STIX data type.
|
||||
|
||||
Subclasses can define the following attributes as keyword arguments to
|
||||
__init__():
|
||||
|
||||
- `required` - If `True`, the property must be provided when creating an
|
||||
object with that property. No default value exists for these properties.
|
||||
(Default: `False`)
|
||||
- `fixed` - This provides a constant default value. Users are free to
|
||||
provide this value explicity when constructing an object (which allows
|
||||
you to copy *all* values from an existing object to a new object), but
|
||||
if the user provides a value other than the `fixed` value, it will raise
|
||||
an error. This is semantically equivalent to defining both:
|
||||
- a `validate()` function that checks if the value matches the fixed
|
||||
value, and
|
||||
- a `default()` function that returns the fixed value.
|
||||
(Default: `None`)
|
||||
|
||||
Subclasses can also define the following functions.
|
||||
|
||||
- `def clean(self, value) -> any:`
|
||||
- Transform `value` into a valid value for this property. This should
|
||||
raise a ValueError if such no such transformation is possible.
|
||||
- `def validate(self, value) -> any:`
|
||||
- check that `value` is valid for this property. This should return
|
||||
a valid value (possibly modified) for this property, or raise a
|
||||
ValueError if the value is not valid.
|
||||
(Default: if `clean` is defined, it will attempt to call `clean` and
|
||||
return the result or pass on a ValueError that `clean` raises. If
|
||||
`clean` is not defined, this will return `value` unmodified).
|
||||
- `def default(self):`
|
||||
- provide a default value for this property.
|
||||
- `default()` can return the special value `NOW` to use the current
|
||||
time. This is useful when several timestamps in the same object need
|
||||
to use the same default value, so calling now() for each field--
|
||||
likely several microseconds apart-- does not work.
|
||||
|
||||
Subclasses can instead provide lambda functions for `clean`, and `default`
|
||||
as keyword arguments. `validate` should not be provided as a lambda since
|
||||
lambdas cannot raise their own exceptions.
|
||||
"""
|
||||
|
||||
def __init__(self, required=False, fixed=None, clean=None, validate=None, default=None):
|
||||
self.required = required
|
||||
if fixed:
|
||||
self.validate = lambda x: x == fixed
|
||||
self.default = lambda: fixed
|
||||
if clean:
|
||||
self.clean = clean
|
||||
if validate:
|
||||
self.validate = validate
|
||||
if default:
|
||||
self.default = default
|
||||
|
||||
def clean(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
value = self.clean(value)
|
||||
except NotImplemetedError:
|
||||
pass
|
||||
return value
|
||||
|
||||
|
||||
class TypeProperty(Property):
|
||||
|
||||
def __init__(self, type):
|
||||
self.type = type
|
||||
|
||||
def validate(self, value):
|
||||
if value != self.type:
|
||||
raise ValueError("Invalid type value")
|
||||
|
||||
def default(self):
|
||||
return self.type
|
||||
|
||||
|
||||
class List(Property):
|
||||
|
||||
def __init__(self, contained):
|
||||
"""
|
||||
contained should be a type whose constructor creates an object from the value
|
||||
"""
|
||||
self.contained = contained
|
||||
|
||||
def validate(self, value):
|
||||
# TODO: ensure iterable
|
||||
for item in value:
|
||||
self.contained.validate(item)
|
||||
|
||||
def clean(self, value):
|
||||
return [self.contained(x) for x in value]
|
||||
|
||||
|
||||
class IDProperty(Property):
|
||||
|
||||
def __init__(self, type):
|
||||
self.type = type
|
||||
|
||||
def validate(self, value):
|
||||
# TODO: validate GUID as well
|
||||
return value.startswith(self.type + "--")
|
||||
|
||||
def default(self):
|
||||
return self.type + "--" + str(uuid.uuid4())
|
|
@ -0,0 +1,54 @@
|
|||
import pytest
|
||||
|
||||
from stix2.properties import Property, IDProperty
|
||||
|
||||
|
||||
def test_property():
|
||||
p = Property()
|
||||
|
||||
assert p.required is False
|
||||
|
||||
|
||||
def test_basic_validate():
|
||||
class Prop(Property):
|
||||
|
||||
def validate(self, value):
|
||||
if value == 42:
|
||||
return value
|
||||
else:
|
||||
raise ValueError("Must be 42")
|
||||
|
||||
p = Prop()
|
||||
|
||||
assert p.validate(42) == 42
|
||||
with pytest.raises(ValueError):
|
||||
p.validate(41)
|
||||
|
||||
|
||||
def test_default_field():
|
||||
class Prop(Property):
|
||||
|
||||
def default(self):
|
||||
return 77
|
||||
|
||||
p = Prop()
|
||||
|
||||
assert p.default() == 77
|
||||
|
||||
|
||||
def test_fixed_property():
|
||||
p = Property(fixed="2.0")
|
||||
|
||||
assert p.validate("2.0") is True
|
||||
assert p.validate("x") is False
|
||||
assert p.validate(2.0) is False
|
||||
|
||||
assert p.default() == "2.0"
|
||||
|
||||
|
||||
def test_id_property():
|
||||
idprop = IDProperty('my-type')
|
||||
|
||||
assert idprop.validate('my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') is True
|
||||
assert idprop.validate('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') is False
|
||||
assert idprop.validate(idprop.default())
|
Loading…
Reference in New Issue