diff --git a/stix2/base.py b/stix2/base.py index b26f044..d7c7553 100644 --- a/stix2/base.py +++ b/stix2/base.py @@ -153,15 +153,7 @@ class _STIXBase(collections.Mapping): super(_STIXBase, self).__setattr__(name, value) def __str__(self): - properties = self.object_properties() - - def sort_by(element): - return find_property_index(self, properties, element) - - # separators kwarg -> don't include spaces after commas. - return json.dumps(self, indent=4, cls=STIXJSONEncoder, - item_sort_key=sort_by, - separators=(",", ": ")) + return self.serialize(pretty=True) def __repr__(self): props = [(k, self[k]) for k in self.object_properties() if self.get(k)] @@ -185,6 +177,38 @@ class _STIXBase(collections.Mapping): def revoke(self): return _revoke(self) + def serialize(self, pretty=False, **kwargs): + """ + Serialize a STIX object. + + Args: + pretty (bool): If True, output properties following the STIX specs + formatting. This includes indentation. Refer to notes for more + details. + **kwargs: The arguments for a json.dumps() call. + + Returns: + dict: The serialized JSON object. + + Note: + The argument ``pretty=True`` will output the STIX object following + spec order. Using this argument greatly impacts object serialization + performance. If your use case is centered across machine-to-machine + operation it is recommended to set ``pretty=False``. + + When ``pretty=True`` the following key-value pairs will be added or + overridden: indent=4, separators=(",", ": "), item_sort_key=sort_by. + """ + if pretty: + properties = self.object_properties() + + def sort_by(element): + return find_property_index(self, properties, element) + + kwargs.update({'indent': 4, 'separators': (",", ": "), 'item_sort_key': sort_by}) + + return json.dumps(self, cls=STIXJSONEncoder, **kwargs) + class _Observable(_STIXBase): diff --git a/stix2/test/test_bundle.py b/stix2/test/test_bundle.py index b1cffd0..262b050 100644 --- a/stix2/test/test_bundle.py +++ b/stix2/test/test_bundle.py @@ -1,8 +1,9 @@ +import json + import pytest import stix2 - EXPECTED_BUNDLE = """{ "type": "bundle", "id": "bundle--00000000-0000-0000-0000-000000000004", @@ -41,6 +42,44 @@ EXPECTED_BUNDLE = """{ ] }""" +EXPECTED_BUNDLE_DICT = { + "type": "bundle", + "id": "bundle--00000000-0000-0000-0000-000000000004", + "spec_version": "2.0", + "objects": [ + { + "type": "indicator", + "id": "indicator--00000000-0000-0000-0000-000000000001", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "2017-01-01T12:34:56Z", + "labels": [ + "malicious-activity" + ] + }, + { + "type": "malware", + "id": "malware--00000000-0000-0000-0000-000000000002", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware" + ] + }, + { + "type": "relationship", + "id": "relationship--00000000-0000-0000-0000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210" + } + ] +} + def test_empty_bundle(): bundle = stix2.Bundle() @@ -82,10 +121,18 @@ def test_bundle_with_wrong_spec_version(): assert str(excinfo.value) == "Invalid value for Bundle 'spec_version': must equal '2.0'." -def test_create_bundle(indicator, malware, relationship): +def test_create_bundle1(indicator, malware, relationship): bundle = stix2.Bundle(objects=[indicator, malware, relationship]) assert str(bundle) == EXPECTED_BUNDLE + assert bundle.serialize(pretty=True) == EXPECTED_BUNDLE + + +def test_create_bundle2(indicator, malware, relationship): + bundle = stix2.Bundle(objects=[indicator, malware, relationship]) + + print(repr(bundle)) + assert json.loads(bundle.serialize()) == EXPECTED_BUNDLE_DICT def test_create_bundle_with_positional_args(indicator, malware, relationship):