diff --git a/stix2/base.py b/stix2/base.py index cb17f11..7de193b 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -176,7 +176,7 @@ class _STIXBase(collections.Mapping): if 'modified' not in kwargs: kwargs['modified'] = get_timestamp() else: - new_modified_property = parse_into_datetime(kwargs['modified']) + new_modified_property = parse_into_datetime(kwargs['modified'], precision='millisecond') if new_modified_property < self.modified: raise InvalidValueError(cls, 'modified', "The new modified datetime cannot be before the current modified datatime.") new_obj_inner.update(kwargs) diff --git a/stix2/common.py b/stix2/common.py index c8c243d..7c6e747 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -7,8 +7,8 @@ from .utils import NOW COMMON_PROPERTIES = { # 'type' and 'id' should be defined on each individual type - 'created': TimestampProperty(default=lambda: NOW), - 'modified': TimestampProperty(default=lambda: NOW), + 'created': TimestampProperty(default=lambda: NOW, precision='millisecond'), + 'modified': TimestampProperty(default=lambda: NOW, precision='millisecond'), 'external_references': ListProperty(ExternalReference), 'revoked': BooleanProperty(), 'labels': ListProperty(StringProperty), diff --git a/stix2/observables.py b/stix2/observables.py index 086dc45..a8f3b67 100644 --- a/stix2/observables.py +++ b/stix2/observables.py @@ -215,7 +215,7 @@ class WindowsPEBinaryExt(_Extension): 'imphash': StringProperty(), 'machine_hex': HexProperty(), 'number_of_sections': IntegerProperty(), - 'time_date_stamp': TimestampProperty(), + 'time_date_stamp': TimestampProperty(precision='second'), 'pointer_to_symbol_table_hex': HexProperty(), 'number_of_symbols': IntegerProperty(), 'size_of_optional_header': IntegerProperty(), diff --git a/stix2/properties.py b/stix2/properties.py index 9d54cf0..80e5345 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,18 +1,15 @@ import base64 import binascii import collections -import datetime as dt import inspect import re import uuid -from dateutil import parser -import pytz from six import text_type from .base import _Observable, _STIXBase from .exceptions import DictionaryKeyError -from .utils import get_dict +from .utils import get_dict, parse_into_datetime class Property(object): @@ -215,26 +212,12 @@ class BooleanProperty(Property): class TimestampProperty(Property): - def clean(self, value): - if isinstance(value, dt.date): - if hasattr(value, 'hour'): - return value - else: - # Add a time component - return dt.datetime.combine(value, dt.time(), tzinfo=pytz.utc) + def __init__(self, precision=None, **kwargs): + self.precision = precision + super(TimestampProperty, self).__init__(**kwargs) - # value isn't a date or datetime object so assume it's a string - try: - parsed = parser.parse(value) - except TypeError: - # Unknown format - raise ValueError("must be a datetime object, date object, or " - "timestamp string in a recognizable format.") - if parsed.tzinfo: - return parsed.astimezone(pytz.utc) - else: - # Doesn't have timezone info in the string; assume UTC - return pytz.utc.localize(parsed) + def clean(self, value): + return parse_into_datetime(value, self.precision) class ObservableProperty(Property): diff --git a/stix2/test/conftest.py b/stix2/test/conftest.py index d1f3330..9f61bc2 100644 --- a/stix2/test/conftest.py +++ b/stix2/test/conftest.py @@ -1,4 +1,3 @@ -import datetime as dt import uuid import pytest @@ -13,12 +12,12 @@ from .constants import (FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS, @pytest.fixture def clock(monkeypatch): - class mydatetime(dt.datetime): + class mydatetime(stix2.utils.STIXdatetime): @classmethod def now(cls, tz=None): return FAKE_TIME - monkeypatch.setattr(dt, 'datetime', mydatetime) + monkeypatch.setattr(stix2.utils, 'STIXdatetime', mydatetime) @pytest.fixture diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/test_attack_pattern.py index 7510888..5bd5af2 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/test_attack_pattern.py @@ -9,7 +9,7 @@ from .constants import ATTACK_PATTERN_ID EXPECTED = """{ - "created": "2016-05-12T08:17:27Z", + "created": "2016-05-12T08:17:27.000Z", "description": "...", "external_references": [ { @@ -18,7 +18,7 @@ EXPECTED = """{ } ], "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "modified": "2016-05-12T08:17:27Z", + "modified": "2016-05-12T08:17:27.000Z", "name": "Spear Phishing", "type": "attack-pattern" }""" @@ -27,8 +27,8 @@ EXPECTED = """{ def test_attack_pattern_example(): ap = stix2.AttackPattern( id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - created="2016-05-12T08:17:27Z", - modified="2016-05-12T08:17:27Z", + created="2016-05-12T08:17:27.000Z", + modified="2016-05-12T08:17:27.000Z", name="Spear Phishing", external_references=[{ "source_name": "capec", @@ -45,8 +45,8 @@ def test_attack_pattern_example(): { "type": "attack-pattern", "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "created": "2016-05-12T08:17:27Z", - "modified": "2016-05-12T08:17:27Z", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", "description": "...", "external_references": [ { diff --git a/stix2/test/test_bundle.py b/stix2/test/test_bundle.py index d52a955..fc3e350 100644 --- a/stix2/test/test_bundle.py +++ b/stix2/test/test_bundle.py @@ -7,30 +7,30 @@ EXPECTED_BUNDLE = """{ "id": "bundle--00000000-0000-0000-0000-000000000004", "objects": [ { - "created": "2017-01-01T12:34:56Z", + "created": "2017-01-01T12:34:56.000Z", "id": "indicator--00000000-0000-0000-0000-000000000001", "labels": [ "malicious-activity" ], - "modified": "2017-01-01T12:34:56Z", + "modified": "2017-01-01T12:34:56.000Z", "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "type": "indicator", "valid_from": "2017-01-01T12:34:56Z" }, { - "created": "2017-01-01T12:34:56Z", + "created": "2017-01-01T12:34:56.000Z", "id": "malware--00000000-0000-0000-0000-000000000002", "labels": [ "ransomware" ], - "modified": "2017-01-01T12:34:56Z", + "modified": "2017-01-01T12:34:56.000Z", "name": "Cryptolocker", "type": "malware" }, { - "created": "2017-01-01T12:34:56Z", + "created": "2017-01-01T12:34:56.000Z", "id": "relationship--00000000-0000-0000-0000-000000000003", - "modified": "2017-01-01T12:34:56Z", + "modified": "2017-01-01T12:34:56.000Z", "relationship_type": "indicates", "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210", diff --git a/stix2/test/test_campaign.py b/stix2/test/test_campaign.py index 9920019..30b9444 100644 --- a/stix2/test/test_campaign.py +++ b/stix2/test/test_campaign.py @@ -9,11 +9,11 @@ from .constants import CAMPAIGN_ID EXPECTED = """{ - "created": "2016-04-06T20:03:00Z", + "created": "2016-04-06T20:03:00.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "Campaign by Green Group against a series of targets in the financial services sector.", "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "modified": "2016-04-06T20:03:00Z", + "modified": "2016-04-06T20:03:00.000Z", "name": "Green Group Attacks Against Finance", "type": "campaign" }""" diff --git a/stix2/test/test_course_of_action.py b/stix2/test/test_course_of_action.py index 263eae2..e7a1b22 100644 --- a/stix2/test/test_course_of_action.py +++ b/stix2/test/test_course_of_action.py @@ -9,11 +9,11 @@ from .constants import COURSE_OF_ACTION_ID EXPECTED = """{ - "created": "2016-04-06T20:03:48Z", + "created": "2016-04-06T20:03:48.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "modified": "2016-04-06T20:03:48Z", + "modified": "2016-04-06T20:03:48.000Z", "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", "type": "course-of-action" }""" @@ -23,8 +23,8 @@ def test_course_of_action_example(): coa = stix2.CourseOfAction( id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48Z", - modified="2016-04-06T20:03:48Z", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", name="Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." ) @@ -35,11 +35,11 @@ def test_course_of_action_example(): @pytest.mark.parametrize("data", [ EXPECTED, { - "created": "2016-04-06T20:03:48Z", + "created": "2016-04-06T20:03:48.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "modified": "2016-04-06T20:03:48Z", + "modified": "2016-04-06T20:03:48.000Z", "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", "type": "course-of-action" }, diff --git a/stix2/test/test_fixtures.py b/stix2/test/test_fixtures.py index 9078972..83d5f85 100644 --- a/stix2/test/test_fixtures.py +++ b/stix2/test/test_fixtures.py @@ -1,11 +1,12 @@ -import datetime as dt import uuid +from stix2 import utils + from .constants import FAKE_TIME def test_clock(clock): - assert dt.datetime.now() == FAKE_TIME + assert utils.STIXdatetime.now() == FAKE_TIME def test_my_uuid4_fixture(uuid4): diff --git a/stix2/test/test_identity.py b/stix2/test/test_identity.py index b2c166c..ed51958 100644 --- a/stix2/test/test_identity.py +++ b/stix2/test/test_identity.py @@ -9,10 +9,10 @@ from .constants import IDENTITY_ID EXPECTED = """{ - "created": "2015-12-21T19:59:11Z", + "created": "2015-12-21T19:59:11.000Z", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "identity_class": "individual", - "modified": "2015-12-21T19:59:11Z", + "modified": "2015-12-21T19:59:11.000Z", "name": "John Smith", "type": "identity" }""" @@ -21,8 +21,8 @@ EXPECTED = """{ def test_identity_example(): identity = stix2.Identity( id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", - created="2015-12-21T19:59:11Z", - modified="2015-12-21T19:59:11Z", + created="2015-12-21T19:59:11.000Z", + modified="2015-12-21T19:59:11.000Z", name="John Smith", identity_class="individual", ) @@ -33,10 +33,10 @@ def test_identity_example(): @pytest.mark.parametrize("data", [ EXPECTED, { - "created": "2015-12-21T19:59:11Z", + "created": "2015-12-21T19:59:11.000Z", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "identity_class": "individual", - "modified": "2015-12-21T19:59:11Z", + "modified": "2015-12-21T19:59:11.000Z", "name": "John Smith", "type": "identity" }, @@ -56,8 +56,8 @@ def test_parse_no_type(): stix2.parse(""" { "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", - "created": "2015-12-21T19:59:11Z", - "modified": "2015-12-21T19:59:11Z", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.000Z", "name": "John Smith", "identity_class": "individual" }""") diff --git a/stix2/test/test_indicator.py b/stix2/test/test_indicator.py index 5db50e6..5daa0f6 100644 --- a/stix2/test/test_indicator.py +++ b/stix2/test/test_indicator.py @@ -10,22 +10,22 @@ from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS EXPECTED_INDICATOR = """{ - "created": "2017-01-01T00:00:01Z", + "created": "2017-01-01T00:00:01.000Z", "id": "indicator--01234567-89ab-cdef-0123-456789abcdef", "labels": [ "malicious-activity" ], - "modified": "2017-01-01T00:00:01Z", + "modified": "2017-01-01T00:00:01.000Z", "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "type": "indicator", "valid_from": "1970-01-01T00:00:01Z" }""" EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" - created=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=), + created=STIXdatetime(2017, 1, 1, 0, 0, 1, tzinfo=), id='indicator--01234567-89ab-cdef-0123-456789abcdef', labels=['malicious-activity'], - modified=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=), + modified=STIXdatetime(2017, 1, 1, 0, 0, 1, tzinfo=), pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", type='indicator', valid_from=datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=) @@ -48,6 +48,8 @@ def test_indicator_with_all_required_properties(): assert str(ind) == EXPECTED_INDICATOR rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(ind)) + print(rep) + print(EXPECTED_INDICATOR_REPR) assert rep == EXPECTED_INDICATOR_REPR diff --git a/stix2/test/test_intrusion_set.py b/stix2/test/test_intrusion_set.py index 3241ced..a6eee7f 100644 --- a/stix2/test/test_intrusion_set.py +++ b/stix2/test/test_intrusion_set.py @@ -12,7 +12,7 @@ EXPECTED = """{ "aliases": [ "Zookeeper" ], - "created": "2016-04-06T20:03:48Z", + "created": "2016-04-06T20:03:48.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "Incidents usually feature a shared TTP of a bobcat being released...", "goals": [ @@ -21,7 +21,7 @@ EXPECTED = """{ "damage" ], "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", - "modified": "2016-04-06T20:03:48Z", + "modified": "2016-04-06T20:03:48.000Z", "name": "Bobcat Breakin", "type": "intrusion-set" }""" @@ -31,8 +31,8 @@ def test_intrusion_set_example(): intrusion_set = stix2.IntrusionSet( id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48Z", - modified="2016-04-06T20:03:48Z", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", name="Bobcat Breakin", description="Incidents usually feature a shared TTP of a bobcat being released...", aliases=["Zookeeper"], @@ -48,7 +48,7 @@ def test_intrusion_set_example(): "aliases": [ "Zookeeper" ], - "created": "2016-04-06T20:03:48Z", + "created": "2016-04-06T20:03:48.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "Incidents usually feature a shared TTP of a bobcat being released...", "goals": [ @@ -57,7 +57,7 @@ def test_intrusion_set_example(): "damage" ], "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", - "modified": "2016-04-06T20:03:48Z", + "modified": "2016-04-06T20:03:48.000Z", "name": "Bobcat Breakin", "type": "intrusion-set" }, diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index 266d012..ff0b394 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -10,12 +10,12 @@ from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS EXPECTED_MALWARE = """{ - "created": "2016-05-12T08:17:27Z", + "created": "2016-05-12T08:17:27.000Z", "id": "malware--fedcba98-7654-3210-fedc-ba9876543210", "labels": [ "ransomware" ], - "modified": "2016-05-12T08:17:27Z", + "modified": "2016-05-12T08:17:27.000Z", "name": "Cryptolocker", "type": "malware" }""" @@ -109,8 +109,8 @@ def test_invalid_kwarg_to_malware(): { "type": "malware", "id": "malware--fedcba98-7654-3210-fedc-ba9876543210", - "created": "2016-05-12T08:17:27Z", - "modified": "2016-05-12T08:17:27Z", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", "labels": ["ransomware"], "name": "Cryptolocker", }, diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py index f1f07db..c2e0276 100644 --- a/stix2/test/test_markings.py +++ b/stix2/test/test_markings.py @@ -40,7 +40,7 @@ EXPECTED_GRANULAR_MARKING = """{ }""" EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ - "created": "2016-04-06T20:03:00Z", + "created": "2016-04-06T20:03:00.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "Campaign by Green Group against a series of targets in the financial services sector.", "granular_markings": [ @@ -52,7 +52,7 @@ EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ } ], "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "modified": "2016-04-06T20:03:00Z", + "modified": "2016-04-06T20:03:00.000Z", "name": "Green Group Attacks Against Finance", "type": "campaign" }""" diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 0ed9954..75f3070 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -10,12 +10,12 @@ from .constants import OBSERVED_DATA_ID EXPECTED = """{ - "created": "2016-04-06T19:58:16Z", + "created": "2016-04-06T19:58:16.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "first_observed": "2015-12-21T19:00:00Z", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "last_observed": "2015-12-21T19:00:00Z", - "modified": "2016-04-06T19:58:16Z", + "modified": "2016-04-06T19:58:16.000Z", "number_observed": 50, "objects": { "0": { @@ -31,8 +31,8 @@ def test_observed_data_example(): observed_data = stix2.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T19:58:16Z", - modified="2016-04-06T19:58:16Z", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", number_observed=50, @@ -48,12 +48,12 @@ def test_observed_data_example(): EXPECTED_WITH_REF = """{ - "created": "2016-04-06T19:58:16Z", + "created": "2016-04-06T19:58:16.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "first_observed": "2015-12-21T19:00:00Z", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "last_observed": "2015-12-21T19:00:00Z", - "modified": "2016-04-06T19:58:16Z", + "modified": "2016-04-06T19:58:16.000Z", "number_observed": 50, "objects": { "0": { @@ -76,8 +76,8 @@ def test_observed_data_example_with_refs(): observed_data = stix2.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T19:58:16Z", - modified="2016-04-06T19:58:16Z", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", number_observed=50, @@ -102,8 +102,8 @@ def test_observed_data_example_with_bad_refs(): stix2.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T19:58:16Z", - modified="2016-04-06T19:58:16Z", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", number_observed=50, @@ -130,11 +130,11 @@ def test_observed_data_example_with_bad_refs(): { "type": "observed-data", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", - "created": "2016-04-06T19:58:16Z", + "created": "2016-04-06T19:58:16.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "first_observed": "2015-12-21T19:00:00Z", "last_observed": "2015-12-21T19:00:00Z", - "modified": "2016-04-06T19:58:16Z", + "modified": "2016-04-06T19:58:16.000Z", "number_observed": 50, "objects": { "0": { @@ -466,12 +466,12 @@ def test_parse_basic_tcp_traffic_with_error(data): EXPECTED_PROCESS_OD = """{ - "created": "2016-04-06T19:58:16Z", + "created": "2016-04-06T19:58:16.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "first_observed": "2015-12-21T19:00:00Z", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "last_observed": "2015-12-21T19:00:00Z", - "modified": "2016-04-06T19:58:16Z", + "modified": "2016-04-06T19:58:16.000Z", "number_observed": 50, "objects": { "0": { @@ -499,8 +499,8 @@ def test_observed_data_with_process_example(): observed_data = stix2.ObservedData( id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T19:58:16Z", - modified="2016-04-06T19:58:16Z", + created="2016-04-06T19:58:16.000Z", + modified="2016-04-06T19:58:16.000Z", first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", number_observed=50, diff --git a/stix2/test/test_relationship.py b/stix2/test/test_relationship.py index 1ad792c..362348b 100644 --- a/stix2/test/test_relationship.py +++ b/stix2/test/test_relationship.py @@ -10,9 +10,9 @@ from .constants import (FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID, EXPECTED_RELATIONSHIP = """{ - "created": "2016-04-06T20:06:37Z", + "created": "2016-04-06T20:06:37.000Z", "id": "relationship--00000000-1111-2222-3333-444444444444", - "modified": "2016-04-06T20:06:37Z", + "modified": "2016-04-06T20:06:37.000Z", "relationship_type": "indicates", "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210", diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py index cec217a..4a1f905 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -9,14 +9,14 @@ from .constants import INDICATOR_KWARGS, REPORT_ID EXPECTED = """{ - "created": "2015-12-21T19:59:11Z", + "created": "2015-12-21T19:59:11.000Z", "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", "description": "A simple report with an indicator and campaign", "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", "labels": [ "campaign" ], - "modified": "2015-12-21T19:59:11Z", + "modified": "2015-12-21T19:59:11.000Z", "name": "The Black Vine Cyberespionage Group", "object_refs": [ "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", @@ -32,8 +32,8 @@ def test_report_example(): report = stix2.Report( id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", - created="2015-12-21T19:59:11Z", - modified="2015-12-21T19:59:11Z", + created="2015-12-21T19:59:11.000Z", + modified="2015-12-21T19:59:11.000Z", name="The Black Vine Cyberespionage Group", description="A simple report with an indicator and campaign", published="2016-01-20T17:00:00Z", @@ -95,14 +95,14 @@ def test_report_example_objects_in_object_refs_with_bad_id(): @pytest.mark.parametrize("data", [ EXPECTED, { - "created": "2015-12-21T19:59:11Z", + "created": "2015-12-21T19:59:11.000Z", "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", "description": "A simple report with an indicator and campaign", "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", "labels": [ "campaign" ], - "modified": "2015-12-21T19:59:11Z", + "modified": "2015-12-21T19:59:11.000Z", "name": "The Black Vine Cyberespionage Group", "object_refs": [ "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", diff --git a/stix2/test/test_sighting.py b/stix2/test/test_sighting.py index 6c9b7d8..2036457 100644 --- a/stix2/test/test_sighting.py +++ b/stix2/test/test_sighting.py @@ -9,9 +9,9 @@ from .constants import INDICATOR_ID, SIGHTING_ID, SIGHTING_KWARGS EXPECTED_SIGHTING = """{ - "created": "2016-04-06T20:06:37Z", + "created": "2016-04-06T20:06:37.000Z", "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", - "modified": "2016-04-06T20:06:37Z", + "modified": "2016-04-06T20:06:37.000Z", "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", "type": "sighting", "where_sighted_refs": [ @@ -20,9 +20,9 @@ EXPECTED_SIGHTING = """{ }""" BAD_SIGHTING = """{ - "created": "2016-04-06T20:06:37Z", + "created": "2016-04-06T20:06:37.000Z", "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", - "modified": "2016-04-06T20:06:37Z", + "modified": "2016-04-06T20:06:37.000Z", "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", "type": "sighting", "where_sighted_refs": [ diff --git a/stix2/test/test_threat_actor.py b/stix2/test/test_threat_actor.py index 93e8179..1bab744 100644 --- a/stix2/test/test_threat_actor.py +++ b/stix2/test/test_threat_actor.py @@ -9,14 +9,14 @@ from .constants import THREAT_ACTOR_ID EXPECTED = """{ - "created": "2016-04-06T20:03:48Z", + "created": "2016-04-06T20:03:48.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "The Evil Org threat actor group", "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "labels": [ "crime-syndicate" ], - "modified": "2016-04-06T20:03:48Z", + "modified": "2016-04-06T20:03:48.000Z", "name": "Evil Org", "type": "threat-actor" }""" @@ -26,8 +26,8 @@ def test_threat_actor_example(): threat_actor = stix2.ThreatActor( id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48Z", - modified="2016-04-06T20:03:48Z", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", name="Evil Org", description="The Evil Org threat actor group", labels=["crime-syndicate"], @@ -39,14 +39,14 @@ def test_threat_actor_example(): @pytest.mark.parametrize("data", [ EXPECTED, { - "created": "2016-04-06T20:03:48Z", + "created": "2016-04-06T20:03:48.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "The Evil Org threat actor group", "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "labels": [ "crime-syndicate" ], - "modified": "2016-04-06T20:03:48Z", + "modified": "2016-04-06T20:03:48.000Z", "name": "Evil Org", "type": "threat-actor" }, diff --git a/stix2/test/test_tool.py b/stix2/test/test_tool.py index d7d3bef..04da7b3 100644 --- a/stix2/test/test_tool.py +++ b/stix2/test/test_tool.py @@ -9,13 +9,13 @@ from .constants import TOOL_ID EXPECTED = """{ - "created": "2016-04-06T20:03:48Z", + "created": "2016-04-06T20:03:48.000Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "labels": [ "remote-access" ], - "modified": "2016-04-06T20:03:48Z", + "modified": "2016-04-06T20:03:48.000Z", "name": "VNC", "type": "tool" }""" @@ -25,8 +25,8 @@ def test_tool_example(): tool = stix2.Tool( id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:48Z", - modified="2016-04-06T20:03:48Z", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", name="VNC", labels=["remote-access"], ) diff --git a/stix2/test/test_utils.py b/stix2/test/test_utils.py index dbc0ed5..c73bcd2 100644 --- a/stix2/test/test_utils.py +++ b/stix2/test/test_utils.py @@ -17,6 +17,8 @@ eastern = pytz.timezone('US/Eastern') (eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'), (dt.datetime(2017, 7, 1), '2017-07-01T00:00:00Z'), (dt.datetime(2017, 7, 1, 0, 0, 0, 1), '2017-07-01T00:00:00.000001Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='millisecond'), '2017-07-01T00:00:00.000Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='second'), '2017-07-01T00:00:00Z'), ]) def test_timestamp_formatting(dttm, timestamp): assert stix2.utils.format_datetime(dttm) == timestamp @@ -33,6 +35,17 @@ def test_parse_datetime(timestamp, dttm): assert stix2.utils.parse_into_datetime(timestamp) == dttm +@pytest.mark.parametrize('timestamp, dttm, precision', [ + ('2017-01-01T01:02:03.000001', dt.datetime(2017, 1, 1, 1, 2, 3, 0, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.001', dt.datetime(2017, 1, 1, 1, 2, 3, 1000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.1', dt.datetime(2017, 1, 1, 1, 2, 3, 100000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, 450000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, tzinfo=pytz.utc), 'second'), +]) +def test_parse_datetime_precision(timestamp, dttm, precision): + assert stix2.utils.parse_into_datetime(timestamp, precision) == dttm + + @pytest.mark.parametrize('ts', [ 'foobar', 1, diff --git a/stix2/test/test_vulnerability.py b/stix2/test/test_vulnerability.py index 751460c..27ab85f 100644 --- a/stix2/test/test_vulnerability.py +++ b/stix2/test/test_vulnerability.py @@ -9,7 +9,7 @@ from .constants import VULNERABILITY_ID EXPECTED = """{ - "created": "2016-05-12T08:17:27Z", + "created": "2016-05-12T08:17:27.000Z", "external_references": [ { "external_id": "CVE-2016-1234", @@ -17,7 +17,7 @@ EXPECTED = """{ } ], "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "modified": "2016-05-12T08:17:27Z", + "modified": "2016-05-12T08:17:27.000Z", "name": "CVE-2016-1234", "type": "vulnerability" }""" @@ -26,8 +26,8 @@ EXPECTED = """{ def test_vulnerability_example(): vulnerability = stix2.Vulnerability( id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - created="2016-05-12T08:17:27Z", - modified="2016-05-12T08:17:27Z", + created="2016-05-12T08:17:27.000Z", + modified="2016-05-12T08:17:27.000Z", name="CVE-2016-1234", external_references=[ stix2.ExternalReference(source_name='cve', diff --git a/stix2/utils.py b/stix2/utils.py index bb41937..12b889c 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -12,15 +12,29 @@ import pytz NOW = object() +class STIXdatetime(dt.datetime): + def __new__(cls, *args, **kwargs): + precision = kwargs.pop('precision', None) + if isinstance(args[0], dt.datetime): # Allow passing in a datetime object + dttm = args[0] + args = (dttm.year, dttm.month, dttm.day, dttm.hour, dttm.minute, + dttm.second, dttm.microsecond, dttm.tzinfo) + # self will be an instance of STIXdatetime, not dt.datetime + self = dt.datetime.__new__(cls, *args, **kwargs) + self.precision = precision + return self + + def get_timestamp(): - return dt.datetime.now(tz=pytz.UTC) + return STIXdatetime.now(tz=pytz.UTC) def format_datetime(dttm): # 1. Convert to timezone-aware # 2. Convert to UTC # 3. Format in ISO format - # 4. Add subsecond value if non-zero + # 4. Ensure correct precision + # 4a. Add subsecond value if non-zero and precision not defined # 5. Add "Z" if dttm.tzinfo is None or dttm.tzinfo.utcoffset(dttm) is None: @@ -29,32 +43,53 @@ def format_datetime(dttm): else: zoned = dttm.astimezone(pytz.utc) ts = zoned.strftime("%Y-%m-%dT%H:%M:%S") - if zoned.microsecond > 0: - ms = zoned.strftime("%f") + ms = zoned.strftime("%f") + precision = getattr(dttm, "precision", None) + if precision == 'second': + pass # Alredy precise to the second + elif precision == "millisecond": + ts = ts + '.' + ms[:3] + elif zoned.microsecond > 0: ts = ts + '.' + ms.rstrip("0") return ts + "Z" -def parse_into_datetime(value): +def parse_into_datetime(value, precision=None): if isinstance(value, dt.date): if hasattr(value, 'hour'): - return value + ts = value else: # Add a time component - return dt.datetime.combine(value, dt.time(0, 0, tzinfo=pytz.utc)) - - # value isn't a date or datetime object so assume it's a string - try: - parsed = parser.parse(value) - except (TypeError, ValueError): - # Unknown format - raise ValueError("must be a datetime object, date object, or " - "timestamp string in a recognizable format.") - if parsed.tzinfo: - return parsed.astimezone(pytz.utc) + ts = dt.datetime.combine(value, dt.time(0, 0, tzinfo=pytz.utc)) else: - # Doesn't have timezone info in the string; assume UTC - return pytz.utc.localize(parsed) + # value isn't a date or datetime object so assume it's a string + try: + parsed = parser.parse(value) + except (TypeError, ValueError): + # Unknown format + raise ValueError("must be a datetime object, date object, or " + "timestamp string in a recognizable format.") + if parsed.tzinfo: + ts = parsed.astimezone(pytz.utc) + else: + # Doesn't have timezone info in the string; assume UTC + ts = pytz.utc.localize(parsed) + + # Ensure correct precision + if not precision: + return ts + ms = ts.microsecond + if precision == 'second': + ts = ts.replace(microsecond=0) + elif precision == 'millisecond': + ms_len = len(str(ms)) + if ms_len > 3: + # Truncate to millisecond precision + factor = 10 ** (ms_len - 3) + ts = ts.replace(microsecond=(ts.microsecond // factor) * factor) + else: + ts = ts.replace(microsecond=0) + return STIXdatetime(ts, precision=precision) def get_dict(data):