Skip to content

Commit

Permalink
Merge pull request #374 from chisholm/version_precision
Browse files Browse the repository at this point in the history
Support STIX 2.1 version precision
  • Loading branch information
clenk authored Apr 3, 2020
2 parents 0d77097 + 1741cc9 commit 9145bdf
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 87 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def get_long_description():
keywords='stix stix2 json cti cyber threat intelligence',
packages=find_packages(exclude=['*.test', '*.test.*']),
install_requires=[
'enum34 ; python_version<"3.4"',
'python-dateutil',
'pytz',
'requests',
Expand Down
8 changes: 6 additions & 2 deletions stix2/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,16 @@ def clean(self, value):

class TimestampProperty(Property):

def __init__(self, precision=None, **kwargs):
def __init__(self, precision="any", precision_constraint="exact", **kwargs):
self.precision = precision
self.precision_constraint = precision_constraint

super(TimestampProperty, self).__init__(**kwargs)

def clean(self, value):
return parse_into_datetime(value, self.precision)
return parse_into_datetime(
value, self.precision, self.precision_constraint,
)


class DictionaryProperty(Property):
Expand Down
4 changes: 2 additions & 2 deletions stix2/test/v21/test_attack_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ def test_attack_pattern_invalid_labels():
def test_overly_precise_timestamps():
ap = stix2.v21.AttackPattern(
id=ATTACK_PATTERN_ID,
created="2016-05-12T08:17:27.0000342Z",
modified="2016-05-12T08:17:27.000287Z",
created="2016-05-12T08:17:27.000000342Z",
modified="2016-05-12T08:17:27.000000287Z",
name="Spear Phishing",
external_references=[{
"source_name": "capec",
Expand Down
164 changes: 164 additions & 0 deletions stix2/test/v21/test_timestamp_precision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import datetime
import sys

import pytest

import stix2
from stix2.utils import (
Precision, PrecisionConstraint, STIXdatetime, _to_enum, format_datetime,
parse_into_datetime,
)

_DT = datetime.datetime.utcnow()
# intentionally omit microseconds from the following. We add it in as
# needed for each test.
_DT_STR = _DT.strftime("%Y-%m-%dT%H:%M:%S")


@pytest.mark.parametrize(
"value, enum_type, enum_default, enum_expected", [
("second", Precision, None, Precision.SECOND),
(
"eXaCt", PrecisionConstraint, PrecisionConstraint.MIN,
PrecisionConstraint.EXACT,
),
(None, Precision, Precision.MILLISECOND, Precision.MILLISECOND),
(Precision.ANY, Precision, None, Precision.ANY),
],
)
def test_to_enum(value, enum_type, enum_default, enum_expected):
result = _to_enum(value, enum_type, enum_default)
assert result == enum_expected


@pytest.mark.parametrize(
"value, err_type", [
("foo", KeyError),
(1, TypeError),
(PrecisionConstraint.EXACT, TypeError),
(None, TypeError),
],
)
def test_to_enum_errors(value, err_type):
with pytest.raises(err_type):
_to_enum(value, Precision)


@pytest.mark.xfail(
sys.version_info[:2] == (3, 6), strict=True,
reason="https://bugs.python.org/issue32404",
)
def test_stix_datetime_now():
dt = STIXdatetime.utcnow()
assert dt.precision is Precision.ANY
assert dt.precision_constraint is PrecisionConstraint.EXACT


def test_stix_datetime():
dt = datetime.datetime.utcnow()

sdt = STIXdatetime(dt, precision=Precision.SECOND)
assert sdt.precision is Precision.SECOND
assert sdt == dt

sdt = STIXdatetime(
dt,
precision_constraint=PrecisionConstraint.EXACT,
)
assert sdt.precision_constraint is PrecisionConstraint.EXACT
assert sdt == dt


@pytest.mark.parametrize(
"us, precision, precision_constraint, expected_truncated_us", [
(123456, Precision.ANY, PrecisionConstraint.EXACT, 123456),
(123456, Precision.SECOND, PrecisionConstraint.EXACT, 0),
(123456, Precision.SECOND, PrecisionConstraint.MIN, 123456),
(123456, Precision.MILLISECOND, PrecisionConstraint.EXACT, 123000),
(123456, Precision.MILLISECOND, PrecisionConstraint.MIN, 123456),
(1234, Precision.MILLISECOND, PrecisionConstraint.EXACT, 1000),
(123, Precision.MILLISECOND, PrecisionConstraint.EXACT, 0),
],
)
def test_parse_datetime(
us, precision, precision_constraint, expected_truncated_us,
):

# complete the datetime string with microseconds
dt_us_str = "{}.{:06d}Z".format(_DT_STR, us)

sdt = parse_into_datetime(
dt_us_str,
precision=precision,
precision_constraint=precision_constraint,
)

assert sdt.precision is precision
assert sdt.precision_constraint is precision_constraint
assert sdt.microsecond == expected_truncated_us


@pytest.mark.parametrize(
"us, precision, precision_constraint, expected_us_str", [
(123456, Precision.ANY, PrecisionConstraint.EXACT, ".123456"),
(123456, Precision.SECOND, PrecisionConstraint.EXACT, ""),
(123456, Precision.SECOND, PrecisionConstraint.MIN, ".123456"),
(123456, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".123"),
(123456, Precision.MILLISECOND, PrecisionConstraint.MIN, ".123456"),
(0, Precision.SECOND, PrecisionConstraint.MIN, ""),
(0, Precision.MILLISECOND, PrecisionConstraint.MIN, ".000"),
(0, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".000"),
(1000, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".001"),
(10000, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".010"),
(100000, Precision.MILLISECOND, PrecisionConstraint.EXACT, ".100"),
(1000, Precision.ANY, PrecisionConstraint.EXACT, ".001"),
(10000, Precision.ANY, PrecisionConstraint.EXACT, ".01"),
(100000, Precision.ANY, PrecisionConstraint.EXACT, ".1"),
(1001, Precision.MILLISECOND, PrecisionConstraint.MIN, ".001001"),
(10010, Precision.MILLISECOND, PrecisionConstraint.MIN, ".01001"),
(100100, Precision.MILLISECOND, PrecisionConstraint.MIN, ".1001"),
],
)
def test_format_datetime(us, precision, precision_constraint, expected_us_str):

dt = _DT.replace(microsecond=us)
expected_dt_str = "{}{}Z".format(_DT_STR, expected_us_str)

sdt = STIXdatetime(
dt,
precision=precision,
precision_constraint=precision_constraint,
)
s = format_datetime(sdt)
assert s == expected_dt_str


def test_sdo_extra_precision():
# add extra precision for "modified", ensure it's not lost
identity_dict = {
"type": "identity",
"id": "identity--4a457eeb-6639-4aa3-be81-5930a3000c39",
"created": "2015-12-21T19:59:11.000Z",
"modified": "2015-12-21T19:59:11.0001Z",
"name": "John Smith",
"identity_class": "individual",
"spec_version": "2.1",
}

identity_obj = stix2.parse(identity_dict)
assert identity_obj.modified.microsecond == 100
assert identity_obj.modified.precision is Precision.MILLISECOND
assert identity_obj.modified.precision_constraint is PrecisionConstraint.MIN

identity_str = identity_obj.serialize(pretty=True)

# ensure precision is retained in JSON
assert identity_str == """{
"type": "identity",
"spec_version": "2.1",
"id": "identity--4a457eeb-6639-4aa3-be81-5930a3000c39",
"created": "2015-12-21T19:59:11.000Z",
"modified": "2015-12-21T19:59:11.0001Z",
"name": "John Smith",
"identity_class": "individual"
}"""
36 changes: 34 additions & 2 deletions stix2/test/v21/test_versioning.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import datetime

import pytest

import stix2
import stix2.utils

from .constants import CAMPAIGN_MORE_KWARGS

Expand Down Expand Up @@ -236,8 +239,7 @@ def test_remove_custom_stix_property():
mal_nc = stix2.utils.remove_custom_stix(mal)

assert "x_custom" not in mal_nc
assert (stix2.utils.parse_into_datetime(mal["modified"], precision="millisecond") <
stix2.utils.parse_into_datetime(mal_nc["modified"], precision="millisecond"))
assert mal["modified"] < mal_nc["modified"]


def test_remove_custom_stix_object():
Expand All @@ -264,3 +266,33 @@ def test_remove_custom_stix_no_custom():
assert len(campaign_v1.keys()) == len(campaign_v2.keys())
assert campaign_v1.id == campaign_v2.id
assert campaign_v1.description == campaign_v2.description


@pytest.mark.parametrize(
"old, candidate_new, expected_new, use_stix21", [
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", "1999-08-15T00:19:07.001Z", False),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", False),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.000Z", "1999-08-15T00:19:07.001Z", False),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.999Z", "1999-08-15T00:19:07.001Z", False),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.0001Z", "1999-08-15T00:19:07.001Z", False),
("1999-08-15T00:19:07.999Z", "1999-08-15T00:19:07.9999Z", "1999-08-15T00:19:08.000Z", False),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.001Z", "1999-08-15T00:19:07.001Z", True),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000001Z", True),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.000Z", "1999-08-15T00:19:07.000001Z", True),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:06.999999Z", "1999-08-15T00:19:07.000001Z", True),
("1999-08-15T00:19:07.000Z", "1999-08-15T00:19:07.000001Z", "1999-08-15T00:19:07.000001Z", True),
("1999-08-15T00:19:07.999Z", "1999-08-15T00:19:07.999999Z", "1999-08-15T00:19:07.999999Z", True),
],
)
def test_fudge_modified(old, candidate_new, expected_new, use_stix21):
old_dt = datetime.datetime.strptime(old, "%Y-%m-%dT%H:%M:%S.%fZ")
candidate_new_dt = datetime.datetime.strptime(
candidate_new, "%Y-%m-%dT%H:%M:%S.%fZ",
)
expected_new_dt = datetime.datetime.strptime(
expected_new, "%Y-%m-%dT%H:%M:%S.%fZ",
)

fudged = stix2.utils._fudge_modified(old_dt, candidate_new_dt, use_stix21)
assert fudged == expected_new_dt
Loading

0 comments on commit 9145bdf

Please sign in to comment.