diff --git a/stix2/markings.py b/stix2/markings.py index aff2ccb..7742bbf 100644 --- a/stix2/markings.py +++ b/stix2/markings.py @@ -1,7 +1,8 @@ """STIX 2.0 Marking Objects""" from .base import _STIXBase -from .properties import IDProperty, TypeProperty, ListProperty, ReferenceProperty, Property, SelectorProperty +from .properties import (IDProperty, TypeProperty, ListProperty, TimestampProperty, + ReferenceProperty, Property, SelectorProperty) from .utils import NOW @@ -15,7 +16,7 @@ class GranularMarking(_STIXBase): class MarkingDefinition(_STIXBase): _type = 'marking-definition' _properties = { - 'created': Property(default=lambda: NOW), + 'created': TimestampProperty(default=lambda: NOW), 'external_references': Property(), 'created_by_ref': ReferenceProperty(type="identity"), 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")), diff --git a/stix2/properties.py b/stix2/properties.py index 1ed09a1..e2ad31e 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -206,25 +206,26 @@ class BooleanProperty(Property): class TimestampProperty(Property): def validate(self, value): - if isinstance(value, dt.datetime): - return value - elif isinstance(value, dt.date): - return dt.datetime.combine(value, dt.time()) + 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.timezone('US/Eastern')) + # value isn't a date or datetime object so assume it's a string try: - return parser.parse(value).astimezone(pytz.utc) - except ValueError: - # Doesn't have timezone info in the string - try: - return pytz.utc.localize(parser.parse(value)) - except TypeError: - # Unknown format - raise ValueError("must be a datetime object, date object, or " - "timestamp string in a recognizable format.") + parsed = parser.parse(value) except TypeError: - # Isn't a string + # Unknown format raise ValueError("must be a datetime object, date object, or " - "timestamp string.") + "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 + # TODO Should we default to system local timezone instead? + return pytz.utc.localize(parsed) REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py index fd9b376..78e42f9 100644 --- a/stix2/test/test_markings.py +++ b/stix2/test/test_markings.py @@ -3,7 +3,7 @@ from stix2.markings import TLP_WHITE import pytest EXPECTED_TLP_MARKING_DEFINITION = """{ - "created": "2017-01-20T00:00:00.000Z", + "created": "2017-01-20T00:00:00Z", "definition": { "tlp": "white" }, @@ -13,7 +13,7 @@ EXPECTED_TLP_MARKING_DEFINITION = """{ }""" EXPECTED_STATEMENT_MARKING_DEFINITION = """{ - "created": "2017-01-20T00:00:00.000Z", + "created": "2017-01-20T00:00:00Z", "definition": { "statement": "Copyright 2016, Example Corp" }, @@ -33,7 +33,7 @@ EXPECTED_GRANULAR_MARKING = """{ }""" EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ - "created": "2016-04-06T20:03:00.000Z", + "created": "2016-04-06T20:03:00Z", "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": [ @@ -45,7 +45,7 @@ EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ } ], "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", - "modified": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00Z", "name": "Green Group Attacks Against Finance", "type": "campaign" }""" @@ -100,8 +100,8 @@ def test_campaign_with_granular_markings_example(): campaign = stix2.Campaign( id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", - created="2016-04-06T20:03:00.000Z", - modified="2016-04-06T20:03:00.000Z", + created="2016-04-06T20:03:00Z", + modified="2016-04-06T20:03:00Z", name="Green Group Attacks Against Finance", description="Campaign by Green Group against a series of targets in the financial services sector.", granular_markings=[ diff --git a/stix2/utils.py b/stix2/utils.py index 0acd5de..eafaa76 100644 --- a/stix2/utils.py +++ b/stix2/utils.py @@ -1,7 +1,7 @@ """Utility functions and classes for the stix2 library.""" import datetime as dt - +import time import pytz # Sentinel value for fields that should be set to the current time. @@ -15,12 +15,20 @@ def get_timestamp(): def format_datetime(dttm): - # TODO: how to handle naive datetime + # 1. Convert to timezone-aware + # 2. Convert to UTC + # 3. Format in ISO format + # 4. Add subsecond value if non-zero + # 5. Add "Z" - # 1. Convert to UTC - # 2. Format in ISO format - # 3. Strip off "+00:00" - # 4. Add "Z" - - # TODO: how to handle timestamps with subsecond 0's - return dttm.astimezone(pytz.utc).isoformat()[:-6] + "Z" + try: + zoned = dttm.astimezone(pytz.utc) + except ValueError: + # dttm is timezone-naive + tz_name = time.tzname[time.localtime().tm_isdst] + zoned = pytz.timezone(tz_name).localize(dttm).astimezone(pytz.utc) + ts = zoned.strftime("%Y-%m-%dT%H:%M:%S") + if zoned.microsecond > 0: + ms = zoned.strftime("%f") + ts = ts + '.' + ms.rstrip("0") + return ts + "Z"