Repository: oasis-open/cti-python-stix2
Pull request #275 (2019-07-02 17:20:13)

Stix21master


Patch:
diff --git a/stix2/v21/__init__.py b/stix2/v21/__init__.py
index efac071..b2451d2 100644
--- a/stix2/v21/__init__.py
+++ b/stix2/v21/__init__.py
@@ -32,9 +32,10 @@ from .observables import (
     X509Certificate, X509V3ExtenstionsType,
 )
 from .sdo import (
-    AttackPattern, Campaign, CourseOfAction, CustomObject, Identity, Indicator,
-    IntrusionSet, Location, Malware, MalwareAnalysis, Note, ObservedData,
-    Opinion, Report, ThreatActor, Tool, Vulnerability,
+    AttackPattern, Campaign, CourseOfAction, CustomObject, Grouping, Identity,
+    Indicator, Infrastructure, IntrusionSet, Location, Malware,
+    MalwareAnalysis, Note, ObservedData, Opinion, Report, ThreatActor, Tool,
+    Vulnerability,
 )
 from .sro import Relationship, Sighting
 
@@ -43,8 +44,10 @@ OBJ_MAP = {
     'bundle': Bundle,
     'campaign': Campaign,
     'course-of-action': CourseOfAction,
+    'grouping': Grouping,
     'identity': Identity,
     'indicator': Indicator,
+    'infrastructure': Infrastructure,
     'intrusion-set': IntrusionSet,
     'language-content': LanguageContent,
     'location': Location,
diff --git a/stix2/v21/sdo.py b/stix2/v21/sdo.py
index 0717f88..8ec4131 100644
--- a/stix2/v21/sdo.py
+++ b/stix2/v21/sdo.py
@@ -122,6 +122,34 @@ class CourseOfAction(STIXDomainObject):
         )
 
 
+class Grouping(STIXDomainObject):
+    # TODO: Add link
+    """For more detailed information on this object's properties, see
+    `the STIX 2.1 specification <link here>`__.
+    """
+
+    _type = 'grouping'
+    _properties = OrderedDict([
+        ('type', TypeProperty(_type)),
+        ('spec_version', StringProperty(fixed='2.1')),
+        ('id', IDProperty(_type, spec_version='2.1')),
+        ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
+        ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
+        ('created_by_ref', ReferenceProperty(type='identity', spec_version='2.1')),
+        ('revoked', BooleanProperty(default=lambda: False)),
+        ('labels', ListProperty(StringProperty)),
+        ('confidence', IntegerProperty()),
+        ('lang', StringProperty()),
+        ('external_references', ListProperty(ExternalReference)),
+        ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition', spec_version='2.1'))),
+        ('granular_markings', ListProperty(GranularMarking)),
+        ('name', StringProperty()),
+        ('description', StringProperty()),
+        ('context', StringProperty(required=True)),
+        ('object_refs', ListProperty(ReferenceProperty)),
+    ])
+
+
 class Identity(STIXDomainObject):
     # TODO: Add link
     """For more detailed information on this object's properties, see
@@ -170,7 +198,7 @@ class Indicator(STIXDomainObject):
         ('description', StringProperty()),
         ('indicator_types', ListProperty(StringProperty, required=True)),
         ('pattern', PatternProperty(required=True)),
-        ('valid_from', TimestampProperty(default=lambda: NOW)),
+        ('valid_from', TimestampProperty(default=lambda: NOW, required=True)),
         ('valid_until', TimestampProperty()),
         ('kill_chain_phases', ListProperty(KillChainPhase)),
         ('revoked', BooleanProperty(default=lambda: False)),
@@ -193,6 +221,46 @@ class Indicator(STIXDomainObject):
             raise ValueError(msg.format(self))
 
 
+class Infrastructure(STIXDomainObject):
+    # TODO: Add link
+    """For more detailed information on this object's properties, see
+    `the STIX 2.1 specification <link here>`__.
+    """
+
+    _type = 'infrastructure'
+    _properties = OrderedDict([
+        ('type', TypeProperty(_type)),
+        ('spec_version', StringProperty(fixed='2.1')),
+        ('id', IDProperty(_type, spec_version='2.1')),
+        ('created_by_ref', ReferenceProperty(type='identity', spec_version='2.1')),
+        ('created', TimestampProperty(default=lambda: NOW, precision='millisecond')),
+        ('modified', TimestampProperty(default=lambda: NOW, precision='millisecond')),
+        ('revoked', BooleanProperty(default=lambda: False)),
+        ('labels', ListProperty(StringProperty)),
+        ('confidence', IntegerProperty()),
+        ('lang', StringProperty()),
+        ('external_references', ListProperty(ExternalReference)),
+        ('object_marking_refs', ListProperty(ReferenceProperty(type='marking-definition', spec_version='2.1'))),
+        ('granular_markings', ListProperty(GranularMarking)),
+        ('name', StringProperty(required=True)),
+        ('description', StringProperty()),
+        ('infrastructure_types', ListProperty(StringProperty, required=True)),
+        ('kill_chain_phases', ListProperty(KillChainPhase)),
+        ('first_seen', TimestampProperty()),
+        ('last_seen', TimestampProperty()),
+    ])
+
+    def _check_object_constraints(self):
+        super(self.__class__, self)._check_object_constraints()
+
+        first_seen = self.get('first_seen')
+        last_seen = self.get('last_seen')
+
+        if first_seen and last_seen and last_seen < first_seen:
+            msg = "{0.id} 'last_seen' must be greater than or equal to 'first_seen'"
+            raise ValueError(msg.format(self))
+
+
 class IntrusionSet(STIXDomainObject):
     # TODO: Add link
     """For more detailed information on this object's properties, see
@@ -346,7 +414,16 @@ class Malware(STIXDomainObject):
         ('name', StringProperty(required=True)),
         ('description', StringProperty()),
         ('malware_types', ListProperty(StringProperty, required=True)),
+        ('is_family', BooleanProperty(required=True)),
+        ('aliases', ListProperty(StringProperty)),
         ('kill_chain_phases', ListProperty(KillChainPhase)),
+        ('first_seen', TimestampProperty()),
+        ('last_seen', TimestampProperty()),
+        ('os_execution_envs', ListProperty(StringProperty)),
+        ('architecture_execution_envs', ListProperty(StringProperty)),
+        ('implementation_languages', ListProperty(StringProperty)),
+        ('capabilities', ListProperty(StringProperty)),
+        ('sample_refs', ListProperty(ReferenceProperty(spec_version='2.1'))),
         ('revoked', BooleanProperty(default=lambda: False)),
         ('labels', ListProperty(StringProperty)),
         ('confidence', IntegerProperty()),
@@ -356,6 +433,16 @@ class Malware(STIXDomainObject):
         ('granular_markings', ListProperty(GranularMarking)),
     ])
 
+    def _check_object_constraints(self):
+        super(self.__class__, self)._check_object_constraints()
+
+        first_seen = self.get('first_seen')
+        last_seen = self.get('last_seen')
+
+        if first_seen and last_seen and last_seen < first_seen:
+            msg = "{0.id} 'last_seen' must be greater than or equal to 'first_seen'"
+            raise ValueError(msg.format(self))
+
 
 class MalwareAnalysis(STIXDomainObject):
     # TODO: Add link
@@ -596,6 +683,7 @@ class Tool(STIXDomainObject):
         ('name', StringProperty(required=True)),
         ('description', StringProperty()),
         ('tool_types', ListProperty(StringProperty, required=True)),
+        ('aliases', ListProperty(StringProperty)),
         ('kill_chain_phases', ListProperty(KillChainPhase)),
         ('tool_version', StringProperty()),
         ('revoked', BooleanProperty(default=lambda: False)),


Test patch:
diff --git a/stix2/test/v20/test_datastore_filesystem.py b/stix2/test/v20/test_datastore_filesystem.py
index 25de37e..317f927 100644
--- a/stix2/test/v20/test_datastore_filesystem.py
+++ b/stix2/test/v20/test_datastore_filesystem.py
@@ -125,15 +125,13 @@ def rel_fs_store():
 
 
 def test_filesystem_source_nonexistent_folder():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.FileSystemSource('nonexistent-folder')
-    assert "for STIX data does not exist" in str(excinfo)
 
 
 def test_filesystem_sink_nonexistent_folder():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.FileSystemSink('nonexistent-folder')
-    assert "for STIX data does not exist" in str(excinfo)
 
 
 def test_filesystem_source_bad_json_file(fs_source, bad_json_files):
@@ -441,9 +439,8 @@ def test_filesystem_attempt_stix_file_overwrite(fs_store):
     )
 
     # Now attempt to overwrite the existing file
-    with pytest.raises(DataSourceError) as excinfo:
+    with pytest.raises(DataSourceError):
         fs_store.add(camp8)
-    assert "Attempted to overwrite file" in str(excinfo)
 
     os.remove(filepath)
 
diff --git a/stix2/test/v20/test_pattern_expressions.py b/stix2/test/v20/test_pattern_expressions.py
index 3dc7cde..23a401b 100644
--- a/stix2/test/v20/test_pattern_expressions.py
+++ b/stix2/test/v20/test_pattern_expressions.py
@@ -257,7 +257,7 @@ def test_and_observable_expression():
 
 
 def test_invalid_and_observable_expression():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.AndBooleanExpression([
             stix2.EqualityComparisonExpression(
                 "user-account:display_name",
@@ -268,7 +268,6 @@ def test_invalid_and_observable_expression():
                 stix2.StringConstant("admin"),
             ),
         ])
-    assert "All operands to an 'AND' expression must have the same object type" in str(excinfo)
 
 
 def test_hex():
@@ -352,30 +351,26 @@ def test_list2():
 
 
 def test_invalid_constant_type():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.EqualityComparisonExpression(
             "artifact:payload_bin",
             {'foo': 'bar'},
         )
-    assert 'Unable to create a constant' in str(excinfo)
 
 
 def test_invalid_integer_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.IntegerConstant('foo')
-    assert 'must be an integer' in str(excinfo)
 
 
 def test_invalid_timestamp_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.TimestampConstant('foo')
-    assert 'Must be a datetime object or timestamp string' in str(excinfo)
 
 
 def test_invalid_float_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.FloatConstant('foo')
-    assert 'must be a float' in str(excinfo)
 
 
 @pytest.mark.parametrize(
@@ -400,9 +395,8 @@ def test_boolean_constant(data, result):
 
 
 def test_invalid_boolean_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.BooleanConstant('foo')
-    assert 'must be a boolean' in str(excinfo)
 
 
 @pytest.mark.parametrize(
@@ -412,21 +406,18 @@ def test_invalid_boolean_constant():
     ],
 )
 def test_invalid_hash_constant(hashtype, data):
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.HashConstant(data, hashtype)
-    assert 'is not a valid {} hash'.format(hashtype) in str(excinfo)
 
 
 def test_invalid_hex_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.HexConstant('mm')
-    assert "must contain an even number of hexadecimal characters" in str(excinfo)
 
 
 def test_invalid_binary_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.BinaryConstant('foo')
-    assert 'must contain a base64' in str(excinfo)
 
 
 def test_escape_quotes_and_backslashes():
@@ -459,15 +450,13 @@ def test_repeat_qualifier():
 
 
 def test_invalid_repeat_qualifier():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.RepeatQualifier('foo')
-    assert 'is not a valid argument for a Repeat Qualifier' in str(excinfo)
 
 
 def test_invalid_within_qualifier():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.WithinQualifier('foo')
-    assert 'is not a valid argument for a Within Qualifier' in str(excinfo)
 
 
 def test_startstop_qualifier():
@@ -485,19 +474,17 @@ def test_startstop_qualifier():
 
 
 def test_invalid_startstop_qualifier():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.StartStopQualifier(
             'foo',
             stix2.TimestampConstant('2016-06-01T00:00:00Z'),
         )
-    assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo)
 
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.StartStopQualifier(
             datetime.date(2016, 6, 1),
             'foo',
         )
-    assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo)
 
 
 def test_make_constant_already_a_constant():
diff --git a/stix2/test/v21/conftest.py b/stix2/test/v21/conftest.py
index dea29ca..ea2853d 100644
--- a/stix2/test/v21/conftest.py
+++ b/stix2/test/v21/conftest.py
@@ -5,7 +5,8 @@ import pytest
 import stix2
 
 from .constants import (
-    FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS, RELATIONSHIP_KWARGS,
+    FAKE_TIME, GROUPING_KWARGS, INDICATOR_KWARGS, INFRASTRUCTURE_KWARGS,
+    MALWARE_KWARGS, RELATIONSHIP_KWARGS,
 )
 
 
@@ -39,6 +40,16 @@ def indicator(uuid4, clock):
     return stix2.v21.Indicator(**INDICATOR_KWARGS)
 
 
+@pytest.fixture
+def infrastructure(uuid4, clock):
+    return stix2.v21.Infrastructure(**INFRASTRUCTURE_KWARGS)
+
+
+@pytest.fixture
+def grouping(uuid4, clock):
+    return stix2.v21.Grouping(**GROUPING_KWARGS)
+
+
 @pytest.fixture
 def malware(uuid4, clock):
     return stix2.v21.Malware(**MALWARE_KWARGS)
diff --git a/stix2/test/v21/constants.py b/stix2/test/v21/constants.py
index b0ba1ef..40a2bb5 100644
--- a/stix2/test/v21/constants.py
+++ b/stix2/test/v21/constants.py
@@ -7,8 +7,10 @@ FAKE_TIME = dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
 ATTACK_PATTERN_ID = "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061"
 CAMPAIGN_ID = "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
 COURSE_OF_ACTION_ID = "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
+GROUPING_ID = "grouping--753abcde-3141-5926-ace5-0a810b1ff996"
 IDENTITY_ID = "identity--311b2d2d-f010-4473-83ec-1edf84858f4c"
 INDICATOR_ID = "indicator--a740531e-63ff-4e49-a9e1-a0a3eed0e3e7"
+INFRASTRUCTURE_ID = "infrastructure--3000ae1b-784c-f03d-8abc-0a625b2ff018"
 INTRUSION_SET_ID = "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29"
 LOCATION_ID = "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64"
 MALWARE_ID = "malware--9c4638ec-f1de-4ddb-abf4-1b760417654e"
@@ -70,6 +72,11 @@ COURSE_OF_ACTION_KWARGS = dict(
     name="Block",
 )
 
+GROUPING_KWARGS = dict(
+    name="Harry Potter and the Leet Hackers",
+    context="suspicious-activity",
+)
+
 IDENTITY_KWARGS = dict(
     name="John Smith",
     identity_class="individual",
@@ -78,6 +85,12 @@ IDENTITY_KWARGS = dict(
 INDICATOR_KWARGS = dict(
     indicator_types=['malicious-activity'],
     pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']",
+    valid_from="2017-01-01T12:34:56Z",
+)
+
+INFRASTRUCTURE_KWARGS = dict(
+    name="Poison Ivy C2",
+    infrastructure_types=["command-and-control"],
 )
 
 INTRUSION_SET_KWARGS = dict(
@@ -87,6 +100,7 @@ INTRUSION_SET_KWARGS = dict(
 MALWARE_KWARGS = dict(
     malware_types=['ransomware'],
     name="Cryptolocker",
+    is_family=False,
 )
 
 MALWARE_MORE_KWARGS = dict(
@@ -97,6 +111,7 @@ MALWARE_MORE_KWARGS = dict(
     malware_types=['ransomware'],
     name="Cryptolocker",
     description="A ransomware related to ...",
+    is_family=False,
 )
 
 OBSERVED_DATA_KWARGS = dict(
diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json
index 54343ce..23e28bb 100644
--- a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json
+++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json
@@ -24,5 +24,6 @@
     ],
     "object_marking_refs": [
         "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
-    ]
+    ],
+    "is_family": false
 }
diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json
index 1bedc5b..f65449d 100644
--- a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json
+++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20170531213258226477.json
@@ -27,7 +27,8 @@
                 "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
             ],
             "spec_version": "2.1",
-            "type": "malware"
+            "type": "malware",
+            "is_family": false
         }
     ],
     "type": "bundle"
diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json
index 4236920..1b22cf2 100644
--- a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json
+++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448456000.json
@@ -24,5 +24,6 @@
     ],
     "object_marking_refs": [
         "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
-    ]
+    ],
+    "is_family": false
 }
diff --git a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json
index 37dd9c5..7802c50 100644
--- a/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json
+++ b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38/20181101232448457000.json
@@ -24,5 +24,6 @@
     ],
     "object_marking_refs": [
         "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
-    ]
+    ],
+    "is_family": false
 }
diff --git a/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json b/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json
index 0b7c01e..24f3837 100644
--- a/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json
+++ b/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841/20170531213326565056.json
@@ -27,7 +27,8 @@
                 "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
             ],
             "spec_version": "2.1",
-            "type": "malware"
+            "type": "malware",
+            "is_family": false
         }
     ],
     "type": "bundle"
diff --git a/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json b/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json
index 195c973..8495bfe 100644
--- a/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json
+++ b/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e/20170531213248482655.json
@@ -27,7 +27,8 @@
                 "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
             ],
             "spec_version": "2.1",
-            "type": "malware"
+            "type": "malware",
+            "is_family": false
         }
     ],
     "type": "bundle"
diff --git a/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json b/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json
index 4d57db5..a509a5e 100644
--- a/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json
+++ b/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b/20170531213215263882.json
@@ -26,7 +26,8 @@
             "object_marking_refs": [
                 "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"
             ],
-            "type": "malware"
+            "type": "malware",
+            "is_family": false
         }
     ],
     "spec_version": "2.0",
diff --git a/stix2/test/v21/test_bundle.py b/stix2/test/v21/test_bundle.py
index 47d0a7a..7ba0729 100644
--- a/stix2/test/v21/test_bundle.py
+++ b/stix2/test/v21/test_bundle.py
@@ -31,7 +31,8 @@ EXPECTED_BUNDLE = """{
             "name": "Cryptolocker",
             "malware_types": [
                 "ransomware"
-            ]
+            ],
+            "is_family": false
         },
         {
             "type": "relationship",
@@ -72,6 +73,7 @@ EXPECTED_BUNDLE_DICT = {
             "malware_types": [
                 "ransomware",
             ],
+            "is_family": False,
         },
         {
             "type": "relationship",
@@ -244,6 +246,7 @@ def test_bundle_obj_id_found():
                 "malware_types": [
                     "ransomware",
                 ],
+                "is_family": False,
             },
             {
                 "type": "malware",
@@ -255,6 +258,7 @@ def test_bundle_obj_id_found():
                 "malware_types": [
                     "ransomware",
                 ],
+                "is_family": False,
             },
             {
                 "type": "relationship",
diff --git a/stix2/test/v21/test_core.py b/stix2/test/v21/test_core.py
index bf45f32..06a829c 100644
--- a/stix2/test/v21/test_core.py
+++ b/stix2/test/v21/test_core.py
@@ -31,6 +31,7 @@ BUNDLE = {
             "malware_types": [
                 "ransomware",
             ],
+            "is_family": False,
         },
         {
             "type": "relationship",
diff --git a/stix2/test/v21/test_datastore_filesystem.py b/stix2/test/v21/test_datastore_filesystem.py
index 34b1088..9917ccd 100644
--- a/stix2/test/v21/test_datastore_filesystem.py
+++ b/stix2/test/v21/test_datastore_filesystem.py
@@ -124,15 +124,13 @@ def rel_fs_store():
 
 
 def test_filesystem_source_nonexistent_folder():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.FileSystemSource('nonexistent-folder')
-    assert "for STIX data does not exist" in str(excinfo)
 
 
 def test_filesystem_sink_nonexistent_folder():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.FileSystemSink('nonexistent-folder')
-    assert "for STIX data does not exist" in str(excinfo)
 
 
 def test_filesystem_source_bad_json_file(fs_source, bad_json_files):
diff --git a/stix2/test/v21/test_datastore_filters.py b/stix2/test/v21/test_datastore_filters.py
index 4b9878a..cbe3fe4 100644
--- a/stix2/test/v21/test_datastore_filters.py
+++ b/stix2/test/v21/test_datastore_filters.py
@@ -16,6 +16,7 @@ stix_objs = [
             "remote-access-trojan",
         ],
         "modified": "2017-01-27T13:49:53.997Z",
+        "is_family": False,
         "name": "Poison Ivy",
         "type": "malware",
     },
diff --git a/stix2/test/v21/test_environment.py b/stix2/test/v21/test_environment.py
index e08971e..90f31cb 100644
--- a/stix2/test/v21/test_environment.py
+++ b/stix2/test/v21/test_environment.py
@@ -219,7 +219,8 @@ def test_parse_malware():
         "name": "Cryptolocker",
         "malware_types": [
             "ransomware"
-        ]
+        ],
+        "is_family": false
     }"""
     mal = env.parse(data, version="2.1")
 
@@ -230,6 +231,7 @@ def test_parse_malware():
     assert mal.modified == FAKE_TIME
     assert mal.malware_types == ['ransomware']
     assert mal.name == "Cryptolocker"
+    assert not mal.is_family
 
 
 def test_creator_of():
@@ -351,6 +353,7 @@ def test_related_to_no_id(ds):
     mal = {
         "type": "malware",
         "name": "some variant",
+        "is_family": False,
     }
     with pytest.raises(ValueError) as excinfo:
         env.related_to(mal)
diff --git a/stix2/test/v21/test_grouping.py b/stix2/test/v21/test_grouping.py
new file mode 100644
index 0000000..405a80c
--- /dev/null
+++ b/stix2/test/v21/test_grouping.py
@@ -0,0 +1,112 @@
+import datetime as dt
+
+import pytest
+import pytz
+
+import stix2
+
+from .constants import FAKE_TIME, GROUPING_ID, GROUPING_KWARGS
+
+EXPECTED_GROUPING = """{
+    "type": "grouping",
+    "spec_version": "2.1",
+    "id": "grouping--753abcde-3141-5926-ace5-0a810b1ff996",
+    "created": "2017-01-01T12:34:56.000Z",
+    "modified": "2017-01-01T12:34:56.000Z",
+    "name": "Harry Potter and the Leet Hackers",
+    "context": "suspicious-activity"
+}"""
+
+
+def test_grouping_with_all_required_properties():
+    now = dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
+
+    grp = stix2.v21.Grouping(
+        type="grouping",
+        id=GROUPING_ID,
+        created=now,
+        modified=now,
+        name="Harry Potter and the Leet Hackers",
+        context="suspicious-activity",
+    )
+
+    assert str(grp) == EXPECTED_GROUPING
+
+
+def test_grouping_autogenerated_properties(grouping):
+    assert grouping.type == 'grouping'
+    assert grouping.id == 'grouping--00000000-0000-4000-8000-000000000001'
+    assert grouping.created == FAKE_TIME
+    assert grouping.modified == FAKE_TIME
+    assert grouping.name == "Harry Potter and the Leet Hackers"
+    assert grouping.context == "suspicious-activity"
+
+    assert grouping['type'] == 'grouping'
+    assert grouping['id'] == 'grouping--00000000-0000-4000-8000-000000000001'
+    assert grouping['created'] == FAKE_TIME
+    assert grouping['modified'] == FAKE_TIME
+    assert grouping['name'] == "Harry Potter and the Leet Hackers"
+    assert grouping['context'] == "suspicious-activity"
+
+
+def test_grouping_type_must_be_grouping():
+    with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
+        stix2.v21.Grouping(type='xxx', **GROUPING_KWARGS)
+
+    assert excinfo.value.cls == stix2.v21.Grouping
+    assert excinfo.value.prop_name == "type"
+    assert excinfo.value.reason == "must equal 'grouping'."
+    assert str(excinfo.value) == "Invalid value for Grouping 'type': must equal 'grouping'."
+
+
+def test_grouping_id_must_start_with_grouping():
+    with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
+        stix2.v21.Grouping(id='my-prefix--', **GROUPING_KWARGS)
+
+    assert excinfo.value.cls == stix2.v21.Grouping
+    assert excinfo.value.prop_name == "id"
+    assert excinfo.value.reason == "must start with 'grouping--'."
+    assert str(excinfo.value) == "Invalid value for Grouping 'id': must start with 'grouping--'."
+
+
+def test_grouping_required_properties():
+    with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo:
+        stix2.v21.Grouping()
+
+    assert excinfo.value.cls == stix2.v21.Grouping
+    assert excinfo.value.properties == ["context"]
+
+
+def test_invalid_kwarg_to_grouping():
+    with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo:
+        stix2.v21.Grouping(my_custom_property="foo", **GROUPING_KWARGS)
+
+    assert excinfo.value.cls == stix2.v21.Grouping
+    assert excinfo.value.properties == ['my_custom_property']
+    assert str(excinfo.value) == "Unexpected properties for Grouping: (my_custom_property)."
+
+
+@pytest.mark.parametrize(
+    "data", [
+        EXPECTED_GROUPING,
+        {
+            "type": "grouping",
+            "spec_version": "2.1",
+            "id": GROUPING_ID,
+            "created": "2017-01-01T12:34:56.000Z",
+            "modified": "2017-01-01T12:34:56.000Z",
+            "name": "Harry Potter and the Leet Hackers",
+            "context": "suspicious-activity",
+        },
+    ],
+)
+def test_parse_grouping(data):
+    grp = stix2.parse(data)
+
+    assert grp.type == 'grouping'
+    assert grp.spec_version == '2.1'
+    assert grp.id == GROUPING_ID
+    assert grp.created == dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
+    assert grp.modified == dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
+    assert grp.name == "Harry Potter and the Leet Hackers"
+    assert grp.context == "suspicious-activity"
diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py
index 49bc6e0..b68b887 100644
--- a/stix2/test/v21/test_indicator.py
+++ b/stix2/test/v21/test_indicator.py
@@ -98,8 +98,8 @@ def test_indicator_required_properties():
         stix2.v21.Indicator()
 
     assert excinfo.value.cls == stix2.v21.Indicator
-    assert excinfo.value.properties == ["indicator_types", "pattern"]
-    assert str(excinfo.value) == "No values for required properties for Indicator: (indicator_types, pattern)."
+    assert excinfo.value.properties == ["indicator_types", "pattern", "valid_from"]
+    assert str(excinfo.value) == "No values for required properties for Indicator: (indicator_types, pattern, valid_from)."
 
 
 def test_indicator_required_property_pattern():
@@ -107,7 +107,7 @@ def test_indicator_required_property_pattern():
         stix2.v21.Indicator(indicator_types=['malicious-activity'])
 
     assert excinfo.value.cls == stix2.v21.Indicator
-    assert excinfo.value.properties == ["pattern"]
+    assert excinfo.value.properties == ["pattern", "valid_from"]
 
 
 def test_indicator_created_ref_invalid_format():
@@ -184,6 +184,7 @@ def test_invalid_indicator_pattern():
         stix2.v21.Indicator(
             indicator_types=['malicious-activity'],
             pattern="file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e'",
+            valid_from="2017-01-01T12:34:56Z",
         )
     assert excinfo.value.cls == stix2.v21.Indicator
     assert excinfo.value.prop_name == 'pattern'
@@ -193,6 +194,7 @@ def test_invalid_indicator_pattern():
         stix2.v21.Indicator(
             indicator_types=['malicious-activity'],
             pattern='[file:hashes.MD5 = "d41d8cd98f00b204e9800998ecf8427e"]',
+            valid_from="2017-01-01T12:34:56Z",
         )
     assert excinfo.value.cls == stix2.v21.Indicator
     assert excinfo.value.prop_name == 'pattern'
diff --git a/stix2/test/v21/test_infrastructure.py b/stix2/test/v21/test_infrastructure.py
new file mode 100644
index 0000000..30632bb
--- /dev/null
+++ b/stix2/test/v21/test_infrastructure.py
@@ -0,0 +1,158 @@
+import datetime as dt
+
+import pytest
+import pytz
+
+import stix2
+
+from .constants import FAKE_TIME, INFRASTRUCTURE_ID, INFRASTRUCTURE_KWARGS
+
+EXPECTED_INFRASTRUCTURE = """{
+    "type": "infrastructure",
+    "spec_version": "2.1",
+    "id": "infrastructure--3000ae1b-784c-f03d-8abc-0a625b2ff018",
+    "created": "2017-01-01T12:34:56.000Z",
+    "modified": "2017-01-01T12:34:56.000Z",
+    "name": "Poison Ivy C2",
+    "infrastructure_types": [
+        "command-and-control"
+    ]
+}"""
+
+
+def test_infrastructure_with_all_required_properties():
+    now = dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
+
+    infra = stix2.v21.Infrastructure(
+        type="infrastructure",
+        id=INFRASTRUCTURE_ID,
+        created=now,
+        modified=now,
+        name="Poison Ivy C2",
+        infrastructure_types=["command-and-control"],
+    )
+
+    assert str(infra) == EXPECTED_INFRASTRUCTURE
+
+
+def test_infrastructure_autogenerated_properties(infrastructure):
+    assert infrastructure.type == 'infrastructure'
+    assert infrastructure.id == 'infrastructure--00000000-0000-4000-8000-000000000001'
+    assert infrastructure.created == FAKE_TIME
+    assert infrastructure.modified == FAKE_TIME
+    assert infrastructure.infrastructure_types == ['command-and-control']
+    assert infrastructure.name == "Poison Ivy C2"
+
+    assert infrastructure['type'] == 'infrastructure'
+    assert infrastructure['id'] == 'infrastructure--00000000-0000-4000-8000-000000000001'
+    assert infrastructure['created'] == FAKE_TIME
+    assert infrastructure['modified'] == FAKE_TIME
+    assert infrastructure['infrastructure_types'] == ['command-and-control']
+    assert infrastructure['name'] == "Poison Ivy C2"
+
+
+def test_infrastructure_type_must_be_infrastructure():
+    with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
+        stix2.v21.Infrastructure(type='xxx', **INFRASTRUCTURE_KWARGS)
+
+    assert excinfo.value.cls == stix2.v21.Infrastructure
+    assert excinfo.value.prop_name == "type"
+    assert excinfo.value.reason == "must equal 'infrastructure'."
+    assert str(excinfo.value) == "Invalid value for Infrastructure 'type': must equal 'infrastructure'."
+
+
+def test_infrastructure_id_must_start_with_infrastructure():
+    with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo:
+        stix2.v21.Infrastructure(id='my-prefix--', **INFRASTRUCTURE_KWARGS)
+
+    assert excinfo.value.cls == stix2.v21.Infrastructure
+    assert excinfo.value.prop_name == "id"
+    assert excinfo.value.reason == "must start with 'infrastructure--'."
+    assert str(excinfo.value) == "Invalid value for Infrastructure 'id': must start with 'infrastructure--'."
+
+
+def test_infrastructure_required_properties():
+    with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo:
+        stix2.v21.Infrastructure()
+
+    assert excinfo.value.cls == stix2.v21.Infrastructure
+    assert excinfo.value.properties == ["infrastructure_types", "name"]
+
+
+def test_infrastructure_required_property_name():
+    with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo:
+        stix2.v21.Infrastructure(infrastructure_types=['command-and-control'])
+
+    assert excinfo.value.cls == stix2.v21.Infrastructure
+    assert excinfo.value.properties == ["name"]
+
+
+def test_invalid_kwarg_to_infrastructure():
+    with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo:
+        stix2.v21.Infrastructure(my_custom_property="foo", **INFRASTRUCTURE_KWARGS)
+
+    assert excinfo.value.cls == stix2.v21.Infrastructure
+    assert excinfo.value.properties == ['my_custom_property']
+    assert str(excinfo.value) == "Unexpected properties for Infrastructure: (my_custom_property)."
+
+
+@pytest.mark.parametrize(
+    "data", [
+        EXPECTED_INFRASTRUCTURE,
+        {
+            "type": "infrastructure",
+            "spec_version": "2.1",
+            "id": INFRASTRUCTURE_ID,
+            "created": "2017-01-01T12:34:56.000Z",
+            "modified": "2017-01-01T12:34:56.000Z",
+            "infrastructure_types": ["command-and-control"],
+            "name": "Poison Ivy C2",
+        },
+    ],
+)
+def test_parse_infrastructure(data):
+    infra = stix2.parse(data)
+
+    assert infra.type == 'infrastructure'
+    assert infra.spec_version == '2.1'
+    assert infra.id == INFRASTRUCTURE_ID
+    assert infra.created == dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
+    assert infra.modified == dt.datetime(2017, 1, 1, 12, 34, 56, tzinfo=pytz.utc)
+    assert infra.infrastructure_types == ['command-and-control']
+    assert infra.name == 'Poison Ivy C2'
+
+
+def test_parse_infrastructure_kill_chain_phases():
+    kill_chain = """
+      "kill_chain_phases": [
+        {
+          "kill_chain_name": "lockheed-martin-cyber-kill-chain",
+          "phase_name": "reconnaissance"
+        }
+      ]"""
+    data = EXPECTED_INFRASTRUCTURE.replace('infrastructure"', 'infrastructure",%s' % kill_chain)
+    infra = stix2.parse(data, version="2.1")
+    assert infra.kill_chain_phases[0].kill_chain_name == "lockheed-martin-cyber-kill-chain"
+    assert infra.kill_chain_phases[0].phase_name == "reconnaissance"
+    assert infra['kill_chain_phases'][0]['kill_chain_name'] == "lockheed-martin-cyber-kill-chain"
+    assert infra['kill_chain_phases'][0]['phase_name'] == "reconnaissance"
+
+
+def test_parse_infrastructure_clean_kill_chain_phases():
+    kill_chain = """
+      "kill_chain_phases": [
+        {
+          "kill_chain_name": "lockheed-martin-cyber-kill-chain",
+          "phase_name": 1
+        }
+      ]"""
+    data = EXPECTED_INFRASTRUCTURE.replace('2.1"', '2.1",%s' % kill_chain)
+    infra = stix2.parse(data, version="2.1")
+    assert infra['kill_chain_phases'][0]['phase_name'] == "1"
+
+
+def test_infrastructure_invalid_last_before_first():
+    with pytest.raises(ValueError) as excinfo:
+        stix2.v21.Infrastructure(first_seen="2017-01-01T12:34:56.000Z", last_seen="2017-01-01T12:33:56.000Z", **INFRASTRUCTURE_KWARGS)
+
+    assert "'last_seen' must be greater than or equal to 'first_seen'" in str(excinfo.value)
diff --git a/stix2/test/v21/test_malware.py b/stix2/test/v21/test_malware.py
index c55bfa9..1817c63 100644
--- a/stix2/test/v21/test_malware.py
+++ b/stix2/test/v21/test_malware.py
@@ -17,7 +17,8 @@ EXPECTED_MALWARE = """{
     "name": "Cryptolocker",
     "malware_types": [
         "ransomware"
-    ]
+    ],
+    "is_family": false
 }"""
 
 
@@ -31,6 +32,7 @@ def test_malware_with_all_required_properties():
         modified=now,
         malware_types=["ransomware"],
         name="Cryptolocker",
+        is_family=False,
     )
 
     assert str(mal) == EXPECTED_MALWARE
@@ -77,7 +79,7 @@ def test_malware_required_properties():
         stix2.v21.Malware()
 
     assert excinfo.value.cls == stix2.v21.Malware
-    assert excinfo.value.properties == ["malware_types", "name"]
+    assert excinfo.value.properties == ["is_family", "malware_types", "name"]
 
 
 def test_malware_required_property_name():
@@ -85,7 +87,7 @@ def test_malware_required_property_name():
         stix2.v21.Malware(malware_types=['ransomware'])
 
     assert excinfo.value.cls == stix2.v21.Malware
-    assert excinfo.value.properties == ["name"]
+    assert excinfo.value.properties == ["is_family", "name"]
 
 
 def test_cannot_assign_to_malware_attributes(malware):
@@ -115,6 +117,7 @@ def test_invalid_kwarg_to_malware():
             "modified": "2016-05-12T08:17:27.000Z",
             "malware_types": ["ransomware"],
             "name": "Cryptolocker",
+            "is_family": False,
         },
     ],
 )
@@ -128,6 +131,7 @@ def test_parse_malware(data):
     assert mal.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc)
     assert mal.malware_types == ['ransomware']
     assert mal.name == 'Cryptolocker'
+    assert not mal.is_family
 
 
 def test_parse_malware_invalid_labels():
@@ -164,3 +168,10 @@ def test_parse_malware_clean_kill_chain_phases():
     data = EXPECTED_MALWARE.replace('2.1"', '2.1",%s' % kill_chain)
     mal = stix2.parse(data, version="2.1")
     assert mal['kill_chain_phases'][0]['phase_name'] == "1"
+
+
+def test_malware_invalid_last_before_first():
+    with pytest.raises(ValueError) as excinfo:
+        stix2.v21.Malware(first_seen="2017-01-01T12:34:56.000Z", last_seen="2017-01-01T12:33:56.000Z", **MALWARE_KWARGS)
+
+    assert "'last_seen' must be greater than or equal to 'first_seen'" in str(excinfo.value)
diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py
index 3dc7cde..23a401b 100644
--- a/stix2/test/v21/test_pattern_expressions.py
+++ b/stix2/test/v21/test_pattern_expressions.py
@@ -257,7 +257,7 @@ def test_and_observable_expression():
 
 
 def test_invalid_and_observable_expression():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.AndBooleanExpression([
             stix2.EqualityComparisonExpression(
                 "user-account:display_name",
@@ -268,7 +268,6 @@ def test_invalid_and_observable_expression():
                 stix2.StringConstant("admin"),
             ),
         ])
-    assert "All operands to an 'AND' expression must have the same object type" in str(excinfo)
 
 
 def test_hex():
@@ -352,30 +351,26 @@ def test_list2():
 
 
 def test_invalid_constant_type():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.EqualityComparisonExpression(
             "artifact:payload_bin",
             {'foo': 'bar'},
         )
-    assert 'Unable to create a constant' in str(excinfo)
 
 
 def test_invalid_integer_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.IntegerConstant('foo')
-    assert 'must be an integer' in str(excinfo)
 
 
 def test_invalid_timestamp_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.TimestampConstant('foo')
-    assert 'Must be a datetime object or timestamp string' in str(excinfo)
 
 
 def test_invalid_float_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.FloatConstant('foo')
-    assert 'must be a float' in str(excinfo)
 
 
 @pytest.mark.parametrize(
@@ -400,9 +395,8 @@ def test_boolean_constant(data, result):
 
 
 def test_invalid_boolean_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.BooleanConstant('foo')
-    assert 'must be a boolean' in str(excinfo)
 
 
 @pytest.mark.parametrize(
@@ -412,21 +406,18 @@ def test_invalid_boolean_constant():
     ],
 )
 def test_invalid_hash_constant(hashtype, data):
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.HashConstant(data, hashtype)
-    assert 'is not a valid {} hash'.format(hashtype) in str(excinfo)
 
 
 def test_invalid_hex_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.HexConstant('mm')
-    assert "must contain an even number of hexadecimal characters" in str(excinfo)
 
 
 def test_invalid_binary_constant():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.BinaryConstant('foo')
-    assert 'must contain a base64' in str(excinfo)
 
 
 def test_escape_quotes_and_backslashes():
@@ -459,15 +450,13 @@ def test_repeat_qualifier():
 
 
 def test_invalid_repeat_qualifier():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.RepeatQualifier('foo')
-    assert 'is not a valid argument for a Repeat Qualifier' in str(excinfo)
 
 
 def test_invalid_within_qualifier():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.WithinQualifier('foo')
-    assert 'is not a valid argument for a Within Qualifier' in str(excinfo)
 
 
 def test_startstop_qualifier():
@@ -485,19 +474,17 @@ def test_startstop_qualifier():
 
 
 def test_invalid_startstop_qualifier():
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.StartStopQualifier(
             'foo',
             stix2.TimestampConstant('2016-06-01T00:00:00Z'),
         )
-    assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo)
 
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError):
         stix2.StartStopQualifier(
             datetime.date(2016, 6, 1),
             'foo',
         )
-    assert 'is not a valid argument for a Start/Stop Qualifier' in str(excinfo)
 
 
 def test_make_constant_already_a_constant():
diff --git a/stix2/test/v21/test_versioning.py b/stix2/test/v21/test_versioning.py
index a7f4a2f..c46183c 100644
--- a/stix2/test/v21/test_versioning.py
+++ b/stix2/test/v21/test_versioning.py
@@ -230,6 +230,7 @@ def test_remove_custom_stix_property():
         malware_types=["rootkit"],
         x_custom="armada",
         allow_custom=True,
+        is_family=False,
     )
 
     mal_nc = stix2.utils.remove_custom_stix(mal)
diff --git a/stix2/test/v21/test_workbench.py b/stix2/test/v21/test_workbench.py
index 0a976d7..0d84422 100644
--- a/stix2/test/v21/test_workbench.py
+++ b/stix2/test/v21/test_workbench.py
@@ -199,7 +199,7 @@ def test_workbench_related():
 def test_workbench_related_with_filters():
     malware = Malware(
         malware_types=["ransomware"], name="CryptorBit",
-        created_by_ref=IDENTITY_ID,
+        created_by_ref=IDENTITY_ID, is_family=False,
     )
     rel = Relationship(malware.id, 'variant-of', MALWARE_ID)
     save([malware, rel])