Fix TimestampProperty

- improved timestamp formatting
- python-stix2 will only include subsecond values if they don't equal 0
- in Python 3.6, datetime.astimezone doesn't throw an error on naive
  timestamps as in previous versions
stix2.1
clenk 2017-04-17 10:48:13 -04:00
parent 35981025c5
commit b4f116a33f
4 changed files with 42 additions and 32 deletions

View File

@ -1,7 +1,8 @@
"""STIX 2.0 Marking Objects""" """STIX 2.0 Marking Objects"""
from .base import _STIXBase 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 from .utils import NOW
@ -15,7 +16,7 @@ class GranularMarking(_STIXBase):
class MarkingDefinition(_STIXBase): class MarkingDefinition(_STIXBase):
_type = 'marking-definition' _type = 'marking-definition'
_properties = { _properties = {
'created': Property(default=lambda: NOW), 'created': TimestampProperty(default=lambda: NOW),
'external_references': Property(), 'external_references': Property(),
'created_by_ref': ReferenceProperty(type="identity"), 'created_by_ref': ReferenceProperty(type="identity"),
'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")), 'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")),

View File

@ -206,25 +206,26 @@ class BooleanProperty(Property):
class TimestampProperty(Property): class TimestampProperty(Property):
def validate(self, value): def validate(self, value):
if isinstance(value, dt.datetime): if isinstance(value, dt.date):
return value if hasattr(value, 'hour'):
elif isinstance(value, dt.date): return value
return dt.datetime.combine(value, dt.time()) 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: try:
return parser.parse(value).astimezone(pytz.utc) parsed = parser.parse(value)
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.")
except TypeError: except TypeError:
# Isn't a string # Unknown format
raise ValueError("must be a datetime object, date object, or " 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}" REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}"

View File

@ -3,7 +3,7 @@ from stix2.markings import TLP_WHITE
import pytest import pytest
EXPECTED_TLP_MARKING_DEFINITION = """{ EXPECTED_TLP_MARKING_DEFINITION = """{
"created": "2017-01-20T00:00:00.000Z", "created": "2017-01-20T00:00:00Z",
"definition": { "definition": {
"tlp": "white" "tlp": "white"
}, },
@ -13,7 +13,7 @@ EXPECTED_TLP_MARKING_DEFINITION = """{
}""" }"""
EXPECTED_STATEMENT_MARKING_DEFINITION = """{ EXPECTED_STATEMENT_MARKING_DEFINITION = """{
"created": "2017-01-20T00:00:00.000Z", "created": "2017-01-20T00:00:00Z",
"definition": { "definition": {
"statement": "Copyright 2016, Example Corp" "statement": "Copyright 2016, Example Corp"
}, },
@ -33,7 +33,7 @@ EXPECTED_GRANULAR_MARKING = """{
}""" }"""
EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ 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", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Campaign by Green Group against a series of targets in the financial services sector.", "description": "Campaign by Green Group against a series of targets in the financial services sector.",
"granular_markings": [ "granular_markings": [
@ -45,7 +45,7 @@ EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{
} }
], ],
"id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "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", "name": "Green Group Attacks Against Finance",
"type": "campaign" "type": "campaign"
}""" }"""
@ -100,8 +100,8 @@ def test_campaign_with_granular_markings_example():
campaign = stix2.Campaign( campaign = stix2.Campaign(
id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:00.000Z", created="2016-04-06T20:03:00Z",
modified="2016-04-06T20:03:00.000Z", modified="2016-04-06T20:03:00Z",
name="Green Group Attacks Against Finance", name="Green Group Attacks Against Finance",
description="Campaign by Green Group against a series of targets in the financial services sector.", description="Campaign by Green Group against a series of targets in the financial services sector.",
granular_markings=[ granular_markings=[

View File

@ -1,7 +1,7 @@
"""Utility functions and classes for the stix2 library.""" """Utility functions and classes for the stix2 library."""
import datetime as dt import datetime as dt
import time
import pytz import pytz
# Sentinel value for fields that should be set to the current time. # Sentinel value for fields that should be set to the current time.
@ -15,12 +15,20 @@ def get_timestamp():
def format_datetime(dttm): 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 try:
# 2. Format in ISO format zoned = dttm.astimezone(pytz.utc)
# 3. Strip off "+00:00" except ValueError:
# 4. Add "Z" # dttm is timezone-naive
tz_name = time.tzname[time.localtime().tm_isdst]
# TODO: how to handle timestamps with subsecond 0's zoned = pytz.timezone(tz_name).localize(dttm).astimezone(pytz.utc)
return dttm.astimezone(pytz.utc).isoformat()[:-6] + "Z" 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"