Repository: oasis-open/cti-stix-validator
Pull request #198 (2022-08-08 21:23:31)

Fixed timestamp fields comparison
I just discovered this kind of behavior can happen when 2 timestamp values don't have the same time precision:
![image](https://user-images.githubusercontent.com/16307976/183508984-baec4882-bdf4-4db1-b0e7-4f1068ca3ab7.png)

I did not find in the standard any rule to forbid this difference in time precision, so I guess it should be valid.

The issue comes from the python comparison operators used to compare strings, which will always consider a timestamp value with fewer digits later than the same value with more digits.

I just added an additional test to compare the datetime formatted values before raising a validation exception due to this particular behavior of the strings comparison

```python
In [1]: created = '2022-08-03T07:11:07.029Z'

In [2]: modified = '2022-08-03T07:11:07.029036Z'

In [3]: modified >= created
Out[3]: False

In [4]: from datetime import datetime

In [5]: regex = '%Y-%m-%dT%H:%M:%S.%fZ'

In [6]: datetime.strptime(modified, regex) >= datetime.strptime(created, regex)
Out[6]: True
```


Patch:
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 ..output import info
 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 @@ CUSTOM_PROPERTY_LAX_PREFIX_RE = re.compile(r"^x_.+$")
 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 ..output import info
 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 @@ CUSTOM_EXT_LAX_PREFIX_RE = re.compile(r"^x\-.+\-ext$")
 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'])


Test patch:
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 @@ class AttackPatternTestCases(ValidatorTest):
 
         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 @@ class CustomObjectTestCases(ValidatorTest):
         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 @@ class IdentityTestCases(ValidatorTest):
         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 @@ class IndicatorTestCases(ValidatorTest):
         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 @@ class IntrusionSetTestCases(ValidatorTest):
         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 @@ class MalwareTestCases(ValidatorTest):
         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 @@ class ObservedDataTestCases(ValidatorTest):
         }
         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 @@ class RelationshipTestCases(ValidatorTest):
         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 @@ class IdentityTestCases(ValidatorTest):
 
     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 @@ class IdentityTestCases(ValidatorTest):
         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 @@ class ThreatActorTestCases(ValidatorTest):
         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 @@ class ToolTestCases(ValidatorTest):
         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 @@ class VulnerabilityTestCases(ValidatorTest):
         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 @@ class AttackPatternTestCases(ValidatorTest):
         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 @@ class CustomObjectTestCases(ValidatorTest):
         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 @@ class IdentityTestCases(ValidatorTest):
         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 @@ class IncidentTestCases(ValidatorTest):
         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 @@ class IndicatorTestCases(ValidatorTest):
         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 @@ class IntrusionSetTestCases(ValidatorTest):
 
         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 @@ class LanguageContentTestCases(ValidatorTest):
             "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 @@ class LocationTestCases(ValidatorTest):
         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 @@ class MalwareTestCases(ValidatorTest):
 
         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 @@ class MalwareTestCases(ValidatorTest):
         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 @@ class ObservedDataTestCases(ValidatorTest):
 
     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 @@ VALID_OBSERVED_DATA_DEFINITION = u"""
   "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 @@ class ObservedDataTestCases(ValidatorTest):
         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 @@ class ObservedDataTestCases(ValidatorTest):
 
     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 @@ class RelationshipTestCases(ValidatorTest):
         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 @@ VALID_REPORT = u"""
   "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 @@ class IdentityTestCases(ValidatorTest):
 
     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 @@ class IdentityTestCases(ValidatorTest):
         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 @@ class ThreatActorTestCases(ValidatorTest):
         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 @@ class ToolTestCases(ValidatorTest):
         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 @@ class VulnerabilityTestCases(ValidatorTest):
         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']