Merge pull request #25 from oasis-open/issue24

Add timestamp precision for `created` and `modified`
stix2.1
Greg Back 2017-06-29 20:41:15 +00:00 committed by GitHub
commit f8e3a4f0e8
24 changed files with 170 additions and 137 deletions

View File

@ -176,7 +176,7 @@ class _STIXBase(collections.Mapping):
if 'modified' not in kwargs: if 'modified' not in kwargs:
kwargs['modified'] = get_timestamp() kwargs['modified'] = get_timestamp()
else: 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: if new_modified_property < self.modified:
raise InvalidValueError(cls, 'modified', "The new modified datetime cannot be before the current modified datatime.") raise InvalidValueError(cls, 'modified', "The new modified datetime cannot be before the current modified datatime.")
new_obj_inner.update(kwargs) new_obj_inner.update(kwargs)

View File

@ -7,8 +7,8 @@ from .utils import NOW
COMMON_PROPERTIES = { COMMON_PROPERTIES = {
# 'type' and 'id' should be defined on each individual type # 'type' and 'id' should be defined on each individual type
'created': TimestampProperty(default=lambda: NOW), 'created': TimestampProperty(default=lambda: NOW, precision='millisecond'),
'modified': TimestampProperty(default=lambda: NOW), 'modified': TimestampProperty(default=lambda: NOW, precision='millisecond'),
'external_references': ListProperty(ExternalReference), 'external_references': ListProperty(ExternalReference),
'revoked': BooleanProperty(), 'revoked': BooleanProperty(),
'labels': ListProperty(StringProperty), 'labels': ListProperty(StringProperty),

View File

@ -215,7 +215,7 @@ class WindowsPEBinaryExt(_Extension):
'imphash': StringProperty(), 'imphash': StringProperty(),
'machine_hex': HexProperty(), 'machine_hex': HexProperty(),
'number_of_sections': IntegerProperty(), 'number_of_sections': IntegerProperty(),
'time_date_stamp': TimestampProperty(), 'time_date_stamp': TimestampProperty(precision='second'),
'pointer_to_symbol_table_hex': HexProperty(), 'pointer_to_symbol_table_hex': HexProperty(),
'number_of_symbols': IntegerProperty(), 'number_of_symbols': IntegerProperty(),
'size_of_optional_header': IntegerProperty(), 'size_of_optional_header': IntegerProperty(),

View File

@ -1,18 +1,15 @@
import base64 import base64
import binascii import binascii
import collections import collections
import datetime as dt
import inspect import inspect
import re import re
import uuid import uuid
from dateutil import parser
import pytz
from six import text_type from six import text_type
from .base import _Observable, _STIXBase from .base import _Observable, _STIXBase
from .exceptions import DictionaryKeyError from .exceptions import DictionaryKeyError
from .utils import get_dict from .utils import get_dict, parse_into_datetime
class Property(object): class Property(object):
@ -215,26 +212,12 @@ class BooleanProperty(Property):
class TimestampProperty(Property): class TimestampProperty(Property):
def clean(self, value): def __init__(self, precision=None, **kwargs):
if isinstance(value, dt.date): self.precision = precision
if hasattr(value, 'hour'): super(TimestampProperty, self).__init__(**kwargs)
return value
else:
# Add a time component
return dt.datetime.combine(value, dt.time(), tzinfo=pytz.utc)
# value isn't a date or datetime object so assume it's a string def clean(self, value):
try: return parse_into_datetime(value, self.precision)
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)
class ObservableProperty(Property): class ObservableProperty(Property):

View File

@ -1,4 +1,3 @@
import datetime as dt
import uuid import uuid
import pytest import pytest
@ -13,12 +12,12 @@ from .constants import (FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS,
@pytest.fixture @pytest.fixture
def clock(monkeypatch): def clock(monkeypatch):
class mydatetime(dt.datetime): class mydatetime(stix2.utils.STIXdatetime):
@classmethod @classmethod
def now(cls, tz=None): def now(cls, tz=None):
return FAKE_TIME return FAKE_TIME
monkeypatch.setattr(dt, 'datetime', mydatetime) monkeypatch.setattr(stix2.utils, 'STIXdatetime', mydatetime)
@pytest.fixture @pytest.fixture

View File

@ -9,7 +9,7 @@ from .constants import ATTACK_PATTERN_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-05-12T08:17:27Z", "created": "2016-05-12T08:17:27.000Z",
"description": "...", "description": "...",
"external_references": [ "external_references": [
{ {
@ -18,7 +18,7 @@ EXPECTED = """{
} }
], ],
"id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
"modified": "2016-05-12T08:17:27Z", "modified": "2016-05-12T08:17:27.000Z",
"name": "Spear Phishing", "name": "Spear Phishing",
"type": "attack-pattern" "type": "attack-pattern"
}""" }"""
@ -27,8 +27,8 @@ EXPECTED = """{
def test_attack_pattern_example(): def test_attack_pattern_example():
ap = stix2.AttackPattern( ap = stix2.AttackPattern(
id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
created="2016-05-12T08:17:27Z", created="2016-05-12T08:17:27.000Z",
modified="2016-05-12T08:17:27Z", modified="2016-05-12T08:17:27.000Z",
name="Spear Phishing", name="Spear Phishing",
external_references=[{ external_references=[{
"source_name": "capec", "source_name": "capec",
@ -45,8 +45,8 @@ def test_attack_pattern_example():
{ {
"type": "attack-pattern", "type": "attack-pattern",
"id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
"created": "2016-05-12T08:17:27Z", "created": "2016-05-12T08:17:27.000Z",
"modified": "2016-05-12T08:17:27Z", "modified": "2016-05-12T08:17:27.000Z",
"description": "...", "description": "...",
"external_references": [ "external_references": [
{ {

View File

@ -7,30 +7,30 @@ EXPECTED_BUNDLE = """{
"id": "bundle--00000000-0000-0000-0000-000000000004", "id": "bundle--00000000-0000-0000-0000-000000000004",
"objects": [ "objects": [
{ {
"created": "2017-01-01T12:34:56Z", "created": "2017-01-01T12:34:56.000Z",
"id": "indicator--00000000-0000-0000-0000-000000000001", "id": "indicator--00000000-0000-0000-0000-000000000001",
"labels": [ "labels": [
"malicious-activity" "malicious-activity"
], ],
"modified": "2017-01-01T12:34:56Z", "modified": "2017-01-01T12:34:56.000Z",
"pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
"type": "indicator", "type": "indicator",
"valid_from": "2017-01-01T12:34:56Z" "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", "id": "malware--00000000-0000-0000-0000-000000000002",
"labels": [ "labels": [
"ransomware" "ransomware"
], ],
"modified": "2017-01-01T12:34:56Z", "modified": "2017-01-01T12:34:56.000Z",
"name": "Cryptolocker", "name": "Cryptolocker",
"type": "malware" "type": "malware"
}, },
{ {
"created": "2017-01-01T12:34:56Z", "created": "2017-01-01T12:34:56.000Z",
"id": "relationship--00000000-0000-0000-0000-000000000003", "id": "relationship--00000000-0000-0000-0000-000000000003",
"modified": "2017-01-01T12:34:56Z", "modified": "2017-01-01T12:34:56.000Z",
"relationship_type": "indicates", "relationship_type": "indicates",
"source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
"target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210", "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210",

View File

@ -9,11 +9,11 @@ from .constants import CAMPAIGN_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:00Z", "created": "2016-04-06T20:03:00.000Z",
"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.",
"id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "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", "name": "Green Group Attacks Against Finance",
"type": "campaign" "type": "campaign"
}""" }"""

View File

@ -9,11 +9,11 @@ from .constants import COURSE_OF_ACTION_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:48Z", "created": "2016-04-06T20:03:48.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "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 ...", "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", "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", "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
"type": "course-of-action" "type": "course-of-action"
}""" }"""
@ -23,8 +23,8 @@ def test_course_of_action_example():
coa = stix2.CourseOfAction( coa = stix2.CourseOfAction(
id="course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="course-of-action--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:48Z", created="2016-04-06T20:03:48.000Z",
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", 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 ..." 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", [ @pytest.mark.parametrize("data", [
EXPECTED, EXPECTED,
{ {
"created": "2016-04-06T20:03:48Z", "created": "2016-04-06T20:03:48.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "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 ...", "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", "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", "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter",
"type": "course-of-action" "type": "course-of-action"
}, },

View File

@ -1,11 +1,12 @@
import datetime as dt
import uuid import uuid
from stix2 import utils
from .constants import FAKE_TIME from .constants import FAKE_TIME
def test_clock(clock): def test_clock(clock):
assert dt.datetime.now() == FAKE_TIME assert utils.STIXdatetime.now() == FAKE_TIME
def test_my_uuid4_fixture(uuid4): def test_my_uuid4_fixture(uuid4):

View File

@ -9,10 +9,10 @@ from .constants import IDENTITY_ID
EXPECTED = """{ EXPECTED = """{
"created": "2015-12-21T19:59:11Z", "created": "2015-12-21T19:59:11.000Z",
"id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
"identity_class": "individual", "identity_class": "individual",
"modified": "2015-12-21T19:59:11Z", "modified": "2015-12-21T19:59:11.000Z",
"name": "John Smith", "name": "John Smith",
"type": "identity" "type": "identity"
}""" }"""
@ -21,8 +21,8 @@ EXPECTED = """{
def test_identity_example(): def test_identity_example():
identity = stix2.Identity( identity = stix2.Identity(
id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
created="2015-12-21T19:59:11Z", created="2015-12-21T19:59:11.000Z",
modified="2015-12-21T19:59:11Z", modified="2015-12-21T19:59:11.000Z",
name="John Smith", name="John Smith",
identity_class="individual", identity_class="individual",
) )
@ -33,10 +33,10 @@ def test_identity_example():
@pytest.mark.parametrize("data", [ @pytest.mark.parametrize("data", [
EXPECTED, EXPECTED,
{ {
"created": "2015-12-21T19:59:11Z", "created": "2015-12-21T19:59:11.000Z",
"id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
"identity_class": "individual", "identity_class": "individual",
"modified": "2015-12-21T19:59:11Z", "modified": "2015-12-21T19:59:11.000Z",
"name": "John Smith", "name": "John Smith",
"type": "identity" "type": "identity"
}, },
@ -56,8 +56,8 @@ def test_parse_no_type():
stix2.parse(""" stix2.parse("""
{ {
"id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c",
"created": "2015-12-21T19:59:11Z", "created": "2015-12-21T19:59:11.000Z",
"modified": "2015-12-21T19:59:11Z", "modified": "2015-12-21T19:59:11.000Z",
"name": "John Smith", "name": "John Smith",
"identity_class": "individual" "identity_class": "individual"
}""") }""")

View File

@ -10,22 +10,22 @@ from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS
EXPECTED_INDICATOR = """{ EXPECTED_INDICATOR = """{
"created": "2017-01-01T00:00:01Z", "created": "2017-01-01T00:00:01.000Z",
"id": "indicator--01234567-89ab-cdef-0123-456789abcdef", "id": "indicator--01234567-89ab-cdef-0123-456789abcdef",
"labels": [ "labels": [
"malicious-activity" "malicious-activity"
], ],
"modified": "2017-01-01T00:00:01Z", "modified": "2017-01-01T00:00:01.000Z",
"pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
"type": "indicator", "type": "indicator",
"valid_from": "1970-01-01T00:00:01Z" "valid_from": "1970-01-01T00:00:01Z"
}""" }"""
EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join("""
created=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>), created=STIXdatetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
id='indicator--01234567-89ab-cdef-0123-456789abcdef', id='indicator--01234567-89ab-cdef-0123-456789abcdef',
labels=['malicious-activity'], labels=['malicious-activity'],
modified=datetime.datetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>), modified=STIXdatetime(2017, 1, 1, 0, 0, 1, tzinfo=<UTC>),
pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
type='indicator', type='indicator',
valid_from=datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=<UTC>) valid_from=datetime.datetime(1970, 1, 1, 0, 0, 1, tzinfo=<UTC>)
@ -48,6 +48,8 @@ def test_indicator_with_all_required_properties():
assert str(ind) == EXPECTED_INDICATOR assert str(ind) == EXPECTED_INDICATOR
rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(ind)) rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(ind))
print(rep)
print(EXPECTED_INDICATOR_REPR)
assert rep == EXPECTED_INDICATOR_REPR assert rep == EXPECTED_INDICATOR_REPR

View File

@ -12,7 +12,7 @@ EXPECTED = """{
"aliases": [ "aliases": [
"Zookeeper" "Zookeeper"
], ],
"created": "2016-04-06T20:03:48Z", "created": "2016-04-06T20:03:48.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Incidents usually feature a shared TTP of a bobcat being released...", "description": "Incidents usually feature a shared TTP of a bobcat being released...",
"goals": [ "goals": [
@ -21,7 +21,7 @@ EXPECTED = """{
"damage" "damage"
], ],
"id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
"modified": "2016-04-06T20:03:48Z", "modified": "2016-04-06T20:03:48.000Z",
"name": "Bobcat Breakin", "name": "Bobcat Breakin",
"type": "intrusion-set" "type": "intrusion-set"
}""" }"""
@ -31,8 +31,8 @@ def test_intrusion_set_example():
intrusion_set = stix2.IntrusionSet( intrusion_set = stix2.IntrusionSet(
id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T20:03:48Z", created="2016-04-06T20:03:48.000Z",
modified="2016-04-06T20:03:48Z", modified="2016-04-06T20:03:48.000Z",
name="Bobcat Breakin", name="Bobcat Breakin",
description="Incidents usually feature a shared TTP of a bobcat being released...", description="Incidents usually feature a shared TTP of a bobcat being released...",
aliases=["Zookeeper"], aliases=["Zookeeper"],
@ -48,7 +48,7 @@ def test_intrusion_set_example():
"aliases": [ "aliases": [
"Zookeeper" "Zookeeper"
], ],
"created": "2016-04-06T20:03:48Z", "created": "2016-04-06T20:03:48.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "Incidents usually feature a shared TTP of a bobcat being released...", "description": "Incidents usually feature a shared TTP of a bobcat being released...",
"goals": [ "goals": [
@ -57,7 +57,7 @@ def test_intrusion_set_example():
"damage" "damage"
], ],
"id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29",
"modified": "2016-04-06T20:03:48Z", "modified": "2016-04-06T20:03:48.000Z",
"name": "Bobcat Breakin", "name": "Bobcat Breakin",
"type": "intrusion-set" "type": "intrusion-set"
}, },

View File

@ -10,12 +10,12 @@ from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS
EXPECTED_MALWARE = """{ EXPECTED_MALWARE = """{
"created": "2016-05-12T08:17:27Z", "created": "2016-05-12T08:17:27.000Z",
"id": "malware--fedcba98-7654-3210-fedc-ba9876543210", "id": "malware--fedcba98-7654-3210-fedc-ba9876543210",
"labels": [ "labels": [
"ransomware" "ransomware"
], ],
"modified": "2016-05-12T08:17:27Z", "modified": "2016-05-12T08:17:27.000Z",
"name": "Cryptolocker", "name": "Cryptolocker",
"type": "malware" "type": "malware"
}""" }"""
@ -109,8 +109,8 @@ def test_invalid_kwarg_to_malware():
{ {
"type": "malware", "type": "malware",
"id": "malware--fedcba98-7654-3210-fedc-ba9876543210", "id": "malware--fedcba98-7654-3210-fedc-ba9876543210",
"created": "2016-05-12T08:17:27Z", "created": "2016-05-12T08:17:27.000Z",
"modified": "2016-05-12T08:17:27Z", "modified": "2016-05-12T08:17:27.000Z",
"labels": ["ransomware"], "labels": ["ransomware"],
"name": "Cryptolocker", "name": "Cryptolocker",
}, },

View File

@ -40,7 +40,7 @@ EXPECTED_GRANULAR_MARKING = """{
}""" }"""
EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ 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", "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": [
@ -52,7 +52,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:00Z", "modified": "2016-04-06T20:03:00.000Z",
"name": "Green Group Attacks Against Finance", "name": "Green Group Attacks Against Finance",
"type": "campaign" "type": "campaign"
}""" }"""

View File

@ -10,12 +10,12 @@ from .constants import OBSERVED_DATA_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T19:58:16Z", "created": "2016-04-06T19:58:16.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"first_observed": "2015-12-21T19:00:00Z", "first_observed": "2015-12-21T19:00:00Z",
"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
"last_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, "number_observed": 50,
"objects": { "objects": {
"0": { "0": {
@ -31,8 +31,8 @@ def test_observed_data_example():
observed_data = stix2.ObservedData( observed_data = stix2.ObservedData(
id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T19:58:16Z", created="2016-04-06T19:58:16.000Z",
modified="2016-04-06T19:58:16Z", modified="2016-04-06T19:58:16.000Z",
first_observed="2015-12-21T19:00:00Z", first_observed="2015-12-21T19:00:00Z",
last_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z",
number_observed=50, number_observed=50,
@ -48,12 +48,12 @@ def test_observed_data_example():
EXPECTED_WITH_REF = """{ 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", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"first_observed": "2015-12-21T19:00:00Z", "first_observed": "2015-12-21T19:00:00Z",
"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
"last_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, "number_observed": 50,
"objects": { "objects": {
"0": { "0": {
@ -76,8 +76,8 @@ def test_observed_data_example_with_refs():
observed_data = stix2.ObservedData( observed_data = stix2.ObservedData(
id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T19:58:16Z", created="2016-04-06T19:58:16.000Z",
modified="2016-04-06T19:58:16Z", modified="2016-04-06T19:58:16.000Z",
first_observed="2015-12-21T19:00:00Z", first_observed="2015-12-21T19:00:00Z",
last_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z",
number_observed=50, number_observed=50,
@ -102,8 +102,8 @@ def test_observed_data_example_with_bad_refs():
stix2.ObservedData( stix2.ObservedData(
id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T19:58:16Z", created="2016-04-06T19:58:16.000Z",
modified="2016-04-06T19:58:16Z", modified="2016-04-06T19:58:16.000Z",
first_observed="2015-12-21T19:00:00Z", first_observed="2015-12-21T19:00:00Z",
last_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z",
number_observed=50, number_observed=50,
@ -130,11 +130,11 @@ def test_observed_data_example_with_bad_refs():
{ {
"type": "observed-data", "type": "observed-data",
"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "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", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"first_observed": "2015-12-21T19:00:00Z", "first_observed": "2015-12-21T19:00:00Z",
"last_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, "number_observed": 50,
"objects": { "objects": {
"0": { "0": {
@ -466,12 +466,12 @@ def test_parse_basic_tcp_traffic_with_error(data):
EXPECTED_PROCESS_OD = """{ 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", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"first_observed": "2015-12-21T19:00:00Z", "first_observed": "2015-12-21T19:00:00Z",
"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
"last_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, "number_observed": 50,
"objects": { "objects": {
"0": { "0": {
@ -499,8 +499,8 @@ def test_observed_data_with_process_example():
observed_data = stix2.ObservedData( observed_data = stix2.ObservedData(
id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf",
created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
created="2016-04-06T19:58:16Z", created="2016-04-06T19:58:16.000Z",
modified="2016-04-06T19:58:16Z", modified="2016-04-06T19:58:16.000Z",
first_observed="2015-12-21T19:00:00Z", first_observed="2015-12-21T19:00:00Z",
last_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z",
number_observed=50, number_observed=50,

View File

@ -10,9 +10,9 @@ from .constants import (FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID,
EXPECTED_RELATIONSHIP = """{ EXPECTED_RELATIONSHIP = """{
"created": "2016-04-06T20:06:37Z", "created": "2016-04-06T20:06:37.000Z",
"id": "relationship--00000000-1111-2222-3333-444444444444", "id": "relationship--00000000-1111-2222-3333-444444444444",
"modified": "2016-04-06T20:06:37Z", "modified": "2016-04-06T20:06:37.000Z",
"relationship_type": "indicates", "relationship_type": "indicates",
"source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
"target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210", "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210",

View File

@ -9,14 +9,14 @@ from .constants import INDICATOR_KWARGS, REPORT_ID
EXPECTED = """{ EXPECTED = """{
"created": "2015-12-21T19:59:11Z", "created": "2015-12-21T19:59:11.000Z",
"created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
"description": "A simple report with an indicator and campaign", "description": "A simple report with an indicator and campaign",
"id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
"labels": [ "labels": [
"campaign" "campaign"
], ],
"modified": "2015-12-21T19:59:11Z", "modified": "2015-12-21T19:59:11.000Z",
"name": "The Black Vine Cyberespionage Group", "name": "The Black Vine Cyberespionage Group",
"object_refs": [ "object_refs": [
"indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",
@ -32,8 +32,8 @@ def test_report_example():
report = stix2.Report( report = stix2.Report(
id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
created="2015-12-21T19:59:11Z", created="2015-12-21T19:59:11.000Z",
modified="2015-12-21T19:59:11Z", modified="2015-12-21T19:59:11.000Z",
name="The Black Vine Cyberespionage Group", name="The Black Vine Cyberespionage Group",
description="A simple report with an indicator and campaign", description="A simple report with an indicator and campaign",
published="2016-01-20T17:00:00Z", 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", [ @pytest.mark.parametrize("data", [
EXPECTED, EXPECTED,
{ {
"created": "2015-12-21T19:59:11Z", "created": "2015-12-21T19:59:11.000Z",
"created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283",
"description": "A simple report with an indicator and campaign", "description": "A simple report with an indicator and campaign",
"id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3",
"labels": [ "labels": [
"campaign" "campaign"
], ],
"modified": "2015-12-21T19:59:11Z", "modified": "2015-12-21T19:59:11.000Z",
"name": "The Black Vine Cyberespionage Group", "name": "The Black Vine Cyberespionage Group",
"object_refs": [ "object_refs": [
"indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2",

View File

@ -9,9 +9,9 @@ from .constants import INDICATOR_ID, SIGHTING_ID, SIGHTING_KWARGS
EXPECTED_SIGHTING = """{ EXPECTED_SIGHTING = """{
"created": "2016-04-06T20:06:37Z", "created": "2016-04-06T20:06:37.000Z",
"id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", "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", "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
"type": "sighting", "type": "sighting",
"where_sighted_refs": [ "where_sighted_refs": [
@ -20,9 +20,9 @@ EXPECTED_SIGHTING = """{
}""" }"""
BAD_SIGHTING = """{ BAD_SIGHTING = """{
"created": "2016-04-06T20:06:37Z", "created": "2016-04-06T20:06:37.000Z",
"id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", "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", "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef",
"type": "sighting", "type": "sighting",
"where_sighted_refs": [ "where_sighted_refs": [

View File

@ -9,14 +9,14 @@ from .constants import THREAT_ACTOR_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:48Z", "created": "2016-04-06T20:03:48.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "The Evil Org threat actor group", "description": "The Evil Org threat actor group",
"id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"labels": [ "labels": [
"crime-syndicate" "crime-syndicate"
], ],
"modified": "2016-04-06T20:03:48Z", "modified": "2016-04-06T20:03:48.000Z",
"name": "Evil Org", "name": "Evil Org",
"type": "threat-actor" "type": "threat-actor"
}""" }"""
@ -26,8 +26,8 @@ def test_threat_actor_example():
threat_actor = stix2.ThreatActor( threat_actor = stix2.ThreatActor(
id="threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="threat-actor--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:48Z", created="2016-04-06T20:03:48.000Z",
modified="2016-04-06T20:03:48Z", modified="2016-04-06T20:03:48.000Z",
name="Evil Org", name="Evil Org",
description="The Evil Org threat actor group", description="The Evil Org threat actor group",
labels=["crime-syndicate"], labels=["crime-syndicate"],
@ -39,14 +39,14 @@ def test_threat_actor_example():
@pytest.mark.parametrize("data", [ @pytest.mark.parametrize("data", [
EXPECTED, EXPECTED,
{ {
"created": "2016-04-06T20:03:48Z", "created": "2016-04-06T20:03:48.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"description": "The Evil Org threat actor group", "description": "The Evil Org threat actor group",
"id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"labels": [ "labels": [
"crime-syndicate" "crime-syndicate"
], ],
"modified": "2016-04-06T20:03:48Z", "modified": "2016-04-06T20:03:48.000Z",
"name": "Evil Org", "name": "Evil Org",
"type": "threat-actor" "type": "threat-actor"
}, },

View File

@ -9,13 +9,13 @@ from .constants import TOOL_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-04-06T20:03:48Z", "created": "2016-04-06T20:03:48.000Z",
"created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
"id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
"labels": [ "labels": [
"remote-access" "remote-access"
], ],
"modified": "2016-04-06T20:03:48Z", "modified": "2016-04-06T20:03:48.000Z",
"name": "VNC", "name": "VNC",
"type": "tool" "type": "tool"
}""" }"""
@ -25,8 +25,8 @@ def test_tool_example():
tool = stix2.Tool( tool = stix2.Tool(
id="tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", id="tool--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:48Z", created="2016-04-06T20:03:48.000Z",
modified="2016-04-06T20:03:48Z", modified="2016-04-06T20:03:48.000Z",
name="VNC", name="VNC",
labels=["remote-access"], labels=["remote-access"],
) )

View File

@ -17,6 +17,8 @@ eastern = pytz.timezone('US/Eastern')
(eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'), (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), '2017-07-01T00:00:00Z'),
(dt.datetime(2017, 7, 1, 0, 0, 0, 1), '2017-07-01T00:00:00.000001Z'), (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): def test_timestamp_formatting(dttm, timestamp):
assert stix2.utils.format_datetime(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 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', [ @pytest.mark.parametrize('ts', [
'foobar', 'foobar',
1, 1,

View File

@ -9,7 +9,7 @@ from .constants import VULNERABILITY_ID
EXPECTED = """{ EXPECTED = """{
"created": "2016-05-12T08:17:27Z", "created": "2016-05-12T08:17:27.000Z",
"external_references": [ "external_references": [
{ {
"external_id": "CVE-2016-1234", "external_id": "CVE-2016-1234",
@ -17,7 +17,7 @@ EXPECTED = """{
} }
], ],
"id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
"modified": "2016-05-12T08:17:27Z", "modified": "2016-05-12T08:17:27.000Z",
"name": "CVE-2016-1234", "name": "CVE-2016-1234",
"type": "vulnerability" "type": "vulnerability"
}""" }"""
@ -26,8 +26,8 @@ EXPECTED = """{
def test_vulnerability_example(): def test_vulnerability_example():
vulnerability = stix2.Vulnerability( vulnerability = stix2.Vulnerability(
id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061",
created="2016-05-12T08:17:27Z", created="2016-05-12T08:17:27.000Z",
modified="2016-05-12T08:17:27Z", modified="2016-05-12T08:17:27.000Z",
name="CVE-2016-1234", name="CVE-2016-1234",
external_references=[ external_references=[
stix2.ExternalReference(source_name='cve', stix2.ExternalReference(source_name='cve',

View File

@ -12,15 +12,29 @@ import pytz
NOW = object() 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(): def get_timestamp():
return dt.datetime.now(tz=pytz.UTC) return STIXdatetime.now(tz=pytz.UTC)
def format_datetime(dttm): def format_datetime(dttm):
# 1. Convert to timezone-aware # 1. Convert to timezone-aware
# 2. Convert to UTC # 2. Convert to UTC
# 3. Format in ISO format # 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" # 5. Add "Z"
if dttm.tzinfo is None or dttm.tzinfo.utcoffset(dttm) is None: if dttm.tzinfo is None or dttm.tzinfo.utcoffset(dttm) is None:
@ -29,20 +43,25 @@ def format_datetime(dttm):
else: else:
zoned = dttm.astimezone(pytz.utc) zoned = dttm.astimezone(pytz.utc)
ts = zoned.strftime("%Y-%m-%dT%H:%M:%S") 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") ts = ts + '.' + ms.rstrip("0")
return ts + "Z" return ts + "Z"
def parse_into_datetime(value): def parse_into_datetime(value, precision=None):
if isinstance(value, dt.date): if isinstance(value, dt.date):
if hasattr(value, 'hour'): if hasattr(value, 'hour'):
return value ts = value
else: else:
# Add a time component # Add a time component
return dt.datetime.combine(value, dt.time(0, 0, tzinfo=pytz.utc)) ts = dt.datetime.combine(value, dt.time(0, 0, tzinfo=pytz.utc))
else:
# value isn't a date or datetime object so assume it's a string # value isn't a date or datetime object so assume it's a string
try: try:
parsed = parser.parse(value) parsed = parser.parse(value)
@ -51,10 +70,26 @@ def parse_into_datetime(value):
raise ValueError("must be a datetime object, date object, or " raise ValueError("must be a datetime object, date object, or "
"timestamp string in a recognizable format.") "timestamp string in a recognizable format.")
if parsed.tzinfo: if parsed.tzinfo:
return parsed.astimezone(pytz.utc) ts = parsed.astimezone(pytz.utc)
else: else:
# Doesn't have timezone info in the string; assume UTC # Doesn't have timezone info in the string; assume UTC
return pytz.utc.localize(parsed) 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): def get_dict(data):