Repository: oasis-open/cti-python-stix2

Issue:
Create an Object Factory class
This would let users specify defaults for some common properties and easily create STIX objects without needing to specify them every single time; the objects would automatically be created with the default value.

For example, you could specify a default `created_by_ref` value to point to the Identity SDO for your organization.

Discussion:

Patch:
diff --git a/stix2/__init__.py b/stix2/__init__.py
index 904af9c..90bcf59 100644
--- a/stix2/__init__.py
+++ b/stix2/__init__.py
@@ -4,6 +4,7 @@
 
 from . import exceptions
 from .bundle import Bundle
+from .environment import ObjectFactory
 from .observables import (URL, AlternateDataStream, ArchiveExt, Artifact,
                           AutonomousSystem, Directory, DomainName,
                           EmailAddress, EmailMessage, EmailMIMEComponent, File,
diff --git a/stix2/environment.py b/stix2/environment.py
new file mode 100644
index 0000000..f855755
--- /dev/null
+++ b/stix2/environment.py
@@ -0,0 +1,68 @@
+import copy
+
+
+class ObjectFactory(object):
+    """Object Factory
+
+    Used to easily create STIX objects with default values for certain
+    properties.
+
+    Args:
+        created_by_ref: Default created_by_ref value to apply to all
+            objects created by this factory.
+        created: Default created value to apply to all
+            objects created by this factory.
+        external_references: Default `external_references` value to apply
+            to all objects created by this factory.
+        object_marking_refs: Default `object_marking_refs` value to apply
+            to all objects created by this factory.
+        list_append: When a default is set for a list property like
+            `external_references` or `object_marking_refs` and a value for
+            that property is passed into `create()`, if this is set to True,
+            that value will be added to the list alongside the default. If
+            this is set to False, the passed in value will replace the
+            default. Defaults to True.
+    """
+
+    def __init__(self, created_by_ref=None, created=None,
+                 external_references=None, object_marking_refs=None,
+                 list_append=True):
+
+        self._defaults = {}
+        if created_by_ref:
+            self._defaults['created_by_ref'] = created_by_ref
+        if created:
+            self._defaults['created'] = created
+            # If the user provides a default "created" time, we also want to use
+            # that as the modified time.
+            self._defaults['modified'] = created
+        if external_references:
+            self._defaults['external_references'] = external_references
+        if object_marking_refs:
+            self._defaults['object_marking_refs'] = object_marking_refs
+        self._list_append = list_append
+        self._list_properties = ['external_references', 'object_marking_refs']
+
+    def create(self, cls, **kwargs):
+        # Use self.defaults as the base, but update with any explicit args
+        # provided by the user.
+        properties = copy.deepcopy(self._defaults)
+        if kwargs:
+            if self._list_append:
+                # Append provided items to list properties instead of replacing them
+                for list_prop in set(self._list_properties).intersection(kwargs.keys(), properties.keys()):
+                    kwarg_prop = kwargs.pop(list_prop)
+                    if kwarg_prop is None:
+                        del properties[list_prop]
+                        continue
+                    if not isinstance(properties[list_prop], list):
+                        properties[list_prop] = [properties[list_prop]]
+
+                    if isinstance(kwarg_prop, list):
+                        properties[list_prop].extend(kwarg_prop)
+                    else:
+                        properties[list_prop].append(kwarg_prop)
+
+            properties.update(**kwargs)
+
+        return cls(**properties)
diff --git a/stix2/other.py b/stix2/other.py
index 51663b3..cd75745 100644
--- a/stix2/other.py
+++ b/stix2/other.py
@@ -69,7 +69,7 @@ class MarkingProperty(Property):
 class MarkingDefinition(_STIXBase):
     _type = 'marking-definition'
     _properties = {
-        'created': TimestampProperty(default=lambda: NOW, required=True),
+        'created': TimestampProperty(default=lambda: NOW),
         'external_references': ListProperty(ExternalReference),
         'created_by_ref': ReferenceProperty(type="identity"),
         'object_marking_refs': ListProperty(ReferenceProperty(type="marking-definition")),
diff --git a/stix2/properties.py b/stix2/properties.py
index 80e5345..71e4bd9 100644
--- a/stix2/properties.py
+++ b/stix2/properties.py
@@ -5,7 +5,7 @@ import inspect
 import re
 import uuid
 
-from six import text_type
+from six import string_types, text_type
 
 from .base import _Observable, _STIXBase
 from .exceptions import DictionaryKeyError
@@ -101,12 +101,9 @@ class ListProperty(Property):
             iter(value)
         except TypeError:
             raise ValueError("must be an iterable.")
-        try:
-            if isinstance(value, basestring):
-                value = [value]
-        except NameError:
-            if isinstance(value, str):
-                value = [value]
+
+        if isinstance(value, (_STIXBase, string_types)):
+            value = [value]
 
         result = []
         for item in value:


Test patch:
diff --git a/stix2/test/constants.py b/stix2/test/constants.py
index b631d08..958120b 100644
--- a/stix2/test/constants.py
+++ b/stix2/test/constants.py
@@ -20,6 +20,12 @@ TOOL_ID = "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f"
 SIGHTING_ID = "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb"
 VULNERABILITY_ID = "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061"
 
+# Minimum required args for an Identity instance
+IDENTITY_KWARGS = dict(
+    name="John Smith",
+    identity_class="individual",
+)
+
 # Minimum required args for an Indicator instance
 INDICATOR_KWARGS = dict(
     labels=['malicious-activity'],
diff --git a/stix2/test/test_environment.py b/stix2/test/test_environment.py
new file mode 100644
index 0000000..9be8101
--- /dev/null
+++ b/stix2/test/test_environment.py
@@ -0,0 +1,83 @@
+import stix2
+
+from .constants import (FAKE_TIME, IDENTITY_ID, IDENTITY_KWARGS,
+                        INDICATOR_KWARGS)
+
+
+def test_object_factory_created_by_ref_str():
+    factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.created_by_ref == IDENTITY_ID
+
+
+def test_object_factory_created_by_ref_obj():
+    id_obj = stix2.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS)
+    factory = stix2.ObjectFactory(created_by_ref=id_obj)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.created_by_ref == IDENTITY_ID
+
+
+def test_object_factory_override_default():
+    factory = stix2.ObjectFactory(created_by_ref=IDENTITY_ID)
+    new_id = "identity--983b3172-44fe-4a80-8091-eb8098841fe8"
+    ind = factory.create(stix2.Indicator, created_by_ref=new_id, **INDICATOR_KWARGS)
+    assert ind.created_by_ref == new_id
+
+
+def test_object_factory_created():
+    factory = stix2.ObjectFactory(created=FAKE_TIME)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.created == FAKE_TIME
+    assert ind.modified == FAKE_TIME
+
+
+def test_object_factory_external_resource():
+    ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel",
+                                      description="Threat report")
+    factory = stix2.ObjectFactory(external_references=ext_ref)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert ind.external_references[0].source_name == "ACME Threat Intel"
+    assert ind.external_references[0].description == "Threat report"
+
+    ind2 = factory.create(stix2.Indicator, external_references=None, **INDICATOR_KWARGS)
+    assert 'external_references' not in ind2
+
+
+def test_object_factory_obj_markings():
+    stmt_marking = stix2.StatementMarking("Copyright 2016, Example Corp")
+    mark_def = stix2.MarkingDefinition(definition_type="statement",
+                                       definition=stmt_marking)
+    factory = stix2.ObjectFactory(object_marking_refs=[mark_def, stix2.TLP_AMBER])
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert mark_def.id in ind.object_marking_refs
+    assert stix2.TLP_AMBER.id in ind.object_marking_refs
+
+    factory = stix2.ObjectFactory(object_marking_refs=stix2.TLP_RED)
+    ind = factory.create(stix2.Indicator, **INDICATOR_KWARGS)
+    assert stix2.TLP_RED.id in ind.object_marking_refs
+
+
+def test_object_factory_list_append():
+    ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel",
+                                      description="Threat report from ACME")
+    ext_ref2 = stix2.ExternalReference(source_name="Yet Another Threat Report",
+                                       description="Threat report from YATR")
+    ext_ref3 = stix2.ExternalReference(source_name="Threat Report #3",
+                                       description="One more threat report")
+    factory = stix2.ObjectFactory(external_references=ext_ref)
+    ind = factory.create(stix2.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS)
+    assert ind.external_references[1].source_name == "Yet Another Threat Report"
+
+    ind = factory.create(stix2.Indicator, external_references=[ext_ref2, ext_ref3], **INDICATOR_KWARGS)
+    assert ind.external_references[2].source_name == "Threat Report #3"
+
+
+def test_object_factory_list_replace():
+    ext_ref = stix2.ExternalReference(source_name="ACME Threat Intel",
+                                      description="Threat report from ACME")
+    ext_ref2 = stix2.ExternalReference(source_name="Yet Another Threat Report",
+                                       description="Threat report from YATR")
+    factory = stix2.ObjectFactory(external_references=ext_ref, list_append=False)
+    ind = factory.create(stix2.Indicator, external_references=ext_ref2, **INDICATOR_KWARGS)
+    assert len(ind.external_references) == 1
+    assert ind.external_references[0].source_name == "Yet Another Threat Report"
diff --git a/stix2/test/test_markings.py b/stix2/test/test_markings.py
index ebfa480..70d67dd 100644
--- a/stix2/test/test_markings.py
+++ b/stix2/test/test_markings.py
@@ -29,6 +29,19 @@ EXPECTED_STATEMENT_MARKING_DEFINITION = """{
     "type": "marking-definition"
 }"""
 
+EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING = """{
+    "created": "2016-04-06T20:03:00.000Z",
+    "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
+    "description": "Campaign by Green Group against a series of targets in the financial services sector.",
+    "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
+    "modified": "2016-04-06T20:03:00.000Z",
+    "name": "Green Group Attacks Against Finance",
+    "object_marking_refs": [
+        "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"
+    ],
+    "type": "campaign"
+}"""
+
 EXPECTED_GRANULAR_MARKING = """{
     "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
     "selectors": [
@@ -84,6 +97,29 @@ def test_marking_def_example_with_positional_statement():
     assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION
 
 
+def test_marking_def_invalid_type():
+    with pytest.raises(ValueError):
+        stix2.MarkingDefinition(
+            id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
+            created="2017-01-20T00:00:00.000Z",
+            definition_type="my-definiition-type",
+            definition=stix2.StatementMarking("Copyright 2016, Example Corp")
+        )
+
+
+def test_campaign_with_markings_example():
+    campaign = stix2.Campaign(
+        id="campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f",
+        created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff",
+        created="2016-04-06T20:03:00Z",
+        modified="2016-04-06T20:03:00Z",
+        name="Green Group Attacks Against Finance",
+        description="Campaign by Green Group against a series of targets in the financial services sector.",
+        object_marking_refs=TLP_WHITE
+    )
+    assert str(campaign) == EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING
+
+
 def test_granular_example():
     granular_marking = stix2.GranularMarking(
         marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
@@ -119,7 +155,6 @@ def test_campaign_with_granular_markings_example():
                 marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9",
                 selectors=["description"])
             ])
-    print(str(campaign))
     assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS