Repository: oasis-open/cti-python-stix2

Issue:
Granular marking constraint checking is incorrect
The STIX 2.1 spec [section 7.2.3](https://docs.oasis-open.org/cti/stix/v2.1/os/stix-v2.1-os.html#_robezi5egfdr) says that one cannot include both `lang` and `marking_ref` in a granular marking:

> Because each granular marking instance applies to either a language or a marking, one and only one of these properties MUST be present on each instance of a granular marking.

The library does not check this:

```python
import stix2

granular_marking = stix2.GranularMarking(
  lang="en",
  marking_ref=stix2.TLP_GREEN,
  selectors=["foo"]
)

granular_marking.fp_serialize(sys.stdout, indent=4)
```

results in:

```json
{
    "lang": "en",
    "marking_ref": "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da",
    "selectors": [
        "foo"
    ]
}
```

which is invalid.

Hints from issue comments:
@rpiazza: All STIX objects implemented in Python come with methods which check for constraint violations.

See https://github.com/oasis-open/cti-python-stix2/blob/master/stix2/v21/common.py#L77 for the _check_object_constraints method related to GranularMarking.

The method _check_mutually_exclusive_properties on _STIXBase can be add to _check_object_constraints to fix this bug



Solution patch:
diff --git a/stix2/v21/common.py b/stix2/v21/common.py
index 55c4a05d..ca795e02 100644
--- a/stix2/v21/common.py
+++ b/stix2/v21/common.py
@@ -77,6 +77,7 @@ class GranularMarking(_STIXBase21):
     def _check_object_constraints(self):
         super(GranularMarking, self)._check_object_constraints()
         self._check_at_least_one_property(['lang', 'marking_ref'])
+        self._check_mutually_exclusive_properties(['lang', 'marking_ref'])
 
 
 class LanguageContent(_STIXBase21):


Test patch:
diff --git a/stix2/test/v21/test_markings.py b/stix2/test/v21/test_markings.py
index d3fd11bb..0b2e950e 100644
--- a/stix2/test/v21/test_markings.py
+++ b/stix2/test/v21/test_markings.py
@@ -170,6 +170,18 @@ def test_granular_example_with_bad_selector():
     assert str(excinfo.value) == "Invalid value for GranularMarking 'selectors': must adhere to selector syntax."
 
 
+def test_granular_marking_mutual_exclusion_error():
+    with pytest.raises(stix2.exceptions.MutuallyExclusivePropertiesError) as excinfo:
+        stix2.v21.GranularMarking(
+            lang="en",
+            marking_ref=stix2.TLP_GREEN,
+            selectors=["foo"],
+        )
+    assert excinfo.value.cls == stix2.v21.GranularMarking
+    assert excinfo.value.properties == ["lang", "marking_ref"]
+    assert 'are mutually exclusive' in str(excinfo.value)
+
+
 def test_campaign_with_granular_markings_example():
     campaign = stix2.v21.Campaign(
         id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",