diff --git a/stix2validator/test/v20/attack_pattern_tests.py b/stix2validator/test/v20/attack_pattern_tests.py index 66a9414..02e6bad 100644 --- a/stix2validator/test/v20/attack_pattern_tests.py +++ b/stix2validator/test/v20/attack_pattern_tests.py @@ -92,3 +92,9 @@ def test_invalid_timestamp(self): attack_pattern['modified'] = "2016-02-29T08:17:27.000Z" self.assertTrueWithOptions(attack_pattern) + + attack_pattern['created'] = "2016-02-29T08:17:27.123Z" + self.assertFalseWithOptions(attack_pattern) + + attack_pattern['modified'] = "2016-02-29T08:17:27.123Z" + self.assertTrueWithOptions(attack_pattern) diff --git a/stix2validator/test/v20/campaign_tests.py b/stix2validator/test/v20/campaign_tests.py new file mode 100644 index 0000000..1a6f02b --- /dev/null +++ b/stix2validator/test/v20/campaign_tests.py @@ -0,0 +1,52 @@ +import copy +import json + +from . import ValidatorTest +from ... import validate_string + +VALID_CAMPAIGN = u""" +{ + "type": "campaign", + "id": "campaign--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2023-03-17T13:37:42.596Z", + "modified": "2023-09-27T20:12:54.984Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "name": "Operation Dream Job", + "description": "Operation Dream Job was a cyber espionage operation likely conducted by Lazarus Group.", + "aliases": [ + "Operation Dream Job", + "Operation North Star", + "Operation Interception" + ], + "first_seen": "2019-09-01T04:00:00.000Z", + "last_seen": "2020-08-01T04:00:00.000Z" +} +""" + + +class CampaignTestCases(ValidatorTest): + valid_campaign = json.loads(VALID_CAMPAIGN) + + def test_wellformed_campaign(self): + results = validate_string(VALID_CAMPAIGN, self.options) + self.assertTrue(results.is_valid) + + def test_invalid_timestamp(self): + campaign = copy.deepcopy(self.valid_campaign) + campaign['created'] = "2016-09-31T08:17:27.000Z" + self.assertFalseWithOptions(campaign) + + campaign['created'] = "2023-09-27T20:12:54.985Z" + self.assertFalseWithOptions(campaign) + + campaign['modified'] = "2023-09-27T20:12:54.985Z" + self.assertTrueWithOptions(campaign) + + campaign['first_seen'] = "2019-09-31T04:00:00.000Z" + self.assertFalseWithOptions(campaign) + + campaign['first_seen'] = "2020-08-01T04:00:00.001Z" + self.assertFalseWithOptions(campaign) + + campaign['last_seen'] = "2020-08-01T04:00:00.001Z" + self.assertTrueWithOptions(campaign) diff --git a/stix2validator/test/v20/coa_tests.py b/stix2validator/test/v20/coa_tests.py new file mode 100644 index 0000000..cd3b4a4 --- /dev/null +++ b/stix2validator/test/v20/coa_tests.py @@ -0,0 +1,36 @@ +import copy +import json + +from . import ValidatorTest +from ... import validate_string + +VALID_COURSE_OF_ACTION = u""" +{ + "type": "course-of-action", + "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", + "name": "mitigation-poison-ivy-firewall", + "description": "Recommended steps to respond to the Poison Ivy malware" +} +""" + + +class CoATestCases(ValidatorTest): + valid_course_of_action = json.loads(VALID_COURSE_OF_ACTION) + + def test_wellformed_coa(self): + results = validate_string(VALID_COURSE_OF_ACTION, self.options) + self.assertTrue(results.is_valid) + + def test_invalid_timestamp(self): + coa = copy.deepcopy(self.valid_course_of_action) + coa['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(coa) + + coa['created'] = "2016-04-06T20:03:48.123Z" + self.assertFalseWithOptions(coa) + + coa['modified'] = "2016-04-06T20:03:48.123Z" + self.assertTrueWithOptions(coa) diff --git a/stix2validator/test/v20/custom_obj_tests.py b/stix2validator/test/v20/custom_obj_tests.py index f297fad..0d42002 100644 --- a/stix2validator/test/v20/custom_obj_tests.py +++ b/stix2validator/test/v20/custom_obj_tests.py @@ -48,6 +48,17 @@ def test_no_modified(self): results = validate_parsed_json(custom_obj, self.options) self.assertEqual(results.is_valid, False) + def test_invalid_timestamp(self): + custom_object = copy.deepcopy(self.valid_custom_object) + custom_object['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(custom_object) + + custom_object['created'] = "2016-08-01T01:00:01.000Z" + self.assertFalseWithOptions(custom_object) + + custom_object['modified'] = "2016-08-01T01:00:01.000Z" + self.assertTrueWithOptions(custom_object) + def test_invalid_type_name(self): custom_obj = copy.deepcopy(self.valid_custom_object) custom_obj['type'] = "corpo_ration" diff --git a/stix2validator/test/v20/identity_tests.py b/stix2validator/test/v20/identity_tests.py index 16838f3..8e6be68 100644 --- a/stix2validator/test/v20/identity_tests.py +++ b/stix2validator/test/v20/identity_tests.py @@ -25,6 +25,17 @@ def test_invalid_check(self): self.assertRaises(exceptions.ValidationError, self.assertFalseWithOptions, self.valid_identity, enabled='abc') + def test_invalid_timestamp(self): + identity = copy.deepcopy(self.valid_identity) + identity['created'] = "2014-13-08T15:50:10.983Z" + self.assertFalseWithOptions(identity) + + identity['created'] = "2014-08-08T15:50:10.984Z" + self.assertFalseWithOptions(identity) + + identity['modified'] = "2014-08-08T15:50:10.984Z" + self.assertTrueWithOptions(identity) + def test_wellformed_identity(self): results = validate_string(VALID_IDENTITY, self.options) self.assertTrue(results.is_valid) diff --git a/stix2validator/test/v20/indicator_tests.py b/stix2validator/test/v20/indicator_tests.py index 83e6af2..2a16c27 100644 --- a/stix2validator/test/v20/indicator_tests.py +++ b/stix2validator/test/v20/indicator_tests.py @@ -34,11 +34,25 @@ def test_wellformed_indicator(self): results = validate_string(VALID_INDICATOR, self.options) self.assertTrue(results.is_valid) - def test_modified_before_created(self): + def test_invalid_timestamp(self): indicator = copy.deepcopy(self.valid_indicator) - indicator['modified'] = "2001-04-06T20:03:48Z" - results = validate_parsed_json(indicator, self.options) - self.assertEqual(results.is_valid, False) + indicator['created'] = "2016-11-31T20:03:48.000Z" + self.assertFalseWithOptions(indicator) + + indicator['created'] = "2016-04-06T20:03:48.001Z" + self.assertFalseWithOptions(indicator) + + indicator['modified'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(indicator) + + indicator['valid_until'] = "2016-11-31T20:03:48.000Z" + self.assertFalseWithOptions(indicator) + + indicator['valid_until'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(indicator) + + indicator['valid_from'] = "2016-04-06T20:03:48.002Z" + self.assertFalseWithOptions(indicator) def test_custom_property_name_invalid_character(self): indicator = copy.deepcopy(self.valid_indicator) diff --git a/stix2validator/test/v20/intrusion_set_tests.py b/stix2validator/test/v20/intrusion_set_tests.py index f6d07e8..7fd286c 100644 --- a/stix2validator/test/v20/intrusion_set_tests.py +++ b/stix2validator/test/v20/intrusion_set_tests.py @@ -26,6 +26,27 @@ def test_wellformed_intrusion_set(self): results = validate_string(VALID_INTRUSION_SET, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + intrusion_set = copy.deepcopy(self.valid_intrusion_set) + intrusion_set['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(intrusion_set) + + intrusion_set['created'] = "2016-04-06T20:03:48.123Z" + self.assertFalseWithOptions(intrusion_set) + + intrusion_set['modified'] = "2016-04-06T20:03:48.123Z" + self.assertTrueWithOptions(intrusion_set) + + intrusion_set['first_seen'] = "2016-04-06T20:03:48.123Z" + intrusion_set['last_seen'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(intrusion_set) + + intrusion_set['last_seen'] = "2016-04-06T20:03:48.000Z" + self.assertFalseWithOptions(intrusion_set) + + intrusion_set['last_seen'] = "2016-04-06T20:03:48.123Z" + self.assertTrueWithOptions(intrusion_set) + def test_country(self): intrusion_set = copy.deepcopy(self.valid_intrusion_set) intrusion_set['country'] = "USA" diff --git a/stix2validator/test/v20/malware_tests.py b/stix2validator/test/v20/malware_tests.py index 5a1e41f..8ce7d34 100644 --- a/stix2validator/test/v20/malware_tests.py +++ b/stix2validator/test/v20/malware_tests.py @@ -24,6 +24,17 @@ def test_wellformed_malware(self): results = validate_string(VALID_MALWARE, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + malware = copy.deepcopy(self.valid_malware) + malware['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(malware) + + malware['created'] = "2016-05-12T08:17:27.123Z" + self.assertFalseWithOptions(malware) + + malware['modified'] = "2016-05-12T08:17:27.123Z" + self.assertTrueWithOptions(malware) + def test_vocab_malware_label(self): malware = copy.deepcopy(self.valid_malware) malware['labels'] += "something" diff --git a/stix2validator/test/v20/observed_data_tests.py b/stix2validator/test/v20/observed_data_tests.py index be41d14..ebcd5f4 100644 --- a/stix2validator/test/v20/observed_data_tests.py +++ b/stix2validator/test/v20/observed_data_tests.py @@ -827,6 +827,26 @@ def test_invalid_observable_embedded_timestamp(self): } self.assertFalseWithOptions(observed_data) + def test_invalid_timestamp(self): + observed_data = copy.deepcopy(self.valid_observed_data) + observed_data['created'] = "2016-11-31T08:17:27.000Z" + self.assertFalseWithOptions(observed_data) + + observed_data['created'] = "2016-04-06T19:58:16.123Z" + self.assertFalseWithOptions(observed_data) + + observed_data['modified'] = "2016-04-06T19:58:16.123Z" + self.assertTrueWithOptions(observed_data) + + observed_data['first_observed'] = "2016-11-31T08:17:27.000Z" + self.assertFalseWithOptions(observed_data) + + observed_data['first_observed'] = "2015-12-21T19:00:00.123Z" + self.assertFalseWithOptions(observed_data) + + observed_data['last_observed'] = "2015-12-21T19:00:00.123Z" + self.assertTrueWithOptions(observed_data) + def test_additional_schemas_custom_observable(self): observed_data = copy.deepcopy(self.valid_observed_data) observed_data['objects']['2'] = { diff --git a/stix2validator/test/v20/relationship_tests.py b/stix2validator/test/v20/relationship_tests.py index 2f84f84..427f288 100644 --- a/stix2validator/test/v20/relationship_tests.py +++ b/stix2validator/test/v20/relationship_tests.py @@ -25,6 +25,17 @@ def test_wellformed_relationship(self): results = validate_string(VALID_RELATIONSHIP, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + relationship = copy.deepcopy(self.valid_relationship) + relationship['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(relationship) + + relationship['created'] = "2016-04-06T20:06:37.123Z" + self.assertFalseWithOptions(relationship) + + relationship['modified'] = "2016-04-06T20:06:37.123Z" + self.assertTrueWithOptions(relationship) + def test_relationship_type(self): relationship = copy.deepcopy(self.valid_relationship) relationship['relationship_type'] = "SOMETHING" diff --git a/stix2validator/test/v20/report_tests.py b/stix2validator/test/v20/report_tests.py index 046a193..fba6d6d 100644 --- a/stix2validator/test/v20/report_tests.py +++ b/stix2validator/test/v20/report_tests.py @@ -41,5 +41,17 @@ def test_vocab_report_label(self): def test_invalid_timestamp(self): report = copy.deepcopy(self.valid_report) - report['published'] = "2016-11-31T08:17:27.000000Z" + report['published'] = "2016-11-31T08:17:27.000Z" self.assertFalseWithOptions(report) + + report['published'] = "2016-05-21T19:59:11.000Z" + self.assertTrueWithOptions(report) + + report['created'] = "2016-11-31T08:17:27.000Z" + self.assertFalseWithOptions(report) + + report['created'] = "2016-05-21T19:59:11.123Z" + self.assertFalseWithOptions(report) + + report['modified'] = "2016-05-21T19:59:11.123Z" + self.assertTrueWithOptions(report) diff --git a/stix2validator/test/v20/sighting_tests.py b/stix2validator/test/v20/sighting_tests.py index 0995271..ff81f8a 100644 --- a/stix2validator/test/v20/sighting_tests.py +++ b/stix2validator/test/v20/sighting_tests.py @@ -25,6 +25,23 @@ def test_wellformed_report(self): results = validate_string(VALID_SIGHTING, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + sighting = copy.deepcopy(self.valid_sighting) + sighting['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(sighting) + + sighting['created'] = "2016-08-22T14:09:00.124Z" + self.assertFalseWithOptions(sighting) + + sighting['modified'] = "2016-08-22T14:09:00.124Z" + self.assertTrueWithOptions(sighting) + + sighting['last_seen'] = "2016-08-22T14:09:00.000Z" + self.assertFalseWithOptions(sighting) + + sighting['last_seen'] = "2016-08-22T14:09:00.124Z" + self.assertTrueWithOptions(sighting) + def test_sighting_of_ref(self): sighting = copy.deepcopy(self.valid_sighting) sighting['sighting_of_ref'] = "bundle--36ffb872-1dd9-446e-b6f5-d58527e5b5d2" diff --git a/stix2validator/test/v20/threat_actor_tests.py b/stix2validator/test/v20/threat_actor_tests.py index afc2e90..d90cb07 100644 --- a/stix2validator/test/v20/threat_actor_tests.py +++ b/stix2validator/test/v20/threat_actor_tests.py @@ -25,6 +25,17 @@ def test_wellformed_threat_actor(self): results = validate_string(VALID_THREAT_ACTOR, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + threat_actor = copy.deepcopy(self.valid_threat_actor) + threat_actor['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(threat_actor) + + threat_actor['created'] = "2016-04-06T20:03:48.123Z" + self.assertFalseWithOptions(threat_actor) + + threat_actor['modified'] = "2016-04-06T20:03:48.123Z" + self.assertTrueWithOptions(threat_actor) + def test_vocab_attack_motivation(self): threat_actor = copy.deepcopy(self.valid_threat_actor) threat_actor['primary_motivation'] = "selfishness" diff --git a/stix2validator/test/v20/tool_tests.py b/stix2validator/test/v20/tool_tests.py index 2b056dd..330a468 100644 --- a/stix2validator/test/v20/tool_tests.py +++ b/stix2validator/test/v20/tool_tests.py @@ -30,6 +30,17 @@ def test_wellformed_tool(self): results = validate_string(VALID_TOOL, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + tool = copy.deepcopy(self.valid_tool) + tool['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(tool) + + tool['created'] = "2016-04-06T20:03:48.123Z" + self.assertFalseWithOptions(tool) + + tool['modified'] = "2016-04-06T20:03:48.123Z" + self.assertTrueWithOptions(tool) + def test_vocab_tool_label(self): tool = copy.deepcopy(self.valid_tool) tool['labels'] += ["multi-purpose"] diff --git a/stix2validator/test/v20/vulnerability_tests.py b/stix2validator/test/v20/vulnerability_tests.py index 1d0d0e1..1f60dda 100644 --- a/stix2validator/test/v20/vulnerability_tests.py +++ b/stix2validator/test/v20/vulnerability_tests.py @@ -63,6 +63,17 @@ def test_incorrect_cve_source_name(self): results = validate_parsed_json(vulnerability, self.options) self.assertEqual(results.is_valid, False) + def test_invalid_timestamp(self): + vulnerability = copy.deepcopy(self.valid_vulnerability) + vulnerability['created'] = "2016-11-31T01:00:00.000Z" + self.assertFalseWithOptions(vulnerability) + + vulnerability['created'] = "2016-05-12T08:17:27.123Z" + self.assertFalseWithOptions(vulnerability) + + vulnerability['modified'] = "2016-05-12T08:17:27.123Z" + self.assertTrueWithOptions(vulnerability) + def test_url_no_hash(self): vulnerability = copy.deepcopy(self.valid_vulnerability) ext_refs = vulnerability['external_references'] diff --git a/stix2validator/test/v21/attack_pattern_tests.py b/stix2validator/test/v21/attack_pattern_tests.py index d594b22..d329a5d 100644 --- a/stix2validator/test/v21/attack_pattern_tests.py +++ b/stix2validator/test/v21/attack_pattern_tests.py @@ -140,5 +140,8 @@ def test_invalid_timestamp(self): attack_pattern['modified'] = "2017-02-29T08:17:27.000Z" self.assertFalseWithOptions(attack_pattern) - attack_pattern['modified'] = "2016-02-29T08:17:27.000Z" + attack_pattern['created'] = "2016-02-29T08:17:27.000036Z" + self.assertFalseWithOptions(attack_pattern) + + attack_pattern['modified'] = "2016-02-29T08:17:27.001Z" self.assertTrueWithOptions(attack_pattern) diff --git a/stix2validator/test/v21/campaign_tests.py b/stix2validator/test/v21/campaign_tests.py new file mode 100644 index 0000000..813f7b3 --- /dev/null +++ b/stix2validator/test/v21/campaign_tests.py @@ -0,0 +1,53 @@ +import copy +import json + +from . import ValidatorTest +from ... import validate_string + +VALID_CAMPAIGN = u""" +{ + "type": "campaign", + "spec_version": "2.1", + "id": "campaign--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2023-03-17T13:37:42.596Z", + "modified": "2023-09-27T20:12:54.984Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "name": "Operation Dream Job", + "description": "Operation Dream Job was a cyber espionage operation likely conducted by Lazarus Group.", + "aliases": [ + "Operation Dream Job", + "Operation North Star", + "Operation Interception" + ], + "first_seen": "2019-09-01T04:00:00.000Z", + "last_seen": "2020-08-01T04:00:00.000Z" +} +""" + + +class CampaignTestCases(ValidatorTest): + valid_campaign = json.loads(VALID_CAMPAIGN) + + def test_wellformed_campaign(self): + results = validate_string(VALID_CAMPAIGN, self.options) + self.assertTrue(results.is_valid) + + def test_invalid_timestamp(self): + campaign = copy.deepcopy(self.valid_campaign) + campaign['created'] = "2016-09-31T08:17:27.000Z" + self.assertFalseWithOptions(campaign) + + campaign['created'] = "2023-09-27T20:12:54.984123Z" + self.assertFalseWithOptions(campaign) + + campaign['modified'] = "2023-09-27T20:12:54.985Z" + self.assertTrueWithOptions(campaign) + + campaign['first_seen'] = "2019-09-31T04:00:00.000Z" + self.assertFalseWithOptions(campaign) + + campaign['first_seen'] = "2020-08-01T04:00:00.000123Z" + self.assertFalseWithOptions(campaign) + + campaign['last_seen'] = "2020-08-01T04:00:00.001Z" + self.assertTrueWithOptions(campaign) diff --git a/stix2validator/test/v21/coa_tests.py b/stix2validator/test/v21/coa_tests.py index 9061759..395316c 100644 --- a/stix2validator/test/v21/coa_tests.py +++ b/stix2validator/test/v21/coa_tests.py @@ -1,3 +1,4 @@ +import copy import json from . import ValidatorTest @@ -23,3 +24,14 @@ class CoATestCases(ValidatorTest): def test_wellformed_coa(self): results = validate_string(VALID_COURSE_OF_ACTION, self.options) self.assertTrue(results.is_valid) + + def test_invalid_timestamp(self): + coa = copy.deepcopy(self.valid_course_of_action) + coa['created'] = "2016-04-31T20:03:48.000Z" + self.assertFalseWithOptions(coa) + + coa['created'] = "2016-04-06T20:03:48.000123Z" + self.assertFalseWithOptions(coa) + + coa['modified'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(coa) diff --git a/stix2validator/test/v21/custom_obj_tests.py b/stix2validator/test/v21/custom_obj_tests.py index 297ebf8..d404cac 100644 --- a/stix2validator/test/v21/custom_obj_tests.py +++ b/stix2validator/test/v21/custom_obj_tests.py @@ -31,6 +31,17 @@ def test_wellformed_custom_object(self): results = validate_string(VALID_CUSTOM_OBJECT, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + custom_obj = copy.deepcopy(self.valid_custom_object) + custom_obj['created'] = "2021-02-30T09:16:08.989000Z" + self.assertFalseWithOptions(custom_obj) + + custom_obj['created'] = "2021-02-20T09:16:08.989123Z" + self.assertFalseWithOptions(custom_obj) + + custom_obj['modified'] = "2021-02-20T09:16:08.990Z" + self.assertTrueWithOptions(custom_obj) + def test_no_type(self): custom_obj = copy.deepcopy(self.valid_custom_object) del custom_obj['type'] diff --git a/stix2validator/test/v21/identity_tests.py b/stix2validator/test/v21/identity_tests.py index 6f23322..2b461a3 100644 --- a/stix2validator/test/v21/identity_tests.py +++ b/stix2validator/test/v21/identity_tests.py @@ -30,6 +30,17 @@ def test_wellformed_identity(self): results = validate_string(VALID_IDENTITY, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + identity = copy.deepcopy(self.valid_identity) + identity['created'] = "2014-08-32T15:50:10.983123Z" + self.assertFalseWithOptions(identity) + + identity['created'] = "2014-08-08T15:50:10.983123Z" + self.assertFalseWithOptions(identity) + + identity['modified'] = "2014-08-08T15:50:10.984Z" + self.assertTrueWithOptions(identity) + def test_vocab_identity_class(self): identity = copy.deepcopy(self.valid_identity) identity['identity_class'] = "corporation" diff --git a/stix2validator/test/v21/incident_tests.py b/stix2validator/test/v21/incident_tests.py index 8b48efc..1515f8f 100644 --- a/stix2validator/test/v21/incident_tests.py +++ b/stix2validator/test/v21/incident_tests.py @@ -24,6 +24,17 @@ def test_wellformed_incident(self): results = validate_string(VALID_INCIDENT, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + incident = self.valid_incident.copy() + incident['created'] = "2021-03-32T08:17:27.000Z" + self.assertFalseWithOptions(incident) + + incident['created'] = "2021-03-20T08:17:27.000123Z" + self.assertFalseWithOptions(incident) + + incident['modified'] = "2021-03-20T08:17:27.001Z" + self.assertTrueWithOptions(incident) + def test_missing_name(self): incident = copy.deepcopy(self.valid_incident) del incident['name'] diff --git a/stix2validator/test/v21/indicator_tests.py b/stix2validator/test/v21/indicator_tests.py index da27c15..0547b62 100644 --- a/stix2validator/test/v21/indicator_tests.py +++ b/stix2validator/test/v21/indicator_tests.py @@ -36,11 +36,23 @@ def test_wellformed_indicator(self): results = validate_string(VALID_INDICATOR, self.options) self.assertTrue(results.is_valid) - def test_modified_before_created(self): + def test_invalid_timestamp(self): indicator = copy.deepcopy(self.valid_indicator) - indicator['modified'] = "2001-04-06T20:03:48Z" - results = validate_parsed_json(indicator, self.options) - self.assertEqual(results.is_valid, False) + indicator['created'] = "2016-04-31T20:03:48Z" + self.assertFalseWithOptions(indicator) + + indicator['created'] = "2016-04-06T20:03:48.000123Z" + self.assertFalseWithOptions(indicator) + + indicator['modified'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(indicator) + + indicator['valid_from'] = "2016-04-06T20:03:48.000123Z" + indicator['valid_until'] = "2016-04-06T20:03:48.000Z" + self.assertFalseWithOptions(indicator) + + indicator['valid_until'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(indicator) def test_invalid_lang(self): indicator = copy.deepcopy(self.valid_indicator) diff --git a/stix2validator/test/v21/intrusion_set_tests.py b/stix2validator/test/v21/intrusion_set_tests.py index 2ce49d2..7a74961 100644 --- a/stix2validator/test/v21/intrusion_set_tests.py +++ b/stix2validator/test/v21/intrusion_set_tests.py @@ -49,8 +49,17 @@ def test_vocab_attack_resource_level(self): self.check_ignore(intrusion_set, 'attack-resource-level') - def test_invalid_seen_time(self): + def test_invalid_timestamp(self): intrusion_set = copy.deepcopy(self.valid_intrusion_set) + intrusion_set['created'] = "2016-04-31T20:03:48.000Z" + self.assertFalseWithOptions(intrusion_set) + + intrusion_set['created'] = "2016-04-06T20:03:48.000123Z" + self.assertFalseWithOptions(intrusion_set) + + intrusion_set['modified'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(intrusion_set) + intrusion_set['first_seen'] = "2016-04-06T20:06:37.000Z" intrusion_set['last_seen'] = "2016-04-06T20:06:37.000Z" self.assertTrueWithOptions(intrusion_set) diff --git a/stix2validator/test/v21/language_content_tests.py b/stix2validator/test/v21/language_content_tests.py index 3df606f..447af9b 100644 --- a/stix2validator/test/v21/language_content_tests.py +++ b/stix2validator/test/v21/language_content_tests.py @@ -45,3 +45,14 @@ def test_invalid_contents_subkey(self): "a": "boo" } self.assertFalseWithOptions(lang_content) + + def test_invalid_timestamp(self): + lang_content = copy.deepcopy(self.valid_language_content) + lang_content['created'] = "2018-02-30T21:31:22.007Z" + self.assertFalseWithOptions(lang_content) + + lang_content['created'] = "2018-02-08T21:31:22.007123Z" + self.assertFalseWithOptions(lang_content) + + lang_content['modified'] = "2018-02-08T21:31:22.008Z" + self.assertTrueWithOptions(lang_content) diff --git a/stix2validator/test/v21/location_tests.py b/stix2validator/test/v21/location_tests.py index 98fc21e..01dcb9d 100644 --- a/stix2validator/test/v21/location_tests.py +++ b/stix2validator/test/v21/location_tests.py @@ -27,6 +27,17 @@ def test_wellformed_location(self): results = validate_string(VALID_LOCATION, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + location = copy.deepcopy(self.valid_location) + location['created'] = "2016-04-31T20:03:00.000Z" + self.assertFalseWithOptions(location) + + location['created'] = "2016-04-06T20:03:00.000123Z" + self.assertFalseWithOptions(location) + + location['modified'] = "2016-04-06T20:03:00.001Z" + self.assertTrueWithOptions(location) + def test_location_lat_long(self): location = copy.deepcopy(self.valid_location) location['latitude'] = 48.8566 diff --git a/stix2validator/test/v21/malware_analysis_tests.py b/stix2validator/test/v21/malware_analysis_tests.py index 068626e..cc66e91 100644 --- a/stix2validator/test/v21/malware_analysis_tests.py +++ b/stix2validator/test/v21/malware_analysis_tests.py @@ -33,6 +33,17 @@ def test_invalid_product(self): self.check_ignore(malware, 'malware-analysis-product') + def test_invalid_timestamp(self): + malware = copy.deepcopy(self.valid_malware) + malware['created'] = "2016-04-31T20:03:48.000Z" + self.assertFalseWithOptions(malware) + + malware['created'] = "2016-04-06T20:03:48.000123Z" + self.assertFalseWithOptions(malware) + + malware['modified'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(malware) + def test_software_ref(self): malware = copy.deepcopy(self.valid_malware) malware['host_vm_ref'] = "soft-id-false" diff --git a/stix2validator/test/v21/malware_tests.py b/stix2validator/test/v21/malware_tests.py index a8362c4..29112f9 100644 --- a/stix2validator/test/v21/malware_tests.py +++ b/stix2validator/test/v21/malware_tests.py @@ -26,6 +26,27 @@ def test_wellformed_malware(self): results = validate_string(VALID_MALWARE, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + malware = copy.deepcopy(self.valid_malware) + malware['created'] = "2016-05-32T08:17:27.000Z" + self.assertFalseWithOptions(malware) + + malware['created'] = "2016-05-12T08:17:27.000123Z" + self.assertFalseWithOptions(malware) + + malware['modified'] = "2016-05-12T08:17:27.001Z" + self.assertTrueWithOptions(malware) + + malware['first_seen'] = "2016-05-32T08:17:27.000Z" + self.assertFalseWithOptions(malware) + + malware['first_seen'] = "2016-05-12T08:17:27.000123Z" + malware['last_seen'] = "2016-05-32T08:17:27.000Z" + self.assertFalseWithOptions(malware) + + malware['last_seen'] = "2016-05-12T08:17:27.001Z" + self.assertTrueWithOptions(malware) + def test_vocab_malware_label(self): malware = copy.deepcopy(self.valid_malware) malware['malware_types'] += "something" diff --git a/stix2validator/test/v21/network_traffic_tests.py b/stix2validator/test/v21/network_traffic_tests.py index 2a611b5..ecee1dc 100644 --- a/stix2validator/test/v21/network_traffic_tests.py +++ b/stix2validator/test/v21/network_traffic_tests.py @@ -118,12 +118,12 @@ def test_network_traffic_end_is_active(self): def test_invalid_start_end_time(self): net_traffic = copy.deepcopy(self.valid_net_traffic) - net_traffic['start'] = "2016-04-06T20:06:37.000Z" - net_traffic['end'] = "2016-01-01T00:00:00.000Z" + net_traffic['start'] = "2016-04-31T20:06:37.000Z" + net_traffic['end'] = "2016-04-06T20:06:37.000Z" self.assertFalseWithOptions(net_traffic) - net_traffic['end'] = "2016-04-06T20:06:37.000Z" - self.assertTrueWithOptions(net_traffic) + net_traffic['start'] = "2016-04-06T20:06:37.000123Z" + self.assertFalseWithOptions(net_traffic) - net_traffic['end'] = "2016-05-07T20:06:37.000Z" + net_traffic['end'] = "2016-04-06T20:06:37.001Z" self.assertTrueWithOptions(net_traffic) diff --git a/stix2validator/test/v21/observed_data_tests.py b/stix2validator/test/v21/observed_data_tests.py index ff36963..2a37c3a 100644 --- a/stix2validator/test/v21/observed_data_tests.py +++ b/stix2validator/test/v21/observed_data_tests.py @@ -12,8 +12,8 @@ "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", "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", + "first_observed": "2015-12-21T19:00:00.000Z", + "last_observed": "2015-12-21T19:00:00.000Z", "number_observed": 50, "object_refs": [ "ipv4-address--efcd5e80-570d-4131-b213-62cb18eaa6a8", @@ -742,10 +742,16 @@ def test_hash_length(self): self.assertFalseWithOptions(observed_data) def test_invalid_accessed_timestamp(self): - observed_data = copy.deepcopy(self.valid_object) + observed_data = copy.deepcopy(self.valid_observed_data) observed_data['created'] = "2016-11-31T08:17:27.000000Z" self.assertFalseWithOptions(observed_data) + observed_data['created'] = "2016-04-06T19:58:16.000123Z" + self.assertFalseWithOptions(observed_data) + + observed_data['modified'] = "2016-04-06T19:58:16.001Z" + self.assertTrueWithOptions(observed_data) + def test_invalid_extension_timestamp(self): observed_data = copy.deepcopy(self.valid_object) observed_data['extensions'] = {'windows-pebinary-ext': { @@ -834,14 +840,13 @@ def test_url_in_artifact(self): def test_invalid_seen_time(self): observed_data = copy.deepcopy(self.valid_observed_data) - observed_data['first_observed'] = "2016-04-06T20:06:37.000Z" - observed_data['last_observed'] = "2016-04-06T20:06:37.000Z" - self.assertTrueWithOptions(observed_data) + observed_data['first_observed'] = "2015-12-32T19:00:00Z" + self.assertFalseWithOptions(observed_data) - observed_data['last_observed'] = "2016-01-01T00:00:00.000Z" + observed_data['first_observed'] = "2015-12-21T19:00:00.000123Z" self.assertFalseWithOptions(observed_data) - observed_data['last_observed'] = "2016-05-07T20:06:37.000Z" + observed_data['last_observed'] = "2015-12-21T19:00:00.001Z" self.assertTrueWithOptions(observed_data) def test_domain_name_not_deprecated_property(self): diff --git a/stix2validator/test/v21/relationship_tests.py b/stix2validator/test/v21/relationship_tests.py index 3129453..51412d5 100644 --- a/stix2validator/test/v21/relationship_tests.py +++ b/stix2validator/test/v21/relationship_tests.py @@ -99,16 +99,25 @@ def test_missing_required(self): del relationship['relationship_type'] self.assertFalseWithOptions(relationship) - def test_invalid_stop_time(self): + def test_invalid_timestamp(self): relationship = copy.deepcopy(self.valid_relationship) - relationship['start_time'] = "2016-04-06T20:06:37.000Z" + relationship['created'] = "2016-04-31T20:06:37.000Z" + self.assertFalseWithOptions(relationship) + + relationship['created'] = "2016-04-06T20:06:37.000123Z" + self.assertFalseWithOptions(relationship) + + relationship['modified'] = "2016-04-06T20:06:37.001Z" + self.assertTrueWithOptions(relationship) + + relationship['start_time'] = "2016-04-31T20:06:37.000Z" relationship['stop_time'] = "2016-04-06T20:06:37.000Z" self.assertFalseWithOptions(relationship) - relationship['stop_time'] = "2016-01-01T00:00:00.000Z" + relationship['start_time'] = "2016-04-06T20:06:37.000123Z" self.assertFalseWithOptions(relationship) - relationship['stop_time'] = "2016-05-07T20:06:37.000Z" + relationship['stop_time'] = "2016-04-06T20:06:37.001Z" self.assertTrueWithOptions(relationship) def test_enforce_refs(self): diff --git a/stix2validator/test/v21/report_tests.py b/stix2validator/test/v21/report_tests.py index 68d85d6..aa83e1a 100644 --- a/stix2validator/test/v21/report_tests.py +++ b/stix2validator/test/v21/report_tests.py @@ -11,7 +11,7 @@ "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", "created": "2015-12-21T19:59:11.000Z", - "modified": "2016-05-21T19:59:11.000Z", + "modified": "2016-05-21T08:17:27.000Z", "published": "2016-05-21T19:59:11Z", "name": "The Black Vine Cyberespionage Group", "description": "A simple report with an indicator and campaign", @@ -42,5 +42,17 @@ def test_vocab_report_type(self): def test_invalid_timestamp(self): report = copy.deepcopy(self.valid_report) - report['published'] = "2016-11-31T08:17:27.000000Z" + report['published'] = "2016-05-32T08:17:27.000Z" self.assertFalseWithOptions(report) + + report['published'] = "2016-05-12T19:59:11Z" + self.assertTrueWithOptions(report) + + report['created'] = "2016-05-32T08:17:27.000Z" + self.assertFalseWithOptions(report) + + report['created'] = "2016-05-21T08:17:27.000123Z" + self.assertFalseWithOptions(report) + + report['modified'] = "2016-05-21T08:17:27.001Z" + self.assertTrueWithOptions(report) diff --git a/stix2validator/test/v21/sighting_tests.py b/stix2validator/test/v21/sighting_tests.py index 7f39f76..06b4c6d 100644 --- a/stix2validator/test/v21/sighting_tests.py +++ b/stix2validator/test/v21/sighting_tests.py @@ -41,14 +41,23 @@ def test_where_sighted_refs(self): sighting['where_sighted_refs'].append("tool--36ffb872-1dd9-446e-b6f5-d58527e5b5d2") self.assertFalseWithOptions(sighting) - def test_invalid_seen_time(self): + def test_invalid_timestamp(self): sighting = copy.deepcopy(self.valid_sighting) - sighting['first_seen'] = "2016-04-06T20:06:37.000Z" - sighting['last_seen'] = "2016-01-01T00:00:00.000Z" + sighting['created'] = "2016-08-32T14:09:00.123Z" self.assertFalseWithOptions(sighting) - sighting['last_seen'] = "2016-04-06T20:06:37.000Z" + sighting['created'] = "2016-08-22T14:09:00.123456Z" + self.assertFalseWithOptions(sighting) + + sighting['modified'] = "2016-08-22T14:09:00.124Z" self.assertTrueWithOptions(sighting) - sighting['last_seen'] = "2016-05-07T20:06:37.000Z" + sighting['first_seen'] = "2016-08-32T14:09:00.000Z" + sighting['last_seen'] = "2016-08-22T14:09:00.123Z" + self.assertFalseWithOptions(sighting) + + sighting['first_seen'] = "2016-08-22T14:09:00.123456Z" + self.assertFalseWithOptions(sighting) + + sighting['last_seen'] = "2016-08-22T14:09:00.124Z" self.assertTrueWithOptions(sighting) diff --git a/stix2validator/test/v21/threat_actor_tests.py b/stix2validator/test/v21/threat_actor_tests.py index 0c7aeb5..732b435 100644 --- a/stix2validator/test/v21/threat_actor_tests.py +++ b/stix2validator/test/v21/threat_actor_tests.py @@ -26,6 +26,27 @@ def test_wellformed_threat_actor(self): results = validate_string(VALID_THREAT_ACTOR, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + threat_actor = copy.deepcopy(self.valid_threat_actor) + threat_actor['created'] = "2016-04-31T20:03:48.000Z" + self.assertFalseWithOptions(threat_actor) + + threat_actor['created'] = "2016-04-06T20:03:48.000123Z" + self.assertFalseWithOptions(threat_actor) + + threat_actor['modified'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(threat_actor) + + threat_actor['first_seen'] = "2016-04-06T20:03:48.000123Z" + threat_actor['last_seen'] = "2016-04-31T20:03:48.000Z" + self.assertFalseWithOptions(threat_actor) + + threat_actor['last_seen'] = "2016-04-06T20:03:48.000Z" + self.assertFalseWithOptions(threat_actor) + + threat_actor['last_seen'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(threat_actor) + def test_vocab_attack_motivation(self): threat_actor = copy.deepcopy(self.valid_threat_actor) threat_actor['primary_motivation'] = "selfishness" diff --git a/stix2validator/test/v21/tool_tests.py b/stix2validator/test/v21/tool_tests.py index 8333e4a..c98b7a3 100644 --- a/stix2validator/test/v21/tool_tests.py +++ b/stix2validator/test/v21/tool_tests.py @@ -31,6 +31,17 @@ def test_wellformed_tool(self): results = validate_string(VALID_TOOL, self.options) self.assertTrue(results.is_valid) + def test_invalid_timestamp(self): + tool = copy.deepcopy(self.valid_tool) + tool['created'] = "2016-04-31T20:03:48.000Z" + self.assertFalseWithOptions(tool) + + tool['created'] = "2016-04-06T20:03:48.000123Z" + self.assertFalseWithOptions(tool) + + tool['modified'] = "2016-04-06T20:03:48.001Z" + self.assertTrueWithOptions(tool) + def test_vocab_tool_type(self): tool = copy.deepcopy(self.valid_tool) tool['tool_types'] += ["multi-purpose"] diff --git a/stix2validator/test/v21/vulnerability_tests.py b/stix2validator/test/v21/vulnerability_tests.py index 5fb1bec..6de21cd 100644 --- a/stix2validator/test/v21/vulnerability_tests.py +++ b/stix2validator/test/v21/vulnerability_tests.py @@ -59,6 +59,17 @@ def test_incorrect_cve_source_name(self): ext_refs[0]['source_name'] = "CVE" self.assertFalseWithOptions(vulnerability) + def test_invalid_timestamp(self): + vulnerability = copy.deepcopy(self.valid_vulnerability) + vulnerability['created'] = "2016-05-32T08:17:27.000Z" + self.assertFalseWithOptions(vulnerability) + + vulnerability['created'] = "2016-05-12T08:17:27.000123Z" + self.assertFalseWithOptions(vulnerability) + + vulnerability['modified'] = "2016-05-12T08:17:27.001Z" + self.assertTrueWithOptions(vulnerability) + def test_url_no_hash(self): vulnerability = copy.deepcopy(self.valid_vulnerability) ext_refs = vulnerability['external_references'] diff --git a/stix2validator/v20/musts.py b/stix2validator/v20/musts.py index 867143a..c5b9a20 100644 --- a/stix2validator/v20/musts.py +++ b/stix2validator/v20/musts.py @@ -2,6 +2,7 @@ """ from collections.abc import Mapping +from datetime import datetime import re from cpe import CPE @@ -15,6 +16,7 @@ from ..util import cyber_observable_check, has_cyber_observable_data from .errors import JSONError +TIMESTAMP_FORMAT_RE = re.compile(r"^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?Z$") CUSTOM_TYPE_PREFIX_RE = re.compile(r"^x\-.+\-.+$") CUSTOM_TYPE_LAX_PREFIX_RE = re.compile(r"^x\-.+$") CUSTOM_PROPERTY_PREFIX_RE = re.compile(r"^x_.+_.+$") @@ -24,13 +26,12 @@ def timestamp(instance): """Ensure timestamps contain sane months, days, hours, minutes, seconds. """ - ts_re = re.compile(r"^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?Z$") timestamp_props = ['created', 'modified'] if instance['type'] in enums.TIMESTAMP_PROPERTIES: timestamp_props += enums.TIMESTAMP_PROPERTIES[instance['type']] for tprop in timestamp_props: - if tprop in instance and ts_re.match(instance[tprop]): + if tprop in instance and TIMESTAMP_FORMAT_RE.match(instance[tprop]): # Don't raise an error if schemas will catch it try: parser.parse(instance[tprop]) @@ -44,7 +45,7 @@ def timestamp(instance): continue if obj['type'] in enums.TIMESTAMP_OBSERVABLE_PROPERTIES: for tprop in enums.TIMESTAMP_OBSERVABLE_PROPERTIES[obj['type']]: - if tprop in obj and ts_re.match(obj[tprop]): + if tprop in obj and TIMESTAMP_FORMAT_RE.match(obj[tprop]): # Don't raise an error if schemas will catch it try: parser.parse(obj[tprop]) @@ -57,13 +58,13 @@ def timestamp(instance): for tprop in enums.TIMESTAMP_EMBEDDED_PROPERTIES[obj['type']][embed]: if embed == 'extensions': for ext in obj[embed]: - if tprop in obj[embed][ext] and ts_re.match(obj[embed][ext][tprop]): + if tprop in obj[embed][ext] and TIMESTAMP_FORMAT_RE.match(obj[embed][ext][tprop]): try: parser.parse(obj[embed][ext][tprop]) except ValueError as e: yield JSONError("'%s': '%s': '%s': '%s' is not a valid timestamp: %s" % (obj['type'], ext, tprop, obj[embed][ext][tprop], str(e)), instance['id']) - elif tprop in obj[embed] and ts_re.match(obj[embed][tprop]): + elif tprop in obj[embed] and TIMESTAMP_FORMAT_RE.match(obj[embed][tprop]): try: parser.parse(obj[embed][tprop]) except ValueError as e: @@ -71,14 +72,48 @@ def timestamp(instance): % (obj['type'], tprop, obj[embed][tprop], str(e)), instance['id']) -def modified_created(instance): - """`modified` property must be later or equal to `created` property +def compare_timestamps(modified, created): + if TIMESTAMP_FORMAT_RE.match(modified) and TIMESTAMP_FORMAT_RE.match(created): + created_datetime = datetime_from_str(created) + modified_datetime = datetime_from_str(modified) + if isinstance(created_datetime, datetime) and isinstance(modified_datetime, datetime): + return modified_datetime < created_datetime + return modified < created + + +def datetime_from_str(str_datetime): + pattern = '%Y-%m-%dT%H:%M:%S.%fZ' + try: + return datetime.strptime(str_datetime, pattern) + except ValueError: + pattern = '%Y-%m-%dT%H:%M:%SZ' + try: + return datetime.strptime(str_datetime, pattern) + except ValueError: + return str_datetime + + +def timestamp_comparison(instance): + """ + Ensure `modified` property is later or equal to `created` property. + Same for any present timestamp property. """ if 'modified' in instance and 'created' in instance and \ - instance['modified'] < instance['created']: + compare_timestamps(instance['modified'], instance['created']): msg = "'modified' (%s) must be later or equal to 'created' (%s)" return JSONError(msg % (instance['modified'], instance['created']), instance['id']) + if instance['type'] in enums.TIMESTAMP_PROPERTIES: + timestamp_properties = enums.TIMESTAMP_PROPERTIES[instance['type']] + if len(timestamp_properties) == 2: + first, last = enums.TIMESTAMP_PROPERTIES[instance['type']] + if first in instance and last in instance and \ + compare_timestamps(instance[last], instance[first]): + msg = "'%s' (%s) must be later or equal to '%s' (%s)" + return JSONError( + msg % (last, instance[last], first, instance[first]), + instance['id'] + ) def object_marking_circular_refs(instance): @@ -412,7 +447,7 @@ def list_musts(options): """ validator_list = [ timestamp, - modified_created, + timestamp_comparison, object_marking_circular_refs, granular_markings_circular_refs, marking_selector_syntax, diff --git a/stix2validator/v21/musts.py b/stix2validator/v21/musts.py index 86a24f5..bcae641 100644 --- a/stix2validator/v21/musts.py +++ b/stix2validator/v21/musts.py @@ -1,6 +1,7 @@ """Mandatory (MUST) requirement checking functions """ from collections.abc import Mapping +from datetime import datetime import operator import re import uuid @@ -16,6 +17,7 @@ from ..util import cyber_observable_check, has_cyber_observable_data from .errors import JSONError +TIMESTAMP_FORMAT_RE = re.compile(r"^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?Z$") TYPE_FORMAT_RE = re.compile(r'^\-?[a-z0-9]+(-[a-z0-9]+)*\-?$') PROPERTY_FORMAT_RE = re.compile(r'^[a-z0-9_]{3,250}$') CUSTOM_TYPE_PREFIX_RE = re.compile(r"^x\-.+\-.+$") @@ -29,13 +31,12 @@ def timestamp(instance): """Ensure timestamps contain sane months, days, hours, minutes, seconds. """ - ts_re = re.compile(r"^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?Z$") timestamp_props = ['created', 'modified'] if instance['type'] in enums.TIMESTAMP_PROPERTIES: timestamp_props += enums.TIMESTAMP_PROPERTIES[instance['type']] for tprop in timestamp_props: - if tprop in instance and ts_re.match(instance[tprop]): + if tprop in instance and TIMESTAMP_FORMAT_RE.match(instance[tprop]): # Don't raise an error if schemas will catch it try: parser.parse(instance[tprop]) @@ -50,7 +51,7 @@ def timestamp(instance): continue if obj['type'] in enums.TIMESTAMP_OBSERVABLE_PROPERTIES: for tprop in enums.TIMESTAMP_OBSERVABLE_PROPERTIES[obj['type']]: - if tprop in obj and ts_re.match(obj[tprop]): + if tprop in obj and TIMESTAMP_FORMAT_RE.match(obj[tprop]): # Don't raise an error if schemas will catch it try: parser.parse(obj[tprop]) @@ -63,13 +64,13 @@ def timestamp(instance): for tprop in enums.TIMESTAMP_EMBEDDED_PROPERTIES[obj['type']][embed]: if embed == 'extensions': for ext in obj[embed]: - if tprop in obj[embed][ext] and ts_re.match(obj[embed][ext][tprop]): + if tprop in obj[embed][ext] and TIMESTAMP_FORMAT_RE.match(obj[embed][ext][tprop]): try: parser.parse(obj[embed][ext][tprop]) except ValueError as e: yield JSONError("'%s': '%s': '%s': '%s' is not a valid timestamp: %s" % (obj['type'], ext, tprop, obj[embed][ext][tprop], str(e)), instance['id']) - elif tprop in obj[embed] and ts_re.match(obj[embed][tprop]): + elif tprop in obj[embed] and TIMESTAMP_FORMAT_RE.match(obj[embed][tprop]): try: parser.parse(obj[embed][tprop]) except ValueError as e: @@ -80,7 +81,7 @@ def timestamp(instance): return if instance['type'] in enums.TIMESTAMP_OBSERVABLE_PROPERTIES: for tprop in enums.TIMESTAMP_OBSERVABLE_PROPERTIES[instance['type']]: - if tprop in instance and ts_re.match(instance[tprop]): + if tprop in instance and TIMESTAMP_FORMAT_RE.match(instance[tprop]): # Don't raise an error if schemas will catch it try: parser.parse(instance[tprop]) @@ -93,13 +94,13 @@ def timestamp(instance): for tprop in enums.TIMESTAMP_EMBEDDED_PROPERTIES[instance['type']][embed]: if embed == 'extensions': for ext in instance[embed]: - if tprop in instance[embed][ext] and ts_re.match(instance[embed][ext][tprop]): + if tprop in instance[embed][ext] and TIMESTAMP_FORMAT_RE.match(instance[embed][ext][tprop]): try: parser.parse(instance[embed][ext][tprop]) except ValueError as e: yield JSONError("'%s': '%s': '%s': '%s' is not a valid timestamp: %s" % (instance['type'], ext, tprop, instance[embed][ext][tprop], str(e)), instance['id']) - elif tprop in instance[embed] and ts_re.match(instance[embed][tprop]): + elif tprop in instance[embed] and TIMESTAMP_FORMAT_RE.match(instance[embed][tprop]): try: parser.parse(instance[embed][tprop]) except ValueError as e: @@ -107,6 +108,28 @@ def timestamp(instance): % (instance['type'], tprop, instance[embed][tprop], str(e)), instance['id']) +def compare(first, op, second): + comp = getattr(operator, op) + if TIMESTAMP_FORMAT_RE.match(first) and TIMESTAMP_FORMAT_RE.match(second): + created_datetime = datetime_from_str(first) + modified_datetime = datetime_from_str(second) + if isinstance(created_datetime, datetime) and isinstance(modified_datetime, datetime): + return comp(created_datetime, modified_datetime) + return comp(first, second) + + +def datetime_from_str(str_datetime): + pattern = '%Y-%m-%dT%H:%M:%S.%fZ' + try: + return datetime.strptime(str_datetime, pattern) + except ValueError: + pattern = '%Y-%m-%dT%H:%M:%SZ' + try: + return datetime.strptime(str_datetime, pattern) + except ValueError: + return str_datetime + + def get_comparison_string(op): """Return a string explaining the given comparison operator. """ @@ -128,11 +151,9 @@ def timestamp_compare(instance): compares.extend(additional_compares) for first, op, second in compares: - comp = getattr(operator, op) - comp_str = get_comparison_string(op) - if first in instance and second in instance and \ - not comp(instance[first], instance[second]): + not compare(instance[first], op, instance[second]): + comp_str = get_comparison_string(op) msg = "'%s' (%s) must be %s '%s' (%s)" yield JSONError(msg % (first, instance[first], comp_str, second, instance[second]), instance['id']) @@ -145,11 +166,9 @@ def observable_timestamp_compare(instance): """ compares = enums.TIMESTAMP_COMPARE_OBSERVABLE.get(instance.get('type', ''), []) for first, op, second in compares: - comp = getattr(operator, op) - comp_str = get_comparison_string(op) - if first in instance and second in instance and \ - not comp(instance[first], instance[second]): + not compare(instance[first], op, instance[second]): + comp_str = get_comparison_string(op) msg = "In object '%s', '%s' (%s) must be %s '%s' (%s)" yield JSONError(msg % (instance['id'], first, instance[first], comp_str, second, instance[second]), instance['id'])