diff --git a/setup.py b/setup.py index 4c15871..eff4c2b 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ from setuptools import setup, find_packages install_requires = [ 'pytz', 'six', + 'python-dateutil', ] setup( diff --git a/stix2/common.py b/stix2/common.py index 3a77d0b..2a6479a 100644 --- a/stix2/common.py +++ b/stix2/common.py @@ -2,13 +2,13 @@ from .base import _STIXBase from .properties import (Property, BooleanProperty, ReferenceProperty, - StringProperty) + StringProperty, TimestampProperty) from .utils import NOW COMMON_PROPERTIES = { # 'type' and 'id' should be defined on each individual type - 'created': Property(default=lambda: NOW), - 'modified': Property(default=lambda: NOW), + 'created': TimestampProperty(default=lambda: NOW), + 'modified': TimestampProperty(default=lambda: NOW), 'external_references': Property(), 'revoked': BooleanProperty(), 'created_by_ref': ReferenceProperty(), diff --git a/stix2/properties.py b/stix2/properties.py index 7dab517..fe5e520 100644 --- a/stix2/properties.py +++ b/stix2/properties.py @@ -1,6 +1,9 @@ import re import uuid from six import PY2 +import datetime as dt +import pytz +from dateutil import parser class Property(object): @@ -194,6 +197,30 @@ class BooleanProperty(Property): raise ValueError("must be a boolean value.") +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()) + + 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.") + except TypeError: + # Isn't a string + raise ValueError("must be a datetime object, date object, or " + "timestamp string.") + + REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}" "-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") diff --git a/stix2/test/test_attack_pattern.py b/stix2/test/test_attack_pattern.py index a9d53b6..36b5b6d 100644 --- a/stix2/test/test_attack_pattern.py +++ b/stix2/test/test_attack_pattern.py @@ -1,7 +1,7 @@ import stix2 EXPECTED = """{ - "created": "2016-05-12T08:17:27.000Z", + "created": "2016-05-12T08:17:27Z", "description": "...", "external_references": [ { @@ -10,7 +10,7 @@ EXPECTED = """{ } ], "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "modified": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27Z", "name": "Spear Phishing", "type": "attack-pattern" }""" @@ -19,8 +19,8 @@ EXPECTED = """{ def test_attack_pattern_example(): ap = stix2.AttackPattern( id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - created="2016-05-12T08:17:27.000Z", - modified="2016-05-12T08:17:27.000Z", + created="2016-05-12T08:17:27Z", + modified="2016-05-12T08:17:27Z", name="Spear Phishing", external_references=[{ "source_name": "capec", diff --git a/stix2/test/test_campaign.py b/stix2/test/test_campaign.py index ce68946..741f5ba 100644 --- a/stix2/test/test_campaign.py +++ b/stix2/test/test_campaign.py @@ -1,11 +1,11 @@ import stix2 EXPECTED = """{ - "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.", "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" }""" @@ -15,8 +15,8 @@ def test_campaign_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." ) diff --git a/stix2/test/test_course_of_action.py b/stix2/test/test_course_of_action.py index 8c64cde..d35680e 100644 --- a/stix2/test/test_course_of_action.py +++ b/stix2/test/test_course_of_action.py @@ -1,11 +1,11 @@ import stix2 EXPECTED = """{ - "created": "2016-04-06T20:03:48.000Z", + "created": "2016-04-06T20:03:48Z", "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:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", "type": "course-of-action" }""" @@ -15,8 +15,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:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", 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 ..." ) diff --git a/stix2/test/test_identity.py b/stix2/test/test_identity.py index 632f229..e43614e 100644 --- a/stix2/test/test_identity.py +++ b/stix2/test/test_identity.py @@ -1,10 +1,10 @@ import stix2 EXPECTED = """{ - "created": "2015-12-21T19:59:11.000Z", + "created": "2015-12-21T19:59:11Z", "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", "identity_class": "individual", - "modified": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11Z", "name": "John Smith", "type": "identity" }""" @@ -13,8 +13,8 @@ EXPECTED = """{ def test_identity_example(): report = stix2.Identity( id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", - created="2015-12-21T19:59:11.000Z", - modified="2015-12-21T19:59:11.000Z", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", name="John Smith", identity_class="individual", ) diff --git a/stix2/test/test_intrusion_set.py b/stix2/test/test_intrusion_set.py index ba158d7..e60532f 100644 --- a/stix2/test/test_intrusion_set.py +++ b/stix2/test/test_intrusion_set.py @@ -4,7 +4,7 @@ EXPECTED = """{ "aliases": [ "Zookeeper" ], - "created": "2016-04-06T20:03:48.000Z", + "created": "2016-04-06T20:03:48Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "description": "Incidents usually feature a shared TTP of a bobcat being released...", "goals": [ @@ -13,7 +13,7 @@ EXPECTED = """{ "damage" ], "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", - "modified": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "Bobcat Breakin", "type": "intrusion-set" }""" @@ -23,8 +23,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:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", name="Bobcat Breakin", description="Incidents usually feature a shared TTP of a bobcat being released...", aliases=["Zookeeper"], diff --git a/stix2/test/test_malware.py b/stix2/test/test_malware.py index c80c28e..9537840 100644 --- a/stix2/test/test_malware.py +++ b/stix2/test/test_malware.py @@ -107,8 +107,8 @@ def test_parse_malware(data): assert mal.type == 'malware' assert mal.id == MALWARE_ID - assert mal.created == "2016-05-12T08:17:27Z" - assert mal.modified == "2016-05-12T08:17:27Z" + assert mal.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert mal.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) assert mal.labels == ['ransomware'] assert mal.name == "Cryptolocker" diff --git a/stix2/test/test_observed_data.py b/stix2/test/test_observed_data.py index 6fd95a2..f47e43e 100644 --- a/stix2/test/test_observed_data.py +++ b/stix2/test/test_observed_data.py @@ -1,12 +1,12 @@ import stix2 EXPECTED = """{ - "created": "2016-04-06T19:58:16.000Z", + "created": "2016-04-06T19:58:16Z", "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:16.000Z", + "modified": "2016-04-06T19:58:16Z", "number_observed": 50, "objects": { "0": { @@ -21,8 +21,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:16.000Z", - modified="2016-04-06T19:58:16.000Z", + created="2016-04-06T19:58:16Z", + modified="2016-04-06T19:58:16Z", first_observed="2015-12-21T19:00:00Z", last_observed="2015-12-21T19:00:00Z", number_observed=50, diff --git a/stix2/test/test_properties.py b/stix2/test/test_properties.py index dad250c..0bbcae1 100644 --- a/stix2/test/test_properties.py +++ b/stix2/test/test_properties.py @@ -2,7 +2,8 @@ import pytest from stix2.properties import (Property, BooleanProperty, ListProperty, StringProperty, TypeProperty, IDProperty, - ReferenceProperty) + ReferenceProperty, TimestampProperty) +from .constants import FAKE_TIME def test_property(): @@ -131,3 +132,21 @@ def test_reference_property(): assert ref_prop.validate("my-type--3a331bfe-0566-55e1-a4a0-9a2cd355a300") with pytest.raises(ValueError): ref_prop.validate("foo") + + +@pytest.mark.parametrize("value", [ + '2017-01-01T12:34:56Z', + '2017-01-01 12:34:56', + 'Jan 1 2017 12:34:56', +]) +def test_timestamp_property_valid(value): + ts_prop = TimestampProperty() + assert ts_prop.validate(value) == FAKE_TIME + + +def test_timestamp_property_invalid(): + ts_prop = TimestampProperty() + with pytest.raises(ValueError): + ts_prop.validate(1) + with pytest.raises(ValueError): + ts_prop.validate("someday sometime") diff --git a/stix2/test/test_report.py b/stix2/test/test_report.py index acd4e30..2727dbc 100644 --- a/stix2/test/test_report.py +++ b/stix2/test/test_report.py @@ -1,14 +1,14 @@ import stix2 EXPECTED = """{ - "created": "2015-12-21T19:59:11.000Z", + "created": "2015-12-21T19:59:11Z", "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:11.000Z", + "modified": "2015-12-21T19:59:11Z", "name": "The Black Vine Cyberespionage Group", "object_refs": [ "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", @@ -24,8 +24,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:11.000Z", - modified="2015-12-21T19:59:11.000Z", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", name="The Black Vine Cyberespionage Group", description="A simple report with an indicator and campaign", published="2016-01-201T17:00:00Z", diff --git a/stix2/test/test_threat_actor.py b/stix2/test/test_threat_actor.py index 8006ac3..c958f80 100644 --- a/stix2/test/test_threat_actor.py +++ b/stix2/test/test_threat_actor.py @@ -1,14 +1,14 @@ import stix2 EXPECTED = """{ - "created": "2016-04-06T20:03:48.000Z", + "created": "2016-04-06T20:03:48Z", "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:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "Evil Org", "type": "threat-actor" }""" @@ -18,8 +18,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:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", name="Evil Org", description="The Evil Org threat actor group", labels=["crime-syndicate"], diff --git a/stix2/test/test_tool.py b/stix2/test/test_tool.py index 201d333..322ff11 100644 --- a/stix2/test/test_tool.py +++ b/stix2/test/test_tool.py @@ -1,13 +1,13 @@ import stix2 EXPECTED = """{ - "created": "2016-04-06T20:03:48.000Z", + "created": "2016-04-06T20:03:48Z", "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", "labels": [ "remote-access" ], - "modified": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48Z", "name": "VNC", "type": "tool" }""" @@ -17,8 +17,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:48.000Z", - modified="2016-04-06T20:03:48.000Z", + created="2016-04-06T20:03:48Z", + modified="2016-04-06T20:03:48Z", name="VNC", labels=["remote-access"], ) diff --git a/stix2/test/test_vulnerability.py b/stix2/test/test_vulnerability.py index 3daebd9..ee84533 100644 --- a/stix2/test/test_vulnerability.py +++ b/stix2/test/test_vulnerability.py @@ -1,7 +1,7 @@ import stix2 EXPECTED = """{ - "created": "2016-05-12T08:17:27.000Z", + "created": "2016-05-12T08:17:27Z", "external_references": [ { "external_id": "CVE-2016-1234", @@ -9,7 +9,7 @@ EXPECTED = """{ } ], "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - "modified": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27Z", "name": "CVE-2016-1234", "type": "vulnerability" }""" @@ -18,8 +18,8 @@ EXPECTED = """{ def test_vulnerability_example(): vulnerability = stix2.Vulnerability( id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", - created="2016-05-12T08:17:27.000Z", - modified="2016-05-12T08:17:27.000Z", + created="2016-05-12T08:17:27Z", + modified="2016-05-12T08:17:27Z", name="CVE-2016-1234", external_references=[ stix2.ExternalReference(source_name='cve',