diff --git a/stix2/test/v21/conftest.py b/stix2/test/v21/conftest.py new file mode 100644 index 0000000..c73eafb --- /dev/null +++ b/stix2/test/v21/conftest.py @@ -0,0 +1,159 @@ +import uuid + +import pytest + +import stix2 + +from .constants import (FAKE_TIME, INDICATOR_KWARGS, MALWARE_KWARGS, + RELATIONSHIP_KWARGS) + + +# Inspired by: http://stackoverflow.com/a/24006251 +@pytest.fixture +def clock(monkeypatch): + + class mydatetime(stix2.utils.STIXdatetime): + @classmethod + def now(cls, tz=None): + return FAKE_TIME + + monkeypatch.setattr(stix2.utils, 'STIXdatetime', mydatetime) + + +@pytest.fixture +def uuid4(monkeypatch): + def wrapper(): + data = [0] + + def wrapped(): + data[0] += 1 + return "00000000-0000-0000-0000-00000000%04x" % data[0] + + return wrapped + monkeypatch.setattr(uuid, "uuid4", wrapper()) + + +@pytest.fixture +def indicator(uuid4, clock): + return stix2.Indicator(**INDICATOR_KWARGS) + + +@pytest.fixture +def malware(uuid4, clock): + return stix2.Malware(**MALWARE_KWARGS) + + +@pytest.fixture +def relationship(uuid4, clock): + return stix2.Relationship(**RELATIONSHIP_KWARGS) + + +@pytest.fixture +def stix_objs1(): + ind1 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + ind2 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + ind3 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.936Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + ind4 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + ind5 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + return [ind1, ind2, ind3, ind4, ind5] + + +@pytest.fixture +def stix_objs2(): + ind6 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-31T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + ind7 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + ind8 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" + } + return [ind6, ind7, ind8] + + +@pytest.fixture +def real_stix_objs2(stix_objs2): + return [stix2.parse(x) for x in stix_objs2] diff --git a/stix2/test/v21/constants.py b/stix2/test/v21/constants.py new file mode 100644 index 0000000..c49629d --- /dev/null +++ b/stix2/test/v21/constants.py @@ -0,0 +1,139 @@ +import datetime as dt + +import pytz + +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" +IDENTITY_ID = "identity--311b2d2d-f010-5473-83ec-1edf84858f4c" +INDICATOR_ID = "indicator--01234567-89ab-cdef-0123-456789abcdef" +INTRUSION_SET_ID = "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29" +LOCATION_ID = "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64" +MALWARE_ID = "malware--fedcba98-7654-3210-fedc-ba9876543210" +MARKING_DEFINITION_ID = "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" +NOTE_ID = "note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061" +OBSERVED_DATA_ID = "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf" +OPINION_ID = "opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7" +REPORT_ID = "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3" +RELATIONSHIP_ID = "relationship--00000000-1111-2222-3333-444444444444" +THREAT_ACTOR_ID = "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" +TOOL_ID = "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" +SIGHTING_ID = "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb" +VULNERABILITY_ID = "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061" + +MARKING_IDS = [ + "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "marking-definition--443eb5c3-a76c-4a0a-8caa-e93998e7bc09", + "marking-definition--57fcd772-9c1d-41b0-8d1f-3d47713415d9", + "marking-definition--462bf1a6-03d2-419c-b74e-eee2238b2de4", + "marking-definition--68520ae2-fefe-43a9-84ee-2c2a934d2c7d", + "marking-definition--2802dfb1-1019-40a8-8848-68d0ec0e417f", +] +RELATIONSHIP_IDS = [ + 'relationship--06520621-5352-4e6a-b976-e8fa3d437ffd', + 'relationship--181c9c09-43e6-45dd-9374-3bec192f05ef', + 'relationship--a0cbb21c-8daf-4a7f-96aa-7155a4ef8f70' +] + +# *_KWARGS contains all required arguments to create an instance of that STIX object +# *_MORE_KWARGS contains all the required arguments, plus some optional ones + +ATTACK_PATTERN_KWARGS = dict( + name="Phishing", +) + +CAMPAIGN_KWARGS = dict( + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", +) + +CAMPAIGN_MORE_KWARGS = dict( + type='campaign', + spec_version='2.1', + id=CAMPAIGN_ID, + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:00.000Z", + modified="2016-04-06T20:03:00.000Z", + name="Green Group Attacks Against Finance", + description="Campaign by Green Group against a series of targets in the financial services sector.", +) + +COURSE_OF_ACTION_KWARGS = dict( + name="Block", +) + +IDENTITY_KWARGS = dict( + name="John Smith", + identity_class="individual", +) + +INDICATOR_KWARGS = dict( + labels=['malicious-activity'], + pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", +) + +INTRUSION_SET_KWARGS = dict( + name="Bobcat Breakin", +) + +MALWARE_KWARGS = dict( + labels=['ransomware'], + name="Cryptolocker", + is_family=False +) + +MALWARE_MORE_KWARGS = dict( + type='malware', + id=MALWARE_ID, + created="2016-04-06T20:03:00.000Z", + modified="2016-04-06T20:03:00.000Z", + labels=['ransomware'], + name="Cryptolocker", + description="A ransomware related to ...", + is_family=False +) + +OBSERVED_DATA_KWARGS = dict( + first_observed=FAKE_TIME, + last_observed=FAKE_TIME, + number_observed=1, + objects={ + "0": { + "type": "windows-registry-key", + "key": "HKEY_LOCAL_MACHINE\\System\\Foo\\Bar", + } + } +) + +REPORT_KWARGS = dict( + labels=["campaign"], + name="Bad Cybercrime", + published=FAKE_TIME, + object_refs=[INDICATOR_ID], +) + +RELATIONSHIP_KWARGS = dict( + relationship_type="indicates", + source_ref=INDICATOR_ID, + target_ref=MALWARE_ID, +) + +SIGHTING_KWARGS = dict( + sighting_of_ref=INDICATOR_ID, +) + +THREAT_ACTOR_KWARGS = dict( + labels=["crime-syndicate"], + name="Evil Org", +) + +TOOL_KWARGS = dict( + labels=["remote-access"], + name="VNC", +) + +VULNERABILITY_KWARGS = dict( + name="Heartbleed", +) diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22.json rename to stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0a3ead4e-6d47-4ccb-854c-a6a4f9d96b22.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b.json rename to stix2/test/v21/stix2_data/attack-pattern/attack-pattern--0f20e3cb-245b-4a61-8a91-2d93f7cb0e9b.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9.json rename to stix2/test/v21/stix2_data/attack-pattern/attack-pattern--774a3188-6ba9-4dc4-879d-d54ee48a5ce9.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475.json rename to stix2/test/v21/stix2_data/attack-pattern/attack-pattern--7e150503-88e7-4861-866b-ff1ac82c4475.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c.json rename to stix2/test/v21/stix2_data/attack-pattern/attack-pattern--ae676644-d2d2-41b7-af7e-9bed1b55898c.json diff --git a/stix2/test/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a.json b/stix2/test/v21/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a.json rename to stix2/test/v21/stix2_data/attack-pattern/attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a.json diff --git a/stix2/test/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f.json b/stix2/test/v21/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f.json rename to stix2/test/v21/stix2_data/course-of-action/course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f.json diff --git a/stix2/test/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd.json b/stix2/test/v21/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd.json rename to stix2/test/v21/stix2_data/course-of-action/course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd.json diff --git a/stix2/test/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5.json b/stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5.json rename to stix2/test/v21/stix2_data/identity/identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5.json diff --git a/stix2/test/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064.json b/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064.json rename to stix2/test/v21/stix2_data/intrusion-set/intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064.json diff --git a/stix2/test/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a.json b/stix2/test/v21/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a.json rename to stix2/test/v21/stix2_data/intrusion-set/intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a.json diff --git a/stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json b/stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json rename to stix2/test/v21/stix2_data/malware/malware--6b616fc1-1505-48e3-8b2c-0d19337bff38.json diff --git a/stix2/test/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841.json b/stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841.json rename to stix2/test/v21/stix2_data/malware/malware--92ec0cbd-2c30-44a2-b270-73f4ec949841.json diff --git a/stix2/test/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e.json b/stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e.json rename to stix2/test/v21/stix2_data/malware/malware--96b08451-b27a-4ff6-893f-790e26393a8e.json diff --git a/stix2/test/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b.json b/stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b.json rename to stix2/test/v21/stix2_data/malware/malware--b42378e0-f147-496f-992a-26a49705395b.json diff --git a/stix2/test/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json b/stix2/test/v21/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json rename to stix2/test/v21/stix2_data/marking-definition/marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168.json diff --git a/stix2/test/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883.json b/stix2/test/v21/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883.json rename to stix2/test/v21/stix2_data/relationship/relationship--0d4a7788-7f3b-4df8-a498-31a38003c883.json diff --git a/stix2/test/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227.json b/stix2/test/v21/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227.json rename to stix2/test/v21/stix2_data/relationship/relationship--0e55ee98-0c6d-43d4-b424-b18a0036b227.json diff --git a/stix2/test/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432.json b/stix2/test/v21/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432.json rename to stix2/test/v21/stix2_data/relationship/relationship--1e91cd45-a725-4965-abe3-700694374432.json diff --git a/stix2/test/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e.json b/stix2/test/v21/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e.json rename to stix2/test/v21/stix2_data/relationship/relationship--3a3084f9-0302-4fd5-9b8a-e0db10f5345e.json diff --git a/stix2/test/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1.json b/stix2/test/v21/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1.json rename to stix2/test/v21/stix2_data/relationship/relationship--3a3ed0b2-0c38-441f-ac40-53b873e545d1.json diff --git a/stix2/test/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719.json b/stix2/test/v21/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719.json rename to stix2/test/v21/stix2_data/relationship/relationship--592d0c31-e61f-495e-a60e-70d7be59a719.json diff --git a/stix2/test/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1.json b/stix2/test/v21/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1.json rename to stix2/test/v21/stix2_data/relationship/relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1.json diff --git a/stix2/test/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d.json b/stix2/test/v21/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d.json rename to stix2/test/v21/stix2_data/relationship/relationship--8797579b-e3be-4209-a71b-255a4d08243d.json diff --git a/stix2/test/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23.json b/stix2/test/v21/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23.json rename to stix2/test/v21/stix2_data/tool/tool--03342581-f790-4f03-ba41-e82e67392e23.json diff --git a/stix2/test/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966.json b/stix2/test/v21/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966.json old mode 100755 new mode 100644 similarity index 100% rename from stix2/test/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966.json rename to stix2/test/v21/stix2_data/tool/tool--242f3da3-4425-4d11-8f5c-b842886da966.json diff --git a/stix2/test/v21/test_attack_pattern.py b/stix2/test/v21/test_attack_pattern.py new file mode 100644 index 0000000..62c2d81 --- /dev/null +++ b/stix2/test/v21/test_attack_pattern.py @@ -0,0 +1,83 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import ATTACK_PATTERN_ID + +EXPECTED = """{ + "type": "attack-pattern", + "spec_version": "2.1", + "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "name": "Spear Phishing", + "description": "...", + "external_references": [ + { + "source_name": "capec", + "external_id": "CAPEC-163" + } + ] +}""" + + +def test_attack_pattern_example(): + ap = stix2.AttackPattern( + id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + created="2016-05-12T08:17:27.000Z", + modified="2016-05-12T08:17:27.000Z", + name="Spear Phishing", + external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-163" + }], + description="...", + ) + + assert str(ap) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "type": "attack-pattern", + "id": "attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "description": "...", + "external_references": [ + { + "external_id": "CAPEC-163", + "source_name": "capec" + } + ], + "name": "Spear Phishing", + }, +]) +def test_parse_attack_pattern(data): + ap = stix2.parse(data) + + assert ap.type == 'attack-pattern' + assert ap.id == ATTACK_PATTERN_ID + assert ap.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert ap.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert ap.description == "..." + assert ap.external_references[0].external_id == 'CAPEC-163' + assert ap.external_references[0].source_name == 'capec' + assert ap.name == "Spear Phishing" + + +def test_attack_pattern_invalid_labels(): + with pytest.raises(stix2.exceptions.InvalidValueError): + stix2.AttackPattern( + id="attack-pattern--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + created="2016-05-12T08:17:27Z", + modified="2016-05-12T08:17:27Z", + name="Spear Phishing", + labels=1 + ) + +# TODO: Add other examples diff --git a/stix2/test/v21/test_base.py b/stix2/test/v21/test_base.py new file mode 100644 index 0000000..18d3a50 --- /dev/null +++ b/stix2/test/v21/test_base.py @@ -0,0 +1,25 @@ +import datetime as dt +import json + +import pytest +import pytz + +from stix2.base import STIXJSONEncoder + + +def test_encode_json_datetime(): + now = dt.datetime(2017, 3, 22, 0, 0, 0, tzinfo=pytz.UTC) + test_dict = {'now': now} + + expected = '{"now": "2017-03-22T00:00:00Z"}' + assert json.dumps(test_dict, cls=STIXJSONEncoder) == expected + + +def test_encode_json_object(): + obj = object() + test_dict = {'obj': obj} + + with pytest.raises(TypeError) as excinfo: + json.dumps(test_dict, cls=STIXJSONEncoder) + + assert " is not JSON serializable" in str(excinfo.value) diff --git a/stix2/test/v21/test_bundle.py b/stix2/test/v21/test_bundle.py new file mode 100644 index 0000000..7694389 --- /dev/null +++ b/stix2/test/v21/test_bundle.py @@ -0,0 +1,208 @@ +import json + +import pytest + +import stix2 + +EXPECTED_BUNDLE = """{ + "type": "bundle", + "id": "bundle--00000000-0000-0000-0000-000000000007", + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "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", + "spec_version": "2.1", + "id": "malware--00000000-0000-0000-0000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware" + ], + "is_family": false + }, + { + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--00000000-0000-0000-0000-000000000005", + "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" + } + ] +}""" + +EXPECTED_BUNDLE_DICT = { + "type": "bundle", + "id": "bundle--00000000-0000-0000-0000-000000000007", + "objects": [ + { + "type": "indicator", + "spec_version": "2.1", + "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", + "spec_version": "2.1", + "id": "malware--00000000-0000-0000-0000-000000000003", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware" + ], + "is_family": False + }, + { + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--00000000-0000-0000-0000-000000000005", + "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() + + assert bundle.type == "bundle" + assert bundle.id.startswith("bundle--") + with pytest.raises(AttributeError): + assert bundle.objects + + +def test_bundle_with_wrong_type(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Bundle(type="not-a-bundle") + + assert excinfo.value.cls == stix2.Bundle + assert excinfo.value.prop_name == "type" + assert excinfo.value.reason == "must equal 'bundle'." + assert str(excinfo.value) == "Invalid value for Bundle 'type': must equal 'bundle'." + + +def test_bundle_id_must_start_with_bundle(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Bundle(id='my-prefix--') + + assert excinfo.value.cls == stix2.Bundle + assert excinfo.value.prop_name == "id" + assert excinfo.value.reason == "must start with 'bundle--'." + assert str(excinfo.value) == "Invalid value for Bundle 'id': must start with 'bundle--'." + + +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]) + + assert json.loads(bundle.serialize()) == EXPECTED_BUNDLE_DICT + + +def test_create_bundle_with_positional_args(indicator, malware, relationship): + bundle = stix2.Bundle(indicator, malware, relationship) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_positional_listarg(indicator, malware, relationship): + bundle = stix2.Bundle([indicator, malware, relationship]) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_listarg_and_positional_arg(indicator, malware, relationship): + bundle = stix2.Bundle([indicator, malware], relationship) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_listarg_and_kwarg(indicator, malware, relationship): + bundle = stix2.Bundle([indicator, malware], objects=[relationship]) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_with_arg_listarg_and_kwarg(indicator, malware, relationship): + bundle = stix2.Bundle([indicator], malware, objects=[relationship]) + + assert str(bundle) == EXPECTED_BUNDLE + + +def test_create_bundle_invalid(indicator, malware, relationship): + with pytest.raises(ValueError) as excinfo: + stix2.Bundle(objects=[1]) + assert excinfo.value.reason == "This property may only contain a dictionary or object" + + with pytest.raises(ValueError) as excinfo: + stix2.Bundle(objects=[{}]) + assert excinfo.value.reason == "This property may only contain a non-empty dictionary or object" + + with pytest.raises(ValueError) as excinfo: + stix2.Bundle(objects=[{'type': 'bundle'}]) + assert excinfo.value.reason == 'This property may not contain a Bundle object' + + +@pytest.mark.parametrize("version", ["2.1"]) +def test_parse_bundle(version): + bundle = stix2.parse(EXPECTED_BUNDLE, version=version) + + assert bundle.type == "bundle" + assert bundle.id.startswith("bundle--") + assert type(bundle.objects[0]) is stix2.v21.sdo.Indicator + assert bundle.objects[0].type == 'indicator' + assert bundle.objects[1].type == 'malware' + assert bundle.objects[2].type == 'relationship' + + +def test_parse_unknown_type(): + unknown = { + "type": "other", + "id": "other--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created": "2016-04-06T20:03:00Z", + "modified": "2016-04-06T20:03:00Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "name": "Green Group Attacks Against Finance", + } + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse(unknown) + assert str(excinfo.value) == "Can't parse unknown object type 'other'! For custom types, use the CustomObject decorator." + + +def test_stix_object_property(): + prop = stix2.v21.bundle.STIXObjectProperty() + + identity = stix2.Identity(name="test", identity_class="individual") + assert prop.clean(identity) is identity diff --git a/stix2/test/v21/test_campaign.py b/stix2/test/v21/test_campaign.py new file mode 100644 index 0000000..334863c --- /dev/null +++ b/stix2/test/v21/test_campaign.py @@ -0,0 +1,59 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import CAMPAIGN_ID + +EXPECTED = """{ + "type": "campaign", + "spec_version": "2.1", + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "name": "Green Group Attacks Against Finance", + "description": "Campaign by Green Group against a series of targets in the financial services sector." +}""" + + +def test_campaign_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." + ) + + assert str(campaign) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "type": "campaign", + "spec_version": "2.1", + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created": "2016-04-06T20:03:00Z", + "modified": "2016-04-06T20:03:00Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "name": "Green Group Attacks Against Finance", + }, +]) +def test_parse_campaign(data): + cmpn = stix2.parse(data) + + assert cmpn.type == 'campaign' + assert cmpn.id == CAMPAIGN_ID + assert cmpn.created == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert cmpn.modified == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert cmpn.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert cmpn.description == "Campaign by Green Group against a series of targets in the financial services sector." + assert cmpn.name == "Green Group Attacks Against Finance" + +# TODO: Add other examples diff --git a/stix2/test/test_confidence.py b/stix2/test/v21/test_confidence.py similarity index 100% rename from stix2/test/test_confidence.py rename to stix2/test/v21/test_confidence.py diff --git a/stix2/test/v21/test_course_of_action.py b/stix2/test/v21/test_course_of_action.py new file mode 100644 index 0000000..847e47c --- /dev/null +++ b/stix2/test/v21/test_course_of_action.py @@ -0,0 +1,59 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import COURSE_OF_ACTION_ID + +EXPECTED = """{ + "type": "course-of-action", + "spec_version": "2.1", + "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": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", + "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." +}""" + + +def test_course_of_action_example(): + coa = stix2.CourseOfAction( + 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="Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", + description="This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." + ) + + assert str(coa) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ...", + "id": "course-of-action--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter", + "spec_version": "2.1", + "type": "course-of-action" + }, +]) +def test_parse_course_of_action(data): + coa = stix2.parse(data) + + assert coa.type == 'course-of-action' + assert coa.id == COURSE_OF_ACTION_ID + assert coa.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert coa.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert coa.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert coa.description == "This is how to add a filter rule to block inbound access to TCP port 80 to the existing UDP 1434 filter ..." + assert coa.name == "Add TCP port 80 Filter Rule to the existing Block UDP 1434 Filter" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_custom.py b/stix2/test/v21/test_custom.py new file mode 100644 index 0000000..df3edbc --- /dev/null +++ b/stix2/test/v21/test_custom.py @@ -0,0 +1,894 @@ +import pytest + +import stix2 +import stix2.base +import stix2.v21.sdo + +from .constants import FAKE_TIME, MARKING_DEFINITION_ID + +IDENTITY_CUSTOM_PROP = stix2.Identity( + name="John Smith", + identity_class="individual", + x_foo="bar", + allow_custom=True, +) + + +def test_identity_custom_property(): + with pytest.raises(ValueError) as excinfo: + stix2.Identity( + id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties="foobar", + ) + assert str(excinfo.value) == "'custom_properties' must be a dictionary" + + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.Identity( + id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "foo": "bar", + }, + foo="bar", + ) + assert "Unexpected properties for Identity" in str(excinfo.value) + + identity = stix2.Identity( + id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + custom_properties={ + "foo": "bar", + }, + ) + assert identity.foo == "bar" + + +def test_identity_custom_property_invalid(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.Identity( + id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + x_foo="bar", + ) + assert excinfo.value.cls == stix2.Identity + assert excinfo.value.properties == ['x_foo'] + assert "Unexpected properties for" in str(excinfo.value) + + +def test_identity_custom_property_allowed(): + identity = stix2.Identity( + id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11Z", + modified="2015-12-21T19:59:11Z", + name="John Smith", + identity_class="individual", + x_foo="bar", + allow_custom=True, + ) + assert identity.x_foo == "bar" + + +@pytest.mark.parametrize("data", [ + """{ + "type": "identity", + "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + "created": "2015-12-21T19:59:11Z", + "modified": "2015-12-21T19:59:11Z", + "name": "John Smith", + "identity_class": "individual", + "foo": "bar" + }""", +]) +def test_parse_identity_custom_property(data): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + identity = stix2.parse(data) + assert excinfo.value.cls == stix2.v21.sdo.Identity + assert excinfo.value.properties == ['foo'] + assert "Unexpected properties for" in str(excinfo.value) + + identity = stix2.parse(data, allow_custom=True) + assert identity.foo == "bar" + + +def test_custom_property_object_in_bundled_object(): + bundle = stix2.Bundle(IDENTITY_CUSTOM_PROP, allow_custom=True) + + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_properties_object_in_bundled_object(): + obj = stix2.Identity( + name="John Smith", + identity_class="individual", + custom_properties={ + "x_foo": "bar", + } + ) + bundle = stix2.Bundle(obj, allow_custom=True) + + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_property_dict_in_bundled_object(): + custom_identity = { + 'type': 'identity', + 'id': 'identity--311b2d2d-f010-5473-83ec-1edf84858f4c', + 'created': '2015-12-21T19:59:11Z', + 'name': 'John Smith', + 'identity_class': 'individual', + 'x_foo': 'bar', + } + with pytest.raises(stix2.exceptions.ExtraPropertiesError): + bundle = stix2.Bundle(custom_identity) + + bundle = stix2.Bundle(custom_identity, allow_custom=True) + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_properties_dict_in_bundled_object(): + custom_identity = { + 'type': 'identity', + 'id': 'identity--311b2d2d-f010-5473-83ec-1edf84858f4c', + 'created': '2015-12-21T19:59:11Z', + 'name': 'John Smith', + 'identity_class': 'individual', + 'custom_properties': { + 'x_foo': 'bar', + }, + } + bundle = stix2.Bundle(custom_identity) + + assert bundle.objects[0].x_foo == "bar" + assert '"x_foo": "bar"' in str(bundle) + + +def test_custom_property_in_observed_data(): + artifact = stix2.File( + allow_custom=True, + name='test', + x_foo='bar' + ) + observed_data = stix2.ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=0, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_custom_property_object_in_observable_extension(): + ntfs = stix2.NTFSExt( + allow_custom=True, + sid=1, + x_foo='bar', + ) + artifact = stix2.File( + name='test', + extensions={'ntfs-ext': ntfs}, + ) + observed_data = stix2.ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=0, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_custom_property_dict_in_observable_extension(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError): + artifact = stix2.File( + name='test', + extensions={ + 'ntfs-ext': { + 'sid': 1, + 'x_foo': 'bar', + } + }, + ) + + artifact = stix2.File( + allow_custom=True, + name='test', + extensions={ + 'ntfs-ext': { + 'allow_custom': True, + 'sid': 1, + 'x_foo': 'bar', + } + }, + ) + observed_data = stix2.ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=0, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_identity_custom_property_revoke(): + identity = IDENTITY_CUSTOM_PROP.revoke() + assert identity.x_foo == "bar" + + +def test_identity_custom_property_edit_markings(): + marking_obj = stix2.MarkingDefinition( + id=MARKING_DEFINITION_ID, + definition_type="statement", + definition=stix2.StatementMarking(statement="Copyright 2016, Example Corp") + ) + marking_obj2 = stix2.MarkingDefinition( + id=MARKING_DEFINITION_ID, + definition_type="statement", + definition=stix2.StatementMarking(statement="Another one") + ) + + # None of the following should throw exceptions + identity = IDENTITY_CUSTOM_PROP.add_markings(marking_obj) + identity2 = identity.add_markings(marking_obj2, ['x_foo']) + identity2.remove_markings(marking_obj.id) + identity2.remove_markings(marking_obj2.id, ['x_foo']) + identity2.clear_markings() + identity2.clear_markings('x_foo') + + +def test_custom_marking_no_init_1(): + @stix2.CustomMarking('x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewObj(): + pass + + no = NewObj(property1='something') + assert no.property1 == 'something' + + +def test_custom_marking_no_init_2(): + @stix2.CustomMarking('x-new-obj2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewObj2(object): + pass + + no2 = NewObj2(property1='something') + assert no2.property1 == 'something' + + +@stix2.sdo.CustomObject('x-new-type', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), +]) +class NewType(object): + def __init__(self, property2=None, **kwargs): + if property2 and property2 < 10: + raise ValueError("'property2' is too small.") + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_custom_object_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewType(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_custom_object_type(): + nt = NewType(property1='something') + assert nt.property1 == 'something' + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + NewType(property2=42) + assert "No values for required properties" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + NewType(property1='something', property2=4) + assert "'property2' is too small." in str(excinfo.value) + + +def test_custom_object_no_init_1(): + @stix2.sdo.CustomObject('x-new-obj', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewObj(): + pass + + no = NewObj(property1='something') + assert no.property1 == 'something' + + +def test_custom_object_no_init_2(): + @stix2.sdo.CustomObject('x-new-obj2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewObj2(object): + pass + + no2 = NewObj2(property1='something') + assert no2.property1 == 'something' + + +def test_custom_object_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.sdo.CustomObject('x', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewObj(object): + pass # pragma: no cover + assert "Invalid type name 'x': " in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.sdo.CustomObject('x_new_object', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewObj2(object): + pass # pragma: no cover + assert "Invalid type name 'x_new_object':" in str(excinfo.value) + + +def test_parse_custom_object_type(): + nt_string = """{ + "type": "x-new-type", + "created": "2015-12-21T19:59:11Z", + "property1": "something" + }""" + + nt = stix2.parse(nt_string, allow_custom=True) + assert nt["property1"] == 'something' + + +def test_parse_unregistered_custom_object_type(): + nt_string = """{ + "type": "x-foobar-observable", + "created": "2015-12-21T19:59:11Z", + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse(nt_string) + assert "Can't parse unknown object type" in str(excinfo.value) + assert "use the CustomObject decorator." in str(excinfo.value) + + +def test_parse_unregistered_custom_object_type_w_allow_custom(): + """parse an unknown custom object, allowed by passing + 'allow_custom' flag + """ + nt_string = """{ + "type": "x-foobar-observable", + "created": "2015-12-21T19:59:11Z", + "property1": "something" + }""" + + custom_obj = stix2.parse(nt_string, allow_custom=True) + assert custom_obj["type"] == "x-foobar-observable" + + +@stix2.observables.CustomObservable('x-new-observable', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ('x_property3', stix2.properties.BooleanProperty()), +]) +class NewObservable(): + def __init__(self, property2=None, **kwargs): + if property2 and property2 < 10: + raise ValueError("'property2' is too small.") + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_custom_observable_object_1(): + no = NewObservable(property1='something') + assert no.property1 == 'something' + + +def test_custom_observable_object_2(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + NewObservable(property2=42) + assert excinfo.value.properties == ['property1'] + assert "No values for required properties" in str(excinfo.value) + + +def test_custom_observable_object_3(): + with pytest.raises(ValueError) as excinfo: + NewObservable(property1='something', property2=4) + assert "'property2' is too small." in str(excinfo.value) + + +def test_custom_observable_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewObservable(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_custom_observable_object_no_init_1(): + @stix2.observables.CustomObservable('x-new-observable', [ + ('property1', stix2.properties.StringProperty()), + ]) + class NewObs(): + pass + + no = NewObs(property1='something') + assert no.property1 == 'something' + + +def test_custom_observable_object_no_init_2(): + @stix2.observables.CustomObservable('x-new-obs2', [ + ('property1', stix2.properties.StringProperty()), + ]) + class NewObs2(object): + pass + + no2 = NewObs2(property1='something') + assert no2.property1 == 'something' + + +def test_custom_observable_object_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomObservable('x', [ + ('property1', stix2.properties.StringProperty()), + ]) + class NewObs(object): + pass # pragma: no cover + assert "Invalid observable type name 'x':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomObservable('x_new_obs', [ + ('property1', stix2.properties.StringProperty()), + ]) + class NewObs2(object): + pass # pragma: no cover + assert "Invalid observable type name 'x_new_obs':" in str(excinfo.value) + + +def test_custom_observable_object_invalid_ref_property(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomObservable('x-new-obs', [ + ('property_ref', stix2.properties.StringProperty()), + ]) + class NewObs(): + pass + assert "is named like an object reference property but is not an ObjectReferenceProperty" in str(excinfo.value) + + +def test_custom_observable_object_invalid_refs_property(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomObservable('x-new-obs', [ + ('property_refs', stix2.properties.StringProperty()), + ]) + class NewObs(): + pass + assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) + + +def test_custom_observable_object_invalid_refs_list_property(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomObservable('x-new-obs', [ + ('property_refs', stix2.properties.ListProperty(stix2.properties.StringProperty)), + ]) + class NewObs(): + pass + assert "is named like an object reference list property but is not a ListProperty containing ObjectReferenceProperty" in str(excinfo.value) + + +def test_custom_observable_object_invalid_valid_refs(): + @stix2.observables.CustomObservable('x-new-obs', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property_ref', stix2.properties.ObjectReferenceProperty(valid_types='email-addr')), + ]) + class NewObs(): + pass + + with pytest.raises(Exception) as excinfo: + NewObs(_valid_refs=['1'], + property1='something', + property_ref='1') + assert "must be created with _valid_refs as a dict, not a list" in str(excinfo.value) + + +def test_custom_no_properties_raises_exception(): + with pytest.raises(ValueError): + + @stix2.sdo.CustomObject('x-new-object-type') + class NewObject1(object): + pass + + +def test_custom_wrong_properties_arg_raises_exception(): + with pytest.raises(ValueError): + + @stix2.observables.CustomObservable('x-new-object-type', (("prop", stix2.properties.BooleanProperty()))) + class NewObject2(object): + pass + + +def test_parse_custom_observable_object(): + nt_string = """{ + "type": "x-new-observable", + "property1": "something" + }""" + + nt = stix2.parse_observable(nt_string, []) + assert isinstance(nt, stix2.base._STIXBase) + assert nt.property1 == 'something' + + +def test_parse_unregistered_custom_observable_object(): + nt_string = """{ + "type": "x-foobar-observable", + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.CustomContentError) as excinfo: + stix2.parse_observable(nt_string) + assert "Can't parse unknown observable type" in str(excinfo.value) + + parsed_custom = stix2.parse_observable(nt_string, allow_custom=True) + assert parsed_custom['property1'] == 'something' + with pytest.raises(AttributeError) as excinfo: + assert parsed_custom.property1 == 'something' + assert not isinstance(parsed_custom, stix2.base._STIXBase) + + +def test_parse_unregistered_custom_observable_object_with_no_type(): + nt_string = """{ + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse_observable(nt_string, allow_custom=True) + assert "Can't parse observable with no 'type' property" in str(excinfo.value) + + +def test_parse_observed_data_with_custom_observable(): + input_str = """{ + "type": "observed-data", + "id": "observed-data--dc20c4ca-a2a3-4090-a5d5-9558c3af4758", + "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", + "number_observed": 1, + "objects": { + "0": { + "type": "x-foobar-observable", + "property1": "something" + } + } + }""" + parsed = stix2.parse(input_str, allow_custom=True) + assert parsed.objects['0']['property1'] == 'something' + + +def test_parse_invalid_custom_observable_object(): + nt_string = """{ + "property1": "something" + }""" + + with pytest.raises(stix2.exceptions.ParseError) as excinfo: + stix2.parse_observable(nt_string) + assert "Can't parse observable with no 'type' property" in str(excinfo.value) + + +def test_observable_custom_property(): + with pytest.raises(ValueError) as excinfo: + NewObservable( + property1='something', + custom_properties="foobar", + ) + assert "'custom_properties' must be a dictionary" in str(excinfo.value) + + no = NewObservable( + property1='something', + custom_properties={ + "foo": "bar", + }, + ) + assert no.foo == "bar" + + +def test_observable_custom_property_invalid(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + NewObservable( + property1='something', + x_foo="bar", + ) + assert excinfo.value.properties == ['x_foo'] + assert "Unexpected properties for" in str(excinfo.value) + + +def test_observable_custom_property_allowed(): + no = NewObservable( + property1='something', + x_foo="bar", + allow_custom=True, + ) + assert no.x_foo == "bar" + + +def test_observed_data_with_custom_observable_object(): + no = NewObservable(property1='something') + ob_data = stix2.ObservedData( + first_observed=FAKE_TIME, + last_observed=FAKE_TIME, + number_observed=1, + objects={'0': no}, + allow_custom=True, + ) + assert ob_data.objects['0'].property1 == 'something' + + +@stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), +]) +class NewExtension(): + def __init__(self, property2=None, **kwargs): + if property2 and property2 < 10: + raise ValueError("'property2' is too small.") + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_custom_extension_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewExtension(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_custom_extension(): + ext = NewExtension(property1='something') + assert ext.property1 == 'something' + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + NewExtension(property2=42) + assert excinfo.value.properties == ['property1'] + assert str(excinfo.value) == "No values for required properties for _Custom: (property1)." + + with pytest.raises(ValueError) as excinfo: + NewExtension(property1='something', property2=4) + assert str(excinfo.value) == "'property2' is too small." + + +def test_custom_extension_wrong_observable_type(): + # NewExtension is an extension of DomainName, not File + ext = NewExtension(property1='something') + with pytest.raises(ValueError) as excinfo: + stix2.File(name="abc.txt", + extensions={ + "ntfs-ext": ext, + }) + + assert 'Cannot determine extension type' in excinfo.value.reason + + +@pytest.mark.parametrize("data", [ + """{ + "keys": [ + { + "test123": 123, + "test345": "aaaa" + } + ] +}""", +]) +def test_custom_extension_with_list_and_dict_properties_observable_type(data): + @stix2.observables.CustomExtension(stix2.UserAccount, 'some-extension', [ + ('keys', stix2.properties.ListProperty(stix2.properties.DictionaryProperty, required=True)) + ]) + class SomeCustomExtension: + pass + + example = SomeCustomExtension(keys=[{'test123': 123, 'test345': 'aaaa'}]) + assert data == str(example) + + +def test_custom_extension_invalid_observable(): + # These extensions are being applied to improperly-created Observables. + # The Observable classes should have been created with the CustomObservable decorator. + class Foo(object): + pass + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(Foo, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class FooExtension(): + pass # pragma: no cover + assert str(excinfo.value) == "'observable' must be a valid Observable class!" + + class Bar(stix2.observables._Observable): + pass + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(Bar, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class BarExtension(): + pass + assert "Unknown observable type" in str(excinfo.value) + assert "Custom observables must be created with the @CustomObservable decorator." in str(excinfo.value) + + class Baz(stix2.observables._Observable): + _type = 'Baz' + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(Baz, 'x-new-ext', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class BazExtension(): + pass + assert "Unknown observable type" in str(excinfo.value) + assert "Custom observables must be created with the @CustomObservable decorator." in str(excinfo.value) + + +def test_custom_extension_invalid_type_name(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(stix2.File, 'x', { + 'property1': stix2.properties.StringProperty(required=True), + }) + class FooExtension(): + pass # pragma: no cover + assert "Invalid extension type name 'x':" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(stix2.File, 'x_new_ext', { + 'property1': stix2.properties.StringProperty(required=True), + }) + class BlaExtension(): + pass # pragma: no cover + assert "Invalid extension type name 'x_new_ext':" in str(excinfo.value) + + +def test_custom_extension_no_properties(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', None) + class BarExtension(): + pass + assert "Must supply a list, containing tuples." in str(excinfo.value) + + +def test_custom_extension_empty_properties(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', []) + class BarExtension(): + pass + assert "Must supply a list, containing tuples." in str(excinfo.value) + + +def test_custom_extension_dict_properties(): + with pytest.raises(ValueError) as excinfo: + @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', {}) + class BarExtension(): + pass + assert "Must supply a list, containing tuples." in str(excinfo.value) + + +def test_custom_extension_no_init_1(): + @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-extension', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewExt(): + pass + + ne = NewExt(property1="foobar") + assert ne.property1 == "foobar" + + +def test_custom_extension_no_init_2(): + @stix2.observables.CustomExtension(stix2.DomainName, 'x-new-ext2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ]) + class NewExt2(object): + pass + + ne2 = NewExt2(property1="foobar") + assert ne2.property1 == "foobar" + + +def test_parse_observable_with_custom_extension(): + input_str = """{ + "type": "domain-name", + "value": "example.com", + "extensions": { + "x-new-ext": { + "property1": "foo", + "property2": 12 + } + } + }""" + + parsed = stix2.parse_observable(input_str) + assert parsed.extensions['x-new-ext'].property2 == 12 + + +def test_parse_observable_with_unregistered_custom_extension(): + input_str = """{ + "type": "domain-name", + "value": "example.com", + "extensions": { + "x-foobar-ext": { + "property1": "foo", + "property2": 12 + } + } + }""" + + with pytest.raises(ValueError) as excinfo: + stix2.parse_observable(input_str) + assert "Can't parse unknown extension type" in str(excinfo.value) + + parsed_ob = stix2.parse_observable(input_str, allow_custom=True) + assert parsed_ob['extensions']['x-foobar-ext']['property1'] == 'foo' + assert not isinstance(parsed_ob['extensions']['x-foobar-ext'], stix2.base._STIXBase) + + +def test_register_custom_object(): + # Not the way to register custom object. + class CustomObject2(object): + _type = 'awesome-object' + + stix2._register_type(CustomObject2) + # Note that we will always check against newest OBJ_MAP. + assert (CustomObject2._type, CustomObject2) in stix2.OBJ_MAP.items() + + +def test_extension_property_location(): + assert 'extensions' in stix2.v21.observables.OBJ_MAP_OBSERVABLE['x-new-observable']._properties + assert 'extensions' not in stix2.v21.observables.EXT_MAP['domain-name']['x-new-ext']._properties + + +@pytest.mark.parametrize("data", [ + """{ + "type": "x-example", + "id": "x-example--336d8a9f-91f1-46c5-b142-6441bb9f8b8d", + "created": "2018-06-12T16:20:58.059Z", + "modified": "2018-06-12T16:20:58.059Z", + "dictionary": { + "key": { + "key_a": "value", + "key_b": "value" + } + } +}""", +]) +def test_custom_object_nested_dictionary(data): + @stix2.sdo.CustomObject('x-example', [ + ('dictionary', stix2.properties.DictionaryProperty()), + ]) + class Example(object): + def __init__(self, **kwargs): + pass + + example = Example(id='x-example--336d8a9f-91f1-46c5-b142-6441bb9f8b8d', + created='2018-06-12T16:20:58.059Z', + modified='2018-06-12T16:20:58.059Z', + dictionary={'key': {'key_b': 'value', 'key_a': 'value'}}) + + assert data == str(example) diff --git a/stix2/test/v21/test_datastore.py b/stix2/test/v21/test_datastore.py new file mode 100644 index 0000000..323365a --- /dev/null +++ b/stix2/test/v21/test_datastore.py @@ -0,0 +1,117 @@ +import pytest + +from stix2.datastore import (CompositeDataSource, DataSink, DataSource, + DataStoreMixin) +from stix2.datastore.filters import Filter +from stix2.test.constants import CAMPAIGN_MORE_KWARGS + + +def test_datasource_abstract_class_raises_error(): + with pytest.raises(TypeError): + DataSource() + + +def test_datasink_abstract_class_raises_error(): + with pytest.raises(TypeError): + DataSink() + + +def test_datastore_smoke(): + assert DataStoreMixin() is not None + + +def test_datastore_get_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().get("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_all_versions_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().all_versions("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_query_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().query([Filter("type", "=", "indicator")]) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_creator_of_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().creator_of(CAMPAIGN_MORE_KWARGS) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_relationships_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().relationships(obj="indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + target_only=True) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_related_to_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().related_to(obj="indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + target_only=True) + assert "DataStoreMixin has no data source to query" == str(excinfo.value) + + +def test_datastore_add_raises(): + with pytest.raises(AttributeError) as excinfo: + DataStoreMixin().add(CAMPAIGN_MORE_KWARGS) + assert "DataStoreMixin has no data sink to put objects in" == str(excinfo.value) + + +def test_composite_datastore_get_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().get("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_all_versions_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().all_versions("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_query_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().query([Filter("type", "=", "indicator")]) + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_relationships_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().relationships(obj="indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + target_only=True) + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_related_to_raises_error(): + with pytest.raises(AttributeError) as excinfo: + CompositeDataSource().related_to(obj="indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + target_only=True) + assert "CompositeDataSource has no data sources" == str(excinfo.value) + + +def test_composite_datastore_add_data_source_raises_error(): + with pytest.raises(TypeError) as excinfo: + ind = "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f" + CompositeDataSource().add_data_source(ind) + assert "DataSource (to be added) is not of type stix2.DataSource. DataSource type is '{}'".format(type(ind)) == str(excinfo.value) + + +def test_composite_datastore_add_data_sources_raises_error(): + with pytest.raises(TypeError) as excinfo: + ind = "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f" + CompositeDataSource().add_data_sources(ind) + assert "DataSource (to be added) is not of type stix2.DataSource. DataSource type is '{}'".format(type(ind)) == str(excinfo.value) + + +def test_composite_datastore_no_datasource(): + cds = CompositeDataSource() + with pytest.raises(AttributeError) as excinfo: + cds.get("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + assert 'CompositeDataSource has no data source' in str(excinfo.value) diff --git a/stix2/test/v21/test_datastore_filesystem.py b/stix2/test/v21/test_datastore_filesystem.py new file mode 100644 index 0000000..49cbcc1 --- /dev/null +++ b/stix2/test/v21/test_datastore_filesystem.py @@ -0,0 +1,528 @@ +import json +import os +import shutil + +import pytest + +from stix2 import (Bundle, Campaign, CustomObject, FileSystemSink, + FileSystemSource, FileSystemStore, Filter, Identity, + Indicator, Malware, Relationship, properties) +from stix2.test.constants import (CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, + IDENTITY_KWARGS, INDICATOR_ID, + INDICATOR_KWARGS, MALWARE_ID, MALWARE_KWARGS, + RELATIONSHIP_IDS) + +FS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") + + +@pytest.fixture +def fs_store(): + # create + yield FileSystemStore(FS_PATH) + + # remove campaign dir + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +@pytest.fixture +def fs_source(): + # create + fs = FileSystemSource(FS_PATH) + assert fs.stix_dir == FS_PATH + yield fs + + # remove campaign dir + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +@pytest.fixture +def fs_sink(): + # create + fs = FileSystemSink(FS_PATH) + assert fs.stix_dir == FS_PATH + yield fs + + # remove campaign dir + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +@pytest.fixture +def bad_json_files(): + # create erroneous JSON files for tests to make sure handled gracefully + + with open(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-json.txt"), "w+") as f: + f.write("Im not a JSON file") + + with open(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-bad-json.json"), "w+") as f: + f.write("Im not a JSON formatted file") + + yield True # dummy yield so can have teardown + + os.remove(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-json.txt")) + os.remove(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-bad-json.json")) + + +@pytest.fixture +def bad_stix_files(): + # create erroneous STIX JSON files for tests to make sure handled correctly + + # bad STIX object + stix_obj = { + "id": "intrusion-set--test-bad-stix", + "spec_version": "2.0" + # no "type" field + } + + with open(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-stix.json"), "w+") as f: + f.write(json.dumps(stix_obj)) + + yield True # dummy yield so can have teardown + + os.remove(os.path.join(FS_PATH, "intrusion-set", "intrusion-set--test-non-stix.json")) + + +@pytest.fixture(scope='module') +def rel_fs_store(): + cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] + fs = FileSystemStore(FS_PATH) + for o in stix_objs: + fs.add(o) + yield fs + + for o in stix_objs: + os.remove(os.path.join(FS_PATH, o.type, o.id + '.json')) + + +def test_filesystem_source_nonexistent_folder(): + with pytest.raises(ValueError) as excinfo: + 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: + 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): + # this tests the handling of two bad json files + # - one file should just be skipped (silently) as its a ".txt" extension + # - one file should be parsed and raise Exception bc its not JSON + try: + fs_source.get("intrusion-set--test-bad-json") + except TypeError as e: + assert "intrusion-set--test-bad-json" in str(e) + assert "could either not be parsed to JSON or was not valid STIX JSON" in str(e) + + +def test_filesystem_source_bad_stix_file(fs_source, bad_stix_files): + # this tests handling of bad STIX json object + try: + fs_source.get("intrusion-set--test-non-stix") + except TypeError as e: + assert "intrusion-set--test-non-stix" in str(e) + assert "could either not be parsed to JSON or was not valid STIX JSON" in str(e) + + +def test_filesytem_source_get_object(fs_source): + # get object + mal = fs_source.get("malware--6b616fc1-1505-48e3-8b2c-0d19337bff38") + assert mal.id == "malware--6b616fc1-1505-48e3-8b2c-0d19337bff38" + assert mal.name == "Rover" + + +def test_filesytem_source_get_nonexistent_object(fs_source): + ind = fs_source.get("indicator--6b616fc1-1505-48e3-8b2c-0d19337bff38") + assert ind is None + + +def test_filesytem_source_all_versions(fs_source): + # all versions - (currently not a true all versions call as FileSystem cant have multiple versions) + id_ = fs_source.get("identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5") + assert id_.id == "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" + assert id_.name == "The MITRE Corporation" + assert id_.type == "identity" + + +def test_filesytem_source_query_single(fs_source): + # query2 + is_2 = fs_source.query([Filter("external_references.external_id", '=', "T1027")]) + assert len(is_2) == 1 + + is_2 = is_2[0] + assert is_2.id == "attack-pattern--b3d682b6-98f2-4fb0-aa3b-b4df007ca70a" + assert is_2.type == "attack-pattern" + + +def test_filesytem_source_query_multiple(fs_source): + # query + intrusion_sets = fs_source.query([Filter("type", '=', "intrusion-set")]) + assert len(intrusion_sets) == 2 + assert "intrusion-set--a653431d-6a5e-4600-8ad3-609b5af57064" in [is_.id for is_ in intrusion_sets] + assert "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a" in [is_.id for is_ in intrusion_sets] + + is_1 = [is_ for is_ in intrusion_sets if is_.id == "intrusion-set--f3bdec95-3d62-42d9-a840-29630f6cdc1a"][0] + assert "DragonOK" in is_1.aliases + assert len(is_1.external_references) == 4 + + +def test_filesystem_sink_add_python_stix_object(fs_sink, fs_source): + # add python stix object + camp1 = Campaign(name="Hannibal", + objective="Targeting Italian and Spanish Diplomat internet accounts", + aliases=["War Elephant"]) + + fs_sink.add(camp1) + + assert os.path.exists(os.path.join(FS_PATH, "campaign", camp1.id + ".json")) + + camp1_r = fs_source.get(camp1.id) + assert camp1_r.id == camp1.id + assert camp1_r.name == "Hannibal" + assert "War Elephant" in camp1_r.aliases + + os.remove(os.path.join(FS_PATH, "campaign", camp1_r.id + ".json")) + + +def test_filesystem_sink_add_stix_object_dict(fs_sink, fs_source): + # add stix object dict + camp2 = { + "name": "Aurelius", + "type": "campaign", + "objective": "German and French Intelligence Services", + "aliases": ["Purple Robes"], + "id": "campaign--111111b6-1112-4fb0-111b-b111107ca70a", + "created": "2017-05-31T21:31:53.197755Z" + } + + fs_sink.add(camp2) + + assert os.path.exists(os.path.join(FS_PATH, "campaign", camp2["id"] + ".json")) + + camp2_r = fs_source.get(camp2["id"]) + assert camp2_r.id == camp2["id"] + assert camp2_r.name == camp2["name"] + assert "Purple Robes" in camp2_r.aliases + + os.remove(os.path.join(FS_PATH, "campaign", camp2_r.id + ".json")) + + +def test_filesystem_sink_add_stix_bundle_dict(fs_sink, fs_source): + # add stix bundle dict + bund = { + "type": "bundle", + "id": "bundle--112211b6-1112-4fb0-111b-b111107ca70a", + "objects": [ + { + "name": "Atilla", + "type": "campaign", + "objective": "Bulgarian, Albanian and Romanian Intelligence Services", + "aliases": ["Huns"], + "id": "campaign--133111b6-1112-4fb0-111b-b111107ca70a", + "created": "2017-05-31T21:31:53.197755Z" + } + ] + } + + fs_sink.add(bund) + + assert os.path.exists(os.path.join(FS_PATH, "campaign", bund["objects"][0]["id"] + ".json")) + + camp3_r = fs_source.get(bund["objects"][0]["id"]) + assert camp3_r.id == bund["objects"][0]["id"] + assert camp3_r.name == bund["objects"][0]["name"] + assert "Huns" in camp3_r.aliases + + os.remove(os.path.join(FS_PATH, "campaign", camp3_r.id + ".json")) + + +def test_filesystem_sink_add_json_stix_object(fs_sink, fs_source): + # add json-encoded stix obj + camp4 = '{"type": "campaign", "id":"campaign--144111b6-1112-4fb0-111b-b111107ca70a",'\ + ' "created":"2017-05-31T21:31:53.197755Z", "name": "Ghengis Khan", "objective": "China and Russian infrastructure"}' + + fs_sink.add(camp4) + + assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--144111b6-1112-4fb0-111b-b111107ca70a" + ".json")) + + camp4_r = fs_source.get("campaign--144111b6-1112-4fb0-111b-b111107ca70a") + assert camp4_r.id == "campaign--144111b6-1112-4fb0-111b-b111107ca70a" + assert camp4_r.name == "Ghengis Khan" + + os.remove(os.path.join(FS_PATH, "campaign", camp4_r.id + ".json")) + + +def test_filesystem_sink_json_stix_bundle(fs_sink, fs_source): + # add json-encoded stix bundle + bund2 = '{"type": "bundle", "id": "bundle--332211b6-1132-4fb0-111b-b111107ca70a",' \ + ' "objects": [{"type": "campaign", "id": "campaign--155155b6-1112-4fb0-111b-b111107ca70a",' \ + ' "created":"2017-05-31T21:31:53.197755Z", "name": "Spartacus", "objective": "Oppressive regimes of Africa and Middle East"}]}' + fs_sink.add(bund2) + + assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--155155b6-1112-4fb0-111b-b111107ca70a" + ".json")) + + camp5_r = fs_source.get("campaign--155155b6-1112-4fb0-111b-b111107ca70a") + assert camp5_r.id == "campaign--155155b6-1112-4fb0-111b-b111107ca70a" + assert camp5_r.name == "Spartacus" + + os.remove(os.path.join(FS_PATH, "campaign", camp5_r.id + ".json")) + + +def test_filesystem_sink_add_objects_list(fs_sink, fs_source): + # add list of objects + camp6 = Campaign(name="Comanche", + objective="US Midwest manufacturing firms, oil refineries, and businesses", + aliases=["Horse Warrior"]) + + camp7 = { + "name": "Napolean", + "type": "campaign", + "objective": "Central and Eastern Europe military commands and departments", + "aliases": ["The Frenchmen"], + "id": "campaign--122818b6-1112-4fb0-111b-b111107ca70a", + "created": "2017-05-31T21:31:53.197755Z" + } + + fs_sink.add([camp6, camp7]) + + assert os.path.exists(os.path.join(FS_PATH, "campaign", camp6.id + ".json")) + assert os.path.exists(os.path.join(FS_PATH, "campaign", "campaign--122818b6-1112-4fb0-111b-b111107ca70a" + ".json")) + + camp6_r = fs_source.get(camp6.id) + assert camp6_r.id == camp6.id + assert "Horse Warrior" in camp6_r.aliases + + camp7_r = fs_source.get(camp7["id"]) + assert camp7_r.id == camp7["id"] + assert "The Frenchmen" in camp7_r.aliases + + # remove all added objects + os.remove(os.path.join(FS_PATH, "campaign", camp6_r.id + ".json")) + os.remove(os.path.join(FS_PATH, "campaign", camp7_r.id + ".json")) + + +def test_filesystem_store_get_stored_as_bundle(fs_store): + coa = fs_store.get("course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f") + assert coa.id == "course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f" + assert coa.type == "course-of-action" + + +def test_filesystem_store_get_stored_as_object(fs_store): + coa = fs_store.get("course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd") + assert coa.id == "course-of-action--d9727aee-48b8-4fdb-89e2-4c49746ba4dd" + assert coa.type == "course-of-action" + + +def test_filesystem_store_all_versions(fs_store): + # all versions() - (note at this time, all_versions() is still not applicable to FileSystem, as only one version is ever stored) + rel = fs_store.all_versions("relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1")[0] + assert rel.id == "relationship--70dc6b5c-c524-429e-a6ab-0dd40f0482c1" + assert rel.type == "relationship" + + +def test_filesystem_store_query(fs_store): + # query() + tools = fs_store.query([Filter("labels", "in", "tool")]) + assert len(tools) == 2 + assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools] + assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools] + + +def test_filesystem_store_query_single_filter(fs_store): + query = Filter("labels", "in", "tool") + tools = fs_store.query(query) + assert len(tools) == 2 + assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [tool.id for tool in tools] + assert "tool--03342581-f790-4f03-ba41-e82e67392e23" in [tool.id for tool in tools] + + +def test_filesystem_store_empty_query(fs_store): + results = fs_store.query() # returns all + assert len(results) == 26 + assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [obj["id"] for obj in results] + assert "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" in [obj["id"] for obj in results] + + +def test_filesystem_store_query_multiple_filters(fs_store): + fs_store.source.filters.add(Filter("labels", "in", "tool")) + tools = fs_store.query(Filter("id", "=", "tool--242f3da3-4425-4d11-8f5c-b842886da966")) + assert len(tools) == 1 + assert tools[0].id == "tool--242f3da3-4425-4d11-8f5c-b842886da966" + + +def test_filesystem_store_query_dont_include_type_folder(fs_store): + results = fs_store.query(Filter("type", "!=", "tool")) + assert len(results) == 24 + + +def test_filesystem_store_add(fs_store): + # add() + camp1 = Campaign(name="Great Heathen Army", + objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", + aliases=["Ragnar"]) + fs_store.add(camp1) + + camp1_r = fs_store.get(camp1.id) + assert camp1_r.id == camp1.id + assert camp1_r.name == camp1.name + + # remove + os.remove(os.path.join(FS_PATH, "campaign", camp1_r.id + ".json")) + + +def test_filesystem_store_add_as_bundle(): + fs_store = FileSystemStore(FS_PATH, bundlify=True) + + camp1 = Campaign(name="Great Heathen Army", + objective="Targeting the government of United Kingdom and insitutions affiliated with the Church Of England", + aliases=["Ragnar"]) + fs_store.add(camp1) + + with open(os.path.join(FS_PATH, "campaign", camp1.id + ".json")) as bundle_file: + assert '"type": "bundle"' in bundle_file.read() + + camp1_r = fs_store.get(camp1.id) + assert camp1_r.id == camp1.id + assert camp1_r.name == camp1.name + + shutil.rmtree(os.path.join(FS_PATH, "campaign"), True) + + +def test_filesystem_add_bundle_object(fs_store): + bundle = Bundle() + fs_store.add(bundle) + + +def test_filesystem_store_add_invalid_object(fs_store): + ind = ('campaign', 'campaign--111111b6-1112-4fb0-111b-b111107ca70a') # tuple isn't valid + with pytest.raises(TypeError) as excinfo: + fs_store.add(ind) + assert 'stix_data must be' in str(excinfo.value) + assert 'a STIX object' in str(excinfo.value) + assert 'JSON formatted STIX' in str(excinfo.value) + assert 'JSON formatted STIX bundle' in str(excinfo.value) + + +def test_filesystem_object_with_custom_property(fs_store): + camp = Campaign(name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True) + + fs_store.add(camp, True) + + camp_r = fs_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_filesystem_object_with_custom_property_in_bundle(fs_store): + camp = Campaign(name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True) + + bundle = Bundle(camp, allow_custom=True) + fs_store.add(bundle) + + camp_r = fs_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_filesystem_custom_object(fs_store): + @CustomObject('x-new-obj', [ + ('property1', properties.StringProperty(required=True)), + ]) + class NewObj(): + pass + + newobj = NewObj(property1='something') + fs_store.add(newobj) + + newobj_r = fs_store.get(newobj.id) + assert newobj_r["id"] == newobj["id"] + assert newobj_r["property1"] == 'something' + + # remove dir + shutil.rmtree(os.path.join(FS_PATH, "x-new-obj"), True) + + +def test_relationships(rel_fs_store): + mal = rel_fs_store.get(MALWARE_ID) + resp = rel_fs_store.relationships(mal) + + assert len(resp) == 3 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[1] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_type(rel_fs_store): + mal = rel_fs_store.get(MALWARE_ID) + resp = rel_fs_store.relationships(mal, relationship_type='indicates') + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[0] + + +def test_relationships_by_source(rel_fs_store): + resp = rel_fs_store.relationships(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[1] + + +def test_relationships_by_target(rel_fs_store): + resp = rel_fs_store.relationships(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_type(rel_fs_store): + resp = rel_fs_store.relationships(MALWARE_ID, relationship_type='uses', target_only=True) + + assert len(resp) == 1 + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_source(rel_fs_store): + with pytest.raises(ValueError) as excinfo: + rel_fs_store.relationships(MALWARE_ID, target_only=True, source_only=True) + + assert 'not both' in str(excinfo.value) + + +def test_related_to(rel_fs_store): + mal = rel_fs_store.get(MALWARE_ID) + resp = rel_fs_store.related_to(mal) + + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_source(rel_fs_store): + resp = rel_fs_store.related_to(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_target(rel_fs_store): + resp = rel_fs_store.related_to(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) diff --git a/stix2/test/v21/test_datastore_filters.py b/stix2/test/v21/test_datastore_filters.py new file mode 100644 index 0000000..8ed82f3 --- /dev/null +++ b/stix2/test/v21/test_datastore_filters.py @@ -0,0 +1,465 @@ +import pytest + +from stix2 import parse +from stix2.datastore.filters import Filter, apply_common_filters +from stix2.utils import STIXdatetime, parse_into_datetime + +stix_objs = [ + { + "created": "2017-01-27T13:49:53.997Z", + "description": "\n\nTITLE:\n\tPoison Ivy", + "id": "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111", + "spec_version": "2.1", + "labels": [ + "remote-access-trojan" + ], + "modified": "2017-01-27T13:49:53.997Z", + "name": "Poison Ivy", + "type": "malware", + "is_family": False + }, + { + "created": "2014-05-08T09:00:00.000Z", + "id": "indicator--a932fcc6-e032-176c-126f-cb970a5a1ade", + "labels": [ + "file-hash-watchlist" + ], + "modified": "2014-05-08T09:00:00.000Z", + "name": "File hash for Poison Ivy variant", + "pattern": "[file:hashes.'SHA-256' = 'ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c']", + "type": "indicator", + "valid_from": "2014-05-08T09:00:00.000000Z" + }, + { + "created": "2014-05-08T09:00:00.000Z", + "granular_markings": [ + { + "marking_ref": "marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed", + "selectors": [ + "relationship_type" + ] + } + ], + "id": "relationship--2f9a9aa9-108a-4333-83e2-4fb25add0463", + "modified": "2014-05-08T09:00:00.000Z", + "object_marking_refs": [ + "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" + ], + "relationship_type": "indicates", + "revoked": True, + "source_ref": "indicator--a932fcc6-e032-176c-126f-cb970a5a1ade", + "target_ref": "malware--fdd60b30-b67c-11e3-b0b9-f01faf20d111", + "type": "relationship" + }, + { + "id": "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef", + "created": "2016-02-14T00:00:00.000Z", + "created_by_ref": "identity--00000000-0000-0000-0000-b8e91df99dc9", + "modified": "2016-02-14T00:00:00.000Z", + "type": "vulnerability", + "name": "CVE-2014-0160", + "description": "The (1) TLS...", + "external_references": [ + { + "source_name": "cve", + "external_id": "CVE-2014-0160" + } + ], + "labels": ["heartbleed", "has-logo"] + }, + { + "type": "observed-data", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "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", + "number_observed": 1, + "objects": { + "0": { + "type": "file", + "name": "HAL 9000.exe" + } + } + + } +] + + +filters = [ + Filter("type", "!=", "relationship"), + Filter("id", "=", "relationship--2f9a9aa9-108a-4333-83e2-4fb25add0463"), + Filter("labels", "in", "remote-access-trojan"), + Filter("created", ">", "2015-01-01T01:00:00.000Z"), + Filter("revoked", "=", True), + Filter("revoked", "!=", True), + Filter("object_marking_refs", "=", "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9"), + Filter("granular_markings.selectors", "in", "relationship_type"), + Filter("granular_markings.marking_ref", "=", "marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed"), + Filter("external_references.external_id", "in", "CVE-2014-0160,CVE-2017-6608"), + Filter("created_by_ref", "=", "identity--00000000-0000-0000-0000-b8e91df99dc9"), + Filter("object_marking_refs", "=", "marking-definition--613f2e26-0000-0000-0000-b8e91df99dc9"), + Filter("granular_markings.selectors", "in", "description"), + Filter("external_references.source_name", "=", "CVE"), + Filter("objects", "=", {"0": {"type": "file", "name": "HAL 9000.exe"}}) +] + +# same as above objects but converted to real Python STIX2 objects +# to test filters against true Python STIX2 objects +real_stix_objs = [parse(stix_obj) for stix_obj in stix_objs] + + +def test_filter_ops_check(): + # invalid filters - non supported operators + + with pytest.raises(ValueError) as excinfo: + # create Filter that has an operator that is not allowed + Filter('modified', '*', 'not supported operator') + assert str(excinfo.value) == "Filter operator '*' not supported for specified property: 'modified'" + + with pytest.raises(ValueError) as excinfo: + Filter("type", "%", "4") + assert "Filter operator '%' not supported for specified property" in str(excinfo.value) + + +def test_filter_value_type_check(): + # invalid filters - non supported value types + + with pytest.raises(TypeError) as excinfo: + Filter('created', '=', object()) + # On Python 2, the type of object() is `` On Python 3, it's ``. + assert any([s in str(excinfo.value) for s in ["", "''"]]) + assert "is not supported. The type must be a Python immutable type or dictionary" in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + Filter("type", "=", complex(2, -1)) + assert any([s in str(excinfo.value) for s in ["", "''"]]) + assert "is not supported. The type must be a Python immutable type or dictionary" in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + Filter("type", "=", set([16, 23])) + assert any([s in str(excinfo.value) for s in ["", "''"]]) + assert "is not supported. The type must be a Python immutable type or dictionary" in str(excinfo.value) + + +def test_filter_type_underscore_check(): + # check that Filters where property="type", value (name) doesnt have underscores + with pytest.raises(ValueError) as excinfo: + Filter("type", "=", "oh_underscore") + assert "Filter for property 'type' cannot have its value 'oh_underscore'" in str(excinfo.value) + + +def test_apply_common_filters0(): + # "Return any object whose type is not relationship" + resp = list(apply_common_filters(stix_objs, [filters[0]])) + ids = [r['id'] for r in resp] + assert stix_objs[0]['id'] in ids + assert stix_objs[1]['id'] in ids + assert stix_objs[3]['id'] in ids + assert len(ids) == 4 + + resp = list(apply_common_filters(real_stix_objs, [filters[0]])) + ids = [r.id for r in resp] + assert real_stix_objs[0].id in ids + assert real_stix_objs[1].id in ids + assert real_stix_objs[3].id in ids + assert len(ids) == 4 + + +def test_apply_common_filters1(): + # "Return any object that matched id relationship--2f9a9aa9-108a-4333-83e2-4fb25add0463" + resp = list(apply_common_filters(stix_objs, [filters[1]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[1]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters2(): + # "Return any object that contains remote-access-trojan in labels" + resp = list(apply_common_filters(stix_objs, [filters[2]])) + assert resp[0]['id'] == stix_objs[0]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[2]])) + assert resp[0].id == real_stix_objs[0].id + assert len(resp) == 1 + + +def test_apply_common_filters3(): + # "Return any object created after 2015-01-01T01:00:00.000Z" + resp = list(apply_common_filters(stix_objs, [filters[3]])) + assert resp[0]['id'] == stix_objs[0]['id'] + assert len(resp) == 3 + + resp = list(apply_common_filters(real_stix_objs, [filters[3]])) + assert resp[0].id == real_stix_objs[0].id + assert len(resp) == 3 + + +def test_apply_common_filters4(): + # "Return any revoked object" + resp = list(apply_common_filters(stix_objs, [filters[4]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[4]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters5(): + # "Return any object whose not revoked" + resp = list(apply_common_filters(stix_objs, [filters[5]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[5]])) + assert len(resp) == 4 + + +def test_apply_common_filters6(): + # "Return any object that matches marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9 in object_marking_refs" + resp = list(apply_common_filters(stix_objs, [filters[6]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[6]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters7(): + # "Return any object that contains relationship_type in their selectors AND + # also has marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed in marking_ref" + resp = list(apply_common_filters(stix_objs, [filters[7], filters[8]])) + assert resp[0]['id'] == stix_objs[2]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[7], filters[8]])) + assert resp[0].id == real_stix_objs[2].id + assert len(resp) == 1 + + +def test_apply_common_filters8(): + # "Return any object that contains CVE-2014-0160,CVE-2017-6608 in their external_id" + resp = list(apply_common_filters(stix_objs, [filters[9]])) + assert resp[0]['id'] == stix_objs[3]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[9]])) + assert resp[0].id == real_stix_objs[3].id + assert len(resp) == 1 + + +def test_apply_common_filters9(): + # "Return any object that matches created_by_ref identity--00000000-0000-0000-0000-b8e91df99dc9" + resp = list(apply_common_filters(stix_objs, [filters[10]])) + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs, [filters[10]])) + assert len(resp) == 1 + + +def test_apply_common_filters10(): + # "Return any object that matches marking-definition--613f2e26-0000-0000-0000-b8e91df99dc9 in object_marking_refs" (None) + resp = list(apply_common_filters(stix_objs, [filters[11]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[11]])) + assert len(resp) == 0 + + +def test_apply_common_filters11(): + # "Return any object that contains description in its selectors" (None) + resp = list(apply_common_filters(stix_objs, [filters[12]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[12]])) + assert len(resp) == 0 + + +def test_apply_common_filters12(): + # "Return any object that matches CVE in source_name" (None, case sensitive) + resp = list(apply_common_filters(stix_objs, [filters[13]])) + assert len(resp) == 0 + + resp = list(apply_common_filters(real_stix_objs, [filters[13]])) + assert len(resp) == 0 + + +def test_apply_common_filters13(): + # Return any object that matches file object in "objects" + resp = list(apply_common_filters(stix_objs, [filters[14]])) + assert resp[0]["id"] == stix_objs[4]["id"] + assert len(resp) == 1 + # important additional check to make sure original File dict was + # not converted to File object. (this was a deep bug found) + assert isinstance(resp[0]["objects"]["0"], dict) + + resp = list(apply_common_filters(real_stix_objs, [filters[14]])) + assert resp[0].id == real_stix_objs[4].id + assert len(resp) == 1 + + +def test_datetime_filter_behavior(): + """if a filter is initialized with its value being a datetime object + OR the STIX object property being filtered on is a datetime object, all + resulting comparisons executed are done on the string representations + of the datetime objects, as the Filter functionality will convert + all datetime objects to there string forms using format_datetim() + + This test makes sure all datetime comparisons are carried out correctly + """ + filter_with_dt_obj = Filter("created", "=", parse_into_datetime("2016-02-14T00:00:00.000Z", "millisecond")) + filter_with_str = Filter("created", "=", "2016-02-14T00:00:00.000Z") + + # check that filter value is converted from datetime to str + assert isinstance(filter_with_dt_obj.value, str) + + # compare datetime string to filter w/ datetime obj + resp = list(apply_common_filters(stix_objs, [filter_with_dt_obj])) + assert len(resp) == 1 + assert resp[0]["id"] == "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef" + + # compare datetime obj to filter w/ datetime obj + resp = list(apply_common_filters(real_stix_objs, [filter_with_dt_obj])) + assert len(resp) == 1 + assert resp[0]["id"] == "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef" + assert isinstance(resp[0].created, STIXdatetime) # make sure original object not altered + + # compare datetime string to filter w/ str + resp = list(apply_common_filters(stix_objs, [filter_with_str])) + assert len(resp) == 1 + assert resp[0]["id"] == "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef" + + # compare datetime obj to filter w/ str + resp = list(apply_common_filters(real_stix_objs, [filter_with_str])) + assert len(resp) == 1 + assert resp[0]["id"] == "vulnerability--ee916c28-c7a4-4d0d-ad56-a8d357f89fef" + assert isinstance(resp[0].created, STIXdatetime) # make sure original object not altered + + +def test_filters0(stix_objs2, real_stix_objs2): + # "Return any object modified before 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", "<", "2017-01-28T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[1]['id'] + assert len(resp) == 2 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("modified", "<", parse_into_datetime("2017-01-28T13:49:53.935Z"))])) + assert resp[0].id == real_stix_objs2[1].id + assert len(resp) == 2 + + +def test_filters1(stix_objs2, real_stix_objs2): + # "Return any object modified after 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", ">", "2017-01-28T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("modified", ">", parse_into_datetime("2017-01-28T13:49:53.935Z"))])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 1 + + +def test_filters2(stix_objs2, real_stix_objs2): + # "Return any object modified after or on 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", ">=", "2017-01-27T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 3 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("modified", ">=", parse_into_datetime("2017-01-27T13:49:53.935Z"))])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 3 + + +def test_filters3(stix_objs2, real_stix_objs2): + # "Return any object modified before or on 2017-01-28T13:49:53.935Z" + resp = list(apply_common_filters(stix_objs2, [Filter("modified", "<=", "2017-01-27T13:49:53.935Z")])) + assert resp[0]['id'] == stix_objs2[1]['id'] + assert len(resp) == 2 + + # "Return any object modified before or on 2017-01-28T13:49:53.935Z" + fv = Filter("modified", "<=", parse_into_datetime("2017-01-27T13:49:53.935Z")) + resp = list(apply_common_filters(real_stix_objs2, [fv])) + assert resp[0].id == real_stix_objs2[1].id + assert len(resp) == 2 + + +def test_filters4(): + # Assert invalid Filter cannot be created + with pytest.raises(ValueError) as excinfo: + Filter("modified", "?", "2017-01-27T13:49:53.935Z") + assert str(excinfo.value) == ("Filter operator '?' not supported " + "for specified property: 'modified'") + + +def test_filters5(stix_objs2, real_stix_objs2): + # "Return any object whose id is not indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f" + resp = list(apply_common_filters(stix_objs2, [Filter("id", "!=", "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("id", "!=", "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f")])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 1 + + +def test_filters6(stix_objs2, real_stix_objs2): + # Test filtering on non-common property + resp = list(apply_common_filters(stix_objs2, [Filter("name", "=", "Malicious site hosting downloader")])) + assert resp[0]['id'] == stix_objs2[0]['id'] + assert len(resp) == 3 + + resp = list(apply_common_filters(real_stix_objs2, [Filter("name", "=", "Malicious site hosting downloader")])) + assert resp[0].id == real_stix_objs2[0].id + assert len(resp) == 3 + + +def test_filters7(stix_objs2, real_stix_objs2): + # Test filtering on embedded property + obsvd_data_obj = { + "type": "observed-data", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "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", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + }, + "extensions": { + "pdf-ext": { + "version": "1.7", + "document_info_dict": { + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02" + }, + "pdfid0": "DFCE52BD827ECF765649852119D", + "pdfid1": "57A1E0F9ED2AE523E313C" + } + } + } + } + } + + stix_objects = list(stix_objs2) + [obsvd_data_obj] + real_stix_objects = list(real_stix_objs2) + [parse(obsvd_data_obj)] + + resp = list(apply_common_filters(stix_objects, [Filter("objects.0.extensions.pdf-ext.version", ">", "1.2")])) + assert resp[0]['id'] == stix_objects[3]['id'] + assert len(resp) == 1 + + resp = list(apply_common_filters(real_stix_objects, [Filter("objects.0.extensions.pdf-ext.version", ">", "1.2")])) + assert resp[0].id == real_stix_objects[3].id + assert len(resp) == 1 diff --git a/stix2/test/v21/test_datastore_memory.py b/stix2/test/v21/test_datastore_memory.py new file mode 100644 index 0000000..7a5bf10 --- /dev/null +++ b/stix2/test/v21/test_datastore_memory.py @@ -0,0 +1,87 @@ +import pytest + +from stix2.datastore import CompositeDataSource, make_id +from stix2.datastore.filters import Filter +from stix2.datastore.memory import MemorySink, MemorySource + + +def test_add_remove_composite_datasource(): + cds = CompositeDataSource() + ds1 = MemorySource() + ds2 = MemorySource() + ds3 = MemorySink() + + with pytest.raises(TypeError) as excinfo: + cds.add_data_sources([ds1, ds2, ds1, ds3]) + assert str(excinfo.value) == ("DataSource (to be added) is not of type " + "stix2.DataSource. DataSource type is ''") + + cds.add_data_sources([ds1, ds2, ds1]) + + assert len(cds.get_all_data_sources()) == 2 + + cds.remove_data_sources([ds1.id, ds2.id]) + + assert len(cds.get_all_data_sources()) == 0 + + +def test_composite_datasource_operations(stix_objs1, stix_objs2): + BUNDLE1 = dict(id="bundle--%s" % make_id(), + objects=stix_objs1, + spec_version="2.0", + type="bundle") + cds1 = CompositeDataSource() + ds1_1 = MemorySource(stix_data=BUNDLE1) + ds1_2 = MemorySource(stix_data=stix_objs2) + + cds2 = CompositeDataSource() + ds2_1 = MemorySource(stix_data=BUNDLE1) + ds2_2 = MemorySource(stix_data=stix_objs2) + + cds1.add_data_sources([ds1_1, ds1_2]) + cds2.add_data_sources([ds2_1, ds2_2]) + + indicators = cds1.all_versions("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + + # In STIX_OBJS2 changed the 'modified' property to a later time... + assert len(indicators) == 2 + + cds1.add_data_sources([cds2]) + + indicator = cds1.get("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + + assert indicator["id"] == "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f" + assert indicator["modified"] == "2017-01-31T13:49:53.935Z" + assert indicator["type"] == "indicator" + + query1 = [ + Filter("type", "=", "indicator") + ] + + query2 = [ + Filter("valid_from", "=", "2017-01-27T13:49:53.935382Z") + ] + + cds1.filters.add(query2) + + results = cds1.query(query1) + + # STIX_OBJS2 has indicator with later time, one with different id, one with + # original time in STIX_OBJS1 + assert len(results) == 3 + + indicator = cds1.get("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + + assert indicator["id"] == "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f" + assert indicator["modified"] == "2017-01-31T13:49:53.935Z" + assert indicator["type"] == "indicator" + + # There is only one indicator with different ID. Since we use the same data + # when deduplicated, only two indicators (one with different modified). + results = cds1.all_versions("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + assert len(results) == 2 + + # Since we have filters already associated with our CompositeSource providing + # nothing returns the same as cds1.query(query1) (the associated query is query2) + results = cds1.query([]) + assert len(results) == 3 diff --git a/stix2/test/v21/test_datastore_taxii.py b/stix2/test/v21/test_datastore_taxii.py new file mode 100644 index 0000000..8cc5033 --- /dev/null +++ b/stix2/test/v21/test_datastore_taxii.py @@ -0,0 +1,390 @@ +import json + +from medallion.filters.basic_filter import BasicFilter +import pytest +from requests.models import Response +from taxii2client import Collection, _filter_kwargs_to_query_params + +from stix2 import (Bundle, TAXIICollectionSink, TAXIICollectionSource, + TAXIICollectionStore, ThreatActor) +from stix2.datastore import DataSourceError +from stix2.datastore.filters import Filter + +COLLECTION_URL = 'https://example.com/api1/collections/91a7b528-80eb-42ed-a74d-c6fbd5a26116/' + + +class MockTAXIICollectionEndpoint(Collection): + """Mock for taxii2_client.TAXIIClient""" + + def __init__(self, url, **kwargs): + super(MockTAXIICollectionEndpoint, self).__init__(url, **kwargs) + self.objects = [] + + def add_objects(self, bundle): + self._verify_can_write() + if isinstance(bundle, str): + bundle = json.loads(bundle) + for object in bundle.get("objects", []): + self.objects.append(object) + + def get_objects(self, **filter_kwargs): + self._verify_can_read() + query_params = _filter_kwargs_to_query_params(filter_kwargs) + if not isinstance(query_params, dict): + query_params = json.loads(query_params) + full_filter = BasicFilter(query_params or {}) + objs = full_filter.process_filter( + self.objects, + ("id", "type", "version"), + [] + ) + if objs: + return Bundle(objects=objs) + else: + resp = Response() + resp.status_code = 404 + resp.raise_for_status() + + def get_object(self, id, version=None): + self._verify_can_read() + query_params = None + if version: + query_params = _filter_kwargs_to_query_params({"version": version}) + if query_params: + query_params = json.loads(query_params) + full_filter = BasicFilter(query_params or {}) + objs = full_filter.process_filter( + self.objects, + ("version",), + [] + ) + if objs: + return Bundle(objects=objs) + else: + resp = Response() + resp.status_code = 404 + resp.raise_for_status() + + +@pytest.fixture +def collection(stix_objs1): + mock = MockTAXIICollectionEndpoint(COLLECTION_URL, **{ + "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", + "title": "Writable Collection", + "description": "This collection is a dropbox for submitting indicators", + "can_read": True, + "can_write": True, + "media_types": [ + "application/vnd.oasis.stix+json; version=2.0" + ] + }) + + mock.objects.extend(stix_objs1) + return mock + + +@pytest.fixture +def collection_no_rw_access(stix_objs1): + mock = MockTAXIICollectionEndpoint(COLLECTION_URL, **{ + "id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116", + "title": "Not writeable or readable Collection", + "description": "This collection is a dropbox for submitting indicators", + "can_read": False, + "can_write": False, + "media_types": [ + "application/vnd.oasis.stix+json; version=2.0" + ] + }) + + mock.objects.extend(stix_objs1) + return mock + + +def test_ds_taxii(collection): + ds = TAXIICollectionSource(collection) + assert ds.collection is not None + + +def test_add_stix2_object(collection): + tc_sink = TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = ThreatActor(name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ]) + + tc_sink.add(ta) + + +def test_add_stix2_with_custom_object(collection): + tc_sink = TAXIICollectionStore(collection, allow_custom=True) + + # create new STIX threat-actor + ta = ThreatActor(name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ], + foo="bar", + allow_custom=True) + + tc_sink.add(ta) + + +def test_add_list_object(collection, indicator): + tc_sink = TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = ThreatActor(name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ]) + + tc_sink.add([ta, indicator]) + + +def test_add_stix2_bundle_object(collection): + tc_sink = TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = ThreatActor(name="Teddy Bear", + labels=["nation-state"], + sophistication="innovator", + resource_level="government", + goals=[ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector", + ]) + + tc_sink.add(Bundle(objects=[ta])) + + +def test_add_str_object(collection): + tc_sink = TAXIICollectionSink(collection) + + # create new STIX threat-actor + ta = """{ + "type": "threat-actor", + "id": "threat-actor--eddff64f-feb1-4469-b07c-499a73c96415", + "created": "2018-04-23T16:40:50.847Z", + "modified": "2018-04-23T16:40:50.847Z", + "name": "Teddy Bear", + "goals": [ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector" + ], + "sophistication": "innovator", + "resource_level": "government", + "labels": [ + "nation-state" + ] + }""" + + tc_sink.add(ta) + + +def test_add_dict_object(collection): + tc_sink = TAXIICollectionSink(collection) + + ta = { + "type": "threat-actor", + "id": "threat-actor--eddff64f-feb1-4469-b07c-499a73c96415", + "created": "2018-04-23T16:40:50.847Z", + "modified": "2018-04-23T16:40:50.847Z", + "name": "Teddy Bear", + "goals": [ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector" + ], + "sophistication": "innovator", + "resource_level": "government", + "labels": [ + "nation-state" + ] + } + + tc_sink.add(ta) + + +def test_add_dict_bundle_object(collection): + tc_sink = TAXIICollectionSink(collection) + + ta = { + "type": "bundle", + "id": "bundle--860ccc8d-56c9-4fda-9384-84276fb52fb1", + "objects": [ + { + "type": "threat-actor", + "id": "threat-actor--dc5a2f41-f76e-425a-81fe-33afc7aabd75", + "created": "2018-04-23T18:45:11.390Z", + "modified": "2018-04-23T18:45:11.390Z", + "name": "Teddy Bear", + "goals": [ + "compromising environment NGOs", + "water-hole attacks geared towards energy sector" + ], + "sophistication": "innovator", + "resource_level": "government", + "labels": [ + "nation-state" + ] + } + ] + } + + tc_sink.add(ta) + + +def test_get_stix2_object(collection): + tc_sink = TAXIICollectionSource(collection) + + objects = tc_sink.get("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + + assert objects + + +def test_parse_taxii_filters(collection): + query = [ + Filter("added_after", "=", "2016-02-01T00:00:01.000Z"), + Filter("id", "=", "taxii stix object ID"), + Filter("type", "=", "taxii stix object ID"), + Filter("version", "=", "first"), + Filter("created_by_ref", "=", "Bane"), + ] + + taxii_filters_expected = [ + Filter("added_after", "=", "2016-02-01T00:00:01.000Z"), + Filter("id", "=", "taxii stix object ID"), + Filter("type", "=", "taxii stix object ID"), + Filter("version", "=", "first") + ] + + ds = TAXIICollectionSource(collection) + + taxii_filters = ds._parse_taxii_filters(query) + + assert taxii_filters == taxii_filters_expected + + +def test_add_get_remove_filter(collection): + ds = TAXIICollectionSource(collection) + + # First 3 filters are valid, remaining properties are erroneous in some way + valid_filters = [ + Filter('type', '=', 'malware'), + Filter('id', '!=', 'stix object id'), + Filter('labels', 'in', ["heartbleed", "malicious-activity"]), + ] + + assert len(ds.filters) == 0 + + ds.filters.add(valid_filters[0]) + assert len(ds.filters) == 1 + + # Addin the same filter again will have no effect since `filters` acts + # like a set + ds.filters.add(valid_filters[0]) + assert len(ds.filters) == 1 + + ds.filters.add(valid_filters[1]) + assert len(ds.filters) == 2 + + ds.filters.add(valid_filters[2]) + assert len(ds.filters) == 3 + + assert valid_filters == [f for f in ds.filters] + + # remove + ds.filters.remove(valid_filters[0]) + + assert len(ds.filters) == 2 + + ds.filters.add(valid_filters) + + +def test_get_all_versions(collection): + ds = TAXIICollectionStore(collection) + + indicators = ds.all_versions('indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f') + # There are 3 indicators but 2 share the same 'modified' timestamp + assert len(indicators) == 2 + + +def test_can_read_error(collection_no_rw_access): + """create a TAXIICOllectionSource with a taxii2client.Collection + instance that does not have read access, check ValueError exception is raised""" + + with pytest.raises(DataSourceError) as excinfo: + TAXIICollectionSource(collection_no_rw_access) + assert "Collection object provided does not have read access" in str(excinfo.value) + + +def test_can_write_error(collection_no_rw_access): + """create a TAXIICOllectionSink with a taxii2client.Collection + instance that does not have write access, check ValueError exception is raised""" + + with pytest.raises(DataSourceError) as excinfo: + TAXIICollectionSink(collection_no_rw_access) + assert "Collection object provided does not have write access" in str(excinfo.value) + + +def test_get_404(): + """a TAXIICollectionSource.get() call that receives an HTTP 404 response + code from the taxii2client should be be returned as None. + + TAXII spec states that a TAXII server can return a 404 for nonexistent + resources or lack of access. Decided that None is acceptable reponse + to imply that state of the TAXII endpoint. + """ + + class TAXIICollection404(): + can_read = True + + def get_object(self, id, version=None): + resp = Response() + resp.status_code = 404 + resp.raise_for_status() + + ds = TAXIICollectionSource(TAXIICollection404()) + + # this will raise 404 from mock TAXII Client but TAXIICollectionStore + # should handle gracefully and return None + stix_obj = ds.get("indicator--1") + assert stix_obj is None + + +def test_all_versions_404(collection): + """ a TAXIICollectionSource.all_version() call that recieves an HTTP 404 + response code from the taxii2client should be returned as an exception""" + + ds = TAXIICollectionStore(collection) + + with pytest.raises(DataSourceError) as excinfo: + ds.all_versions("indicator--1") + assert "are either not found or access is denied" in str(excinfo.value) + assert "404" in str(excinfo.value) + + +def test_query_404(collection): + """ a TAXIICollectionSource.query() call that recieves an HTTP 404 + response code from the taxii2client should be returned as an exception""" + + ds = TAXIICollectionStore(collection) + query = [Filter("type", "=", "malware")] + + with pytest.raises(DataSourceError) as excinfo: + ds.query(query=query) + assert "are either not found or access is denied" in str(excinfo.value) + assert "404" in str(excinfo.value) diff --git a/stix2/test/v21/test_environment.py b/stix2/test/v21/test_environment.py new file mode 100644 index 0000000..a5166b7 --- /dev/null +++ b/stix2/test/v21/test_environment.py @@ -0,0 +1,352 @@ +import pytest + +import stix2 + +from .constants import (CAMPAIGN_ID, CAMPAIGN_KWARGS, FAKE_TIME, IDENTITY_ID, + IDENTITY_KWARGS, INDICATOR_ID, INDICATOR_KWARGS, + MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS) + + +@pytest.fixture +def ds(): + cam = stix2.Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = stix2.Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = stix2.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = stix2.Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = stix2.Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = stix2.Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = stix2.Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] + yield stix2.MemoryStore(stix_objs) + + +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_reference(): + 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" + + +def test_environment_functions(): + env = stix2.Environment(stix2.ObjectFactory(created_by_ref=IDENTITY_ID), + stix2.MemoryStore()) + + # Create a STIX object + ind = env.create(stix2.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + assert ind.created_by_ref == IDENTITY_ID + + # Add objects to datastore + ind2 = ind.new_version(labels=['benign']) + env.add([ind, ind2]) + + # Get both versions of the object + resp = env.all_versions(INDICATOR_ID) + assert len(resp) == 1 # should be 2, but MemoryStore only keeps 1 version of objects + + # Get just the most recent version of the object + resp = env.get(INDICATOR_ID) + assert resp['labels'][0] == 'benign' + + # Search on something other than id + query = [stix2.Filter('type', '=', 'vulnerability')] + resp = env.query(query) + assert len(resp) == 0 + + # See different results after adding filters to the environment + env.add_filters([stix2.Filter('type', '=', 'indicator'), + stix2.Filter('created_by_ref', '=', IDENTITY_ID)]) + env.add_filter(stix2.Filter('labels', '=', 'benign')) # should be 'malicious-activity' + resp = env.get(INDICATOR_ID) + assert resp['labels'][0] == 'benign' # should be 'malicious-activity' + + +def test_environment_source_and_sink(): + ind = stix2.Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + env = stix2.Environment(source=stix2.MemorySource([ind]), sink=stix2.MemorySink([ind])) + assert env.get(INDICATOR_ID).labels[0] == 'malicious-activity' + + +def test_environment_datastore_and_sink(): + with pytest.raises(ValueError) as excinfo: + stix2.Environment(factory=stix2.ObjectFactory(), + store=stix2.MemoryStore(), sink=stix2.MemorySink) + assert 'Data store already provided' in str(excinfo.value) + + +def test_environment_no_datastore(): + env = stix2.Environment(factory=stix2.ObjectFactory()) + + with pytest.raises(AttributeError) as excinfo: + env.add(stix2.Indicator(**INDICATOR_KWARGS)) + assert 'Environment has no data sink to put objects in' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.get(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.all_versions(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.query(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.relationships(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + with pytest.raises(AttributeError) as excinfo: + env.related_to(INDICATOR_ID) + assert 'Environment has no data source' in str(excinfo.value) + + +def test_environment_add_filters(): + env = stix2.Environment(factory=stix2.ObjectFactory()) + env.add_filters([INDICATOR_ID]) + env.add_filter(INDICATOR_ID) + + +def test_environment_datastore_and_no_object_factory(): + # Uses a default object factory + env = stix2.Environment(store=stix2.MemoryStore()) + ind = env.create(stix2.Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + assert ind.id == INDICATOR_ID + + +def test_parse_malware(): + env = stix2.Environment() + data = """{ + "type": "malware", + "spec_version": "2.1", + "id": "malware--fedcba98-7654-3210-fedc-ba9876543210", + "created": "2017-01-01T12:34:56.000Z", + "modified": "2017-01-01T12:34:56.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware" + ], + "is_family": false + }""" + mal = env.parse(data) + + assert mal.type == 'malware' + assert mal.id == MALWARE_ID + assert mal.created == FAKE_TIME + assert mal.modified == FAKE_TIME + assert mal.labels == ['ransomware'] + assert mal.name == "Cryptolocker" + + +def test_creator_of(): + identity = stix2.Identity(**IDENTITY_KWARGS) + factory = stix2.ObjectFactory(created_by_ref=identity.id) + env = stix2.Environment(store=stix2.MemoryStore(), factory=factory) + env.add(identity) + + ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + creator = env.creator_of(ind) + assert creator is identity + + +def test_creator_of_no_datasource(): + identity = stix2.Identity(**IDENTITY_KWARGS) + factory = stix2.ObjectFactory(created_by_ref=identity.id) + env = stix2.Environment(factory=factory) + + ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + with pytest.raises(AttributeError) as excinfo: + env.creator_of(ind) + assert 'Environment has no data source' in str(excinfo.value) + + +def test_creator_of_not_found(): + identity = stix2.Identity(**IDENTITY_KWARGS) + factory = stix2.ObjectFactory(created_by_ref=identity.id) + env = stix2.Environment(store=stix2.MemoryStore(), factory=factory) + + ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + creator = env.creator_of(ind) + assert creator is None + + +def test_creator_of_no_created_by_ref(): + env = stix2.Environment(store=stix2.MemoryStore()) + ind = env.create(stix2.Indicator, **INDICATOR_KWARGS) + creator = env.creator_of(ind) + assert creator is None + + +def test_relationships(ds): + env = stix2.Environment(store=ds) + mal = env.get(MALWARE_ID) + resp = env.relationships(mal) + + assert len(resp) == 3 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[1] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_no_id(ds): + env = stix2.Environment(store=ds) + mal = { + "type": "malware", + "name": "some variant" + } + with pytest.raises(ValueError) as excinfo: + env.relationships(mal) + assert "object has no 'id' property" in str(excinfo.value) + + +def test_relationships_by_type(ds): + env = stix2.Environment(store=ds) + mal = env.get(MALWARE_ID) + resp = env.relationships(mal, relationship_type='indicates') + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[0] + + +def test_relationships_by_source(ds): + env = stix2.Environment(store=ds) + resp = env.relationships(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[1] + + +def test_relationships_by_target(ds): + env = stix2.Environment(store=ds) + resp = env.relationships(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_type(ds): + env = stix2.Environment(store=ds) + resp = env.relationships(MALWARE_ID, relationship_type='uses', target_only=True) + + assert len(resp) == 1 + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_source(ds): + env = stix2.Environment(store=ds) + with pytest.raises(ValueError) as excinfo: + env.relationships(MALWARE_ID, target_only=True, source_only=True) + + assert 'not both' in str(excinfo.value) + + +def test_related_to(ds): + env = stix2.Environment(store=ds) + mal = env.get(MALWARE_ID) + resp = env.related_to(mal) + + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_no_id(ds): + env = stix2.Environment(store=ds) + mal = { + "type": "malware", + "name": "some variant" + } + with pytest.raises(ValueError) as excinfo: + env.related_to(mal) + assert "object has no 'id' property" in str(excinfo.value) + + +def test_related_to_by_source(ds): + env = stix2.Environment(store=ds) + resp = env.related_to(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == IDENTITY_ID + + +def test_related_to_by_target(ds): + env = stix2.Environment(store=ds) + resp = env.related_to(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) diff --git a/stix2/test/v21/test_external_reference.py b/stix2/test/v21/test_external_reference.py new file mode 100644 index 0000000..9b90998 --- /dev/null +++ b/stix2/test/v21/test_external_reference.py @@ -0,0 +1,122 @@ +"""Tests for stix.ExternalReference""" + +import re + +import pytest + +import stix2 + +VERIS = """{ + "source_name": "veris", + "url": "https://github.com/vz-risk/VCDB/blob/master/data/json/0001AA7F-C601-424A-B2B8-BE6C9F5164E7.json", + "hashes": { + "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b" + }, + "external_id": "0001AA7F-C601-424A-B2B8-BE6C9F5164E7" +}""" + + +def test_external_reference_veris(): + ref = stix2.ExternalReference( + source_name="veris", + external_id="0001AA7F-C601-424A-B2B8-BE6C9F5164E7", + hashes={ + "SHA-256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b" + }, + url="https://github.com/vz-risk/VCDB/blob/master/data/json/0001AA7F-C601-424A-B2B8-BE6C9F5164E7.json", + ) + + assert str(ref) == VERIS + + +CAPEC = """{ + "source_name": "capec", + "external_id": "CAPEC-550" +}""" + + +def test_external_reference_capec(): + ref = stix2.ExternalReference( + source_name="capec", + external_id="CAPEC-550", + ) + + assert str(ref) == CAPEC + assert re.match("ExternalReference\\(source_name=u?'capec', external_id=u?'CAPEC-550'\\)", repr(ref)) + + +CAPEC_URL = """{ + "source_name": "capec", + "url": "http://capec.mitre.org/data/definitions/550.html", + "external_id": "CAPEC-550" +}""" + + +def test_external_reference_capec_url(): + ref = stix2.ExternalReference( + source_name="capec", + external_id="CAPEC-550", + url="http://capec.mitre.org/data/definitions/550.html", + ) + + assert str(ref) == CAPEC_URL + + +THREAT_REPORT = """{ + "source_name": "ACME Threat Intel", + "description": "Threat report", + "url": "http://www.example.com/threat-report.pdf" +}""" + + +def test_external_reference_threat_report(): + ref = stix2.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + url="http://www.example.com/threat-report.pdf", + ) + + assert str(ref) == THREAT_REPORT + + +BUGZILLA = """{ + "source_name": "ACME Bugzilla", + "url": "https://www.example.com/bugs/1370", + "external_id": "1370" +}""" + + +def test_external_reference_bugzilla(): + ref = stix2.ExternalReference( + source_name="ACME Bugzilla", + external_id="1370", + url="https://www.example.com/bugs/1370", + ) + + assert str(ref) == BUGZILLA + + +OFFLINE = """{ + "source_name": "ACME Threat Intel", + "description": "Threat report" +}""" + + +def test_external_reference_offline(): + ref = stix2.ExternalReference( + source_name="ACME Threat Intel", + description="Threat report", + ) + + assert str(ref) == OFFLINE + assert re.match("ExternalReference\\(source_name=u?'ACME Threat Intel', description=u?'Threat report'\\)", repr(ref)) + # Yikes! This works + assert eval("stix2." + repr(ref)) == ref + + +def test_external_reference_source_required(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.ExternalReference() + + assert excinfo.value.cls == stix2.ExternalReference + assert excinfo.value.properties == ["source_name"] diff --git a/stix2/test/v21/test_fixtures.py b/stix2/test/v21/test_fixtures.py new file mode 100644 index 0000000..83d5f85 --- /dev/null +++ b/stix2/test/v21/test_fixtures.py @@ -0,0 +1,18 @@ +import uuid + +from stix2 import utils + +from .constants import FAKE_TIME + + +def test_clock(clock): + assert utils.STIXdatetime.now() == FAKE_TIME + + +def test_my_uuid4_fixture(uuid4): + assert uuid.uuid4() == "00000000-0000-0000-0000-000000000001" + assert uuid.uuid4() == "00000000-0000-0000-0000-000000000002" + assert uuid.uuid4() == "00000000-0000-0000-0000-000000000003" + for _ in range(256): + uuid.uuid4() + assert uuid.uuid4() == "00000000-0000-0000-0000-000000000104" diff --git a/stix2/test/v21/test_granular_markings.py b/stix2/test/v21/test_granular_markings.py new file mode 100644 index 0000000..9e024a1 --- /dev/null +++ b/stix2/test/v21/test_granular_markings.py @@ -0,0 +1,1068 @@ + +import pytest + +from stix2 import TLP_RED, Malware, markings +from stix2.exceptions import MarkingNotFoundError + +from .constants import MALWARE_MORE_KWARGS as MALWARE_KWARGS_CONST +from .constants import MARKING_IDS + +"""Tests for the Data Markings API.""" + +MALWARE_KWARGS = MALWARE_KWARGS_CONST.copy() + + +def test_add_marking_mark_one_selector_multiple_refs(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1] + }, + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +@pytest.mark.parametrize("data", [ + ( + Malware(**MALWARE_KWARGS), + Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0] + }, + ], + **MALWARE_KWARGS), + MARKING_IDS[0], + ), + ( + MALWARE_KWARGS, + dict( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0] + }, + ], + **MALWARE_KWARGS), + MARKING_IDS[0], + ), + ( + Malware(**MALWARE_KWARGS), + Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": TLP_RED.id, + }, + ], + **MALWARE_KWARGS), + TLP_RED, + ), +]) +def test_add_marking_mark_multiple_selector_one_refs(data): + before = data[0] + after = data[1] + + before = markings.add_markings(before, data[2], ["description", "name"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_add_marking_mark_multiple_selector_multiple_refs(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[1] + } + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description", "name"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_add_marking_mark_another_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "name"], + "marking_ref": MARKING_IDS[0] + }, + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0]], ["name"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_add_marking_mark_same_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + ], + **MALWARE_KWARGS + ) + before = markings.add_markings(before, [MARKING_IDS[0]], ["description"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +@pytest.mark.parametrize("data,marking", [ + ({"description": "test description"}, + [["title"], ["marking-definition--1", "marking-definition--2"], + "", ["marking-definition--1", "marking-definition--2"], + [], ["marking-definition--1", "marking-definition--2"], + [""], ["marking-definition--1", "marking-definition--2"], + ["description"], [""], + ["description"], [], + ["description"], ["marking-definition--1", 456] + ]) +]) +def test_add_marking_bad_selector(data, marking): + with pytest.raises(AssertionError): + markings.add_markings(data, marking[0], marking[1]) + + +GET_MARKINGS_TEST_DATA = { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45 + } + ], + "x": { + "y": [ + "hello", + 88 + ], + "z": { + "foo1": "bar", + "foo2": 65 + } + }, + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"] + }, + { + "marking_ref": "2", + "selectors": ["c"] + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"] + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"] + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"] + }, + { + "marking_ref": "6", + "selectors": ["x"] + }, + { + "marking_ref": "7", + "selectors": ["x.y"] + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"] + }, + { + "marking_ref": "9", + "selectors": ["x.z"] + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"] + }, + ] +} + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_smoke(data): + """Test get_markings does not fail.""" + assert len(markings.get_markings(data, "a")) >= 1 + assert markings.get_markings(data, "a") == ["1"] + + +@pytest.mark.parametrize("data", [ + GET_MARKINGS_TEST_DATA, + {"b": 1234}, +]) +def test_get_markings_not_marked(data): + """Test selector that is not marked returns empty list.""" + results = markings.get_markings(data, "b") + assert len(results) == 0 + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_multiple_selectors(data): + """Test multiple selectors return combination of markings.""" + total = markings.get_markings(data, ["x.y", "x.z"]) + xy_markings = markings.get_markings(data, ["x.y"]) + xz_markings = markings.get_markings(data, ["x.z"]) + + assert set(xy_markings).issubset(total) + assert set(xz_markings).issubset(total) + assert set(xy_markings).union(xz_markings).issuperset(total) + + +@pytest.mark.parametrize("data,selector", [ + (GET_MARKINGS_TEST_DATA, "foo"), + (GET_MARKINGS_TEST_DATA, ""), + (GET_MARKINGS_TEST_DATA, []), + (GET_MARKINGS_TEST_DATA, [""]), + (GET_MARKINGS_TEST_DATA, "x.z.[-2]"), + (GET_MARKINGS_TEST_DATA, "c.f"), + (GET_MARKINGS_TEST_DATA, "c.[2].i"), + (GET_MARKINGS_TEST_DATA, "c.[3]"), + (GET_MARKINGS_TEST_DATA, "d"), + (GET_MARKINGS_TEST_DATA, "x.[0]"), + (GET_MARKINGS_TEST_DATA, "z.y.w"), + (GET_MARKINGS_TEST_DATA, "x.z.[1]"), + (GET_MARKINGS_TEST_DATA, "x.z.foo3") +]) +def test_get_markings_bad_selector(data, selector): + """Test bad selectors raise exception""" + with pytest.raises(AssertionError): + markings.get_markings(data, selector) + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_positional_arguments_combinations(data): + """Test multiple combinations for inherited and descendant markings.""" + assert set(markings.get_markings(data, "a", False, False)) == set(["1"]) + assert set(markings.get_markings(data, "a", True, False)) == set(["1"]) + assert set(markings.get_markings(data, "a", True, True)) == set(["1"]) + assert set(markings.get_markings(data, "a", False, True)) == set(["1"]) + + assert set(markings.get_markings(data, "b", False, False)) == set([]) + assert set(markings.get_markings(data, "b", True, False)) == set([]) + assert set(markings.get_markings(data, "b", True, True)) == set([]) + assert set(markings.get_markings(data, "b", False, True)) == set([]) + + assert set(markings.get_markings(data, "c", False, False)) == set(["2"]) + assert set(markings.get_markings(data, "c", True, False)) == set(["2"]) + assert set(markings.get_markings(data, "c", True, True)) == set(["2", "3", "4", "5"]) + assert set(markings.get_markings(data, "c", False, True)) == set(["2", "3", "4", "5"]) + + assert set(markings.get_markings(data, "c.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "c.[0]", True, False)) == set(["2"]) + assert set(markings.get_markings(data, "c.[0]", True, True)) == set(["2"]) + assert set(markings.get_markings(data, "c.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "c.[1]", False, False)) == set(["3"]) + assert set(markings.get_markings(data, "c.[1]", True, False)) == set(["2", "3"]) + assert set(markings.get_markings(data, "c.[1]", True, True)) == set(["2", "3"]) + assert set(markings.get_markings(data, "c.[1]", False, True)) == set(["3"]) + + assert set(markings.get_markings(data, "c.[2]", False, False)) == set(["4"]) + assert set(markings.get_markings(data, "c.[2]", True, False)) == set(["2", "4"]) + assert set(markings.get_markings(data, "c.[2]", True, True)) == set(["2", "4", "5"]) + assert set(markings.get_markings(data, "c.[2]", False, True)) == set(["4", "5"]) + + assert set(markings.get_markings(data, "c.[2].g", False, False)) == set(["5"]) + assert set(markings.get_markings(data, "c.[2].g", True, False)) == set(["2", "4", "5"]) + assert set(markings.get_markings(data, "c.[2].g", True, True)) == set(["2", "4", "5"]) + assert set(markings.get_markings(data, "c.[2].g", False, True)) == set(["5"]) + + assert set(markings.get_markings(data, "x", False, False)) == set(["6"]) + assert set(markings.get_markings(data, "x", True, False)) == set(["6"]) + assert set(markings.get_markings(data, "x", True, True)) == set(["6", "7", "8", "9", "10"]) + assert set(markings.get_markings(data, "x", False, True)) == set(["6", "7", "8", "9", "10"]) + + assert set(markings.get_markings(data, "x.y", False, False)) == set(["7"]) + assert set(markings.get_markings(data, "x.y", True, False)) == set(["6", "7"]) + assert set(markings.get_markings(data, "x.y", True, True)) == set(["6", "7", "8"]) + assert set(markings.get_markings(data, "x.y", False, True)) == set(["7", "8"]) + + assert set(markings.get_markings(data, "x.y.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "x.y.[0]", True, False)) == set(["6", "7"]) + assert set(markings.get_markings(data, "x.y.[0]", True, True)) == set(["6", "7"]) + assert set(markings.get_markings(data, "x.y.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.y.[1]", False, False)) == set(["8"]) + assert set(markings.get_markings(data, "x.y.[1]", True, False)) == set(["6", "7", "8"]) + assert set(markings.get_markings(data, "x.y.[1]", True, True)) == set(["6", "7", "8"]) + assert set(markings.get_markings(data, "x.y.[1]", False, True)) == set(["8"]) + + assert set(markings.get_markings(data, "x.z", False, False)) == set(["9"]) + assert set(markings.get_markings(data, "x.z", True, False)) == set(["6", "9"]) + assert set(markings.get_markings(data, "x.z", True, True)) == set(["6", "9", "10"]) + assert set(markings.get_markings(data, "x.z", False, True)) == set(["9", "10"]) + + assert set(markings.get_markings(data, "x.z.foo1", False, False)) == set([]) + assert set(markings.get_markings(data, "x.z.foo1", True, False)) == set(["6", "9"]) + assert set(markings.get_markings(data, "x.z.foo1", True, True)) == set(["6", "9"]) + assert set(markings.get_markings(data, "x.z.foo1", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.z.foo2", False, False)) == set(["10"]) + assert set(markings.get_markings(data, "x.z.foo2", True, False)) == set(["6", "9", "10"]) + assert set(markings.get_markings(data, "x.z.foo2", True, True)) == set(["6", "9", "10"]) + assert set(markings.get_markings(data, "x.z.foo2", False, True)) == set(["10"]) + + +@pytest.mark.parametrize("data", [ + ( + Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1] + }, + ], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[1]], + ), + ( + dict( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1] + }, + ], + **MALWARE_KWARGS + ), + [MARKING_IDS[0], MARKING_IDS[1]], + ), +]) +def test_remove_marking_remove_one_selector_with_multiple_refs(data): + before = markings.remove_markings(data[0], data[1], ["description"]) + assert "granular_markings" not in before + + +def test_remove_marking_remove_multiple_selector_one_ref(): + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, MARKING_IDS[0], ["description", "modified"]) + assert "granular_markings" not in before + + +def test_remove_marking_mark_one_selector_from_multiple_ones(): + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_remove_marking_mark_one_selector_markings_from_multiple_ones(): + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1] + } + ], + **MALWARE_KWARGS + ) + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1] + } + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_remove_marking_mark_mutilple_selector_multiple_refs(): + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1] + } + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description", "modified"]) + assert "granular_markings" not in before + + +def test_remove_marking_mark_another_property_same_marking(): + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["modified"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_remove_marking_mark_same_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = markings.remove_markings(before, [MARKING_IDS[0]], ["description"]) + assert "granular_markings" not in before + + +def test_remove_no_markings(): + before = { + "description": "test description", + } + after = markings.remove_markings(before, ["marking-definition--1"], ["description"]) + assert before == after + + +def test_remove_marking_bad_selector(): + before = { + "description": "test description", + } + with pytest.raises(AssertionError): + markings.remove_markings(before, ["marking-definition--1", "marking-definition--2"], ["title"]) + + +def test_remove_marking_not_present(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + with pytest.raises(MarkingNotFoundError): + markings.remove_markings(before, [MARKING_IDS[1]], ["description"]) + + +IS_MARKED_TEST_DATA = [ + Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1] + }, + { + "selectors": ["labels", "description"], + "marking_ref": MARKING_IDS[2] + }, + { + "selectors": ["labels", "description"], + "marking_ref": MARKING_IDS[3] + }, + ], + **MALWARE_KWARGS + ), + dict( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1] + }, + { + "selectors": ["labels", "description"], + "marking_ref": MARKING_IDS[2] + }, + { + "selectors": ["labels", "description"], + "marking_ref": MARKING_IDS[3] + }, + ], + **MALWARE_KWARGS + ), +] + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_smoke(data): + """Smoke test is_marked call does not fail.""" + assert markings.is_marked(data, selectors=["description"]) + assert markings.is_marked(data, selectors=["modified"]) is False + + +@pytest.mark.parametrize("data,selector", [ + (IS_MARKED_TEST_DATA[0], "foo"), + (IS_MARKED_TEST_DATA[0], ""), + (IS_MARKED_TEST_DATA[0], []), + (IS_MARKED_TEST_DATA[0], [""]), + (IS_MARKED_TEST_DATA[0], "x.z.[-2]"), + (IS_MARKED_TEST_DATA[0], "c.f"), + (IS_MARKED_TEST_DATA[0], "c.[2].i"), + (IS_MARKED_TEST_DATA[1], "c.[3]"), + (IS_MARKED_TEST_DATA[1], "d"), + (IS_MARKED_TEST_DATA[1], "x.[0]"), + (IS_MARKED_TEST_DATA[1], "z.y.w"), + (IS_MARKED_TEST_DATA[1], "x.z.[1]"), + (IS_MARKED_TEST_DATA[1], "x.z.foo3") +]) +def test_is_marked_invalid_selector(data, selector): + """Test invalid selector raises an error.""" + with pytest.raises(AssertionError): + markings.is_marked(data, selectors=selector) + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_mix_selector(data): + """Test valid selector, one marked and one not marked returns True.""" + assert markings.is_marked(data, selectors=["description", "labels"]) + assert markings.is_marked(data, selectors=["description"]) + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_valid_selector_no_refs(data): + """Test that a valid selector return True when it has marking refs and False when not.""" + assert markings.is_marked(data, selectors=["description"]) + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[3]], ["description"]) + assert markings.is_marked(data, [MARKING_IDS[2]], ["description"]) + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[5]], ["description"]) is False + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_valid_selector_and_refs(data): + """Test that a valid selector returns True when marking_refs match.""" + assert markings.is_marked(data, [MARKING_IDS[1]], ["description"]) + assert markings.is_marked(data, [MARKING_IDS[1]], ["modified"]) is False + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_valid_selector_multiple_refs(data): + """Test that a valid selector returns True if aall marking_refs match. + Otherwise False.""" + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[3]], ["labels"]) + assert markings.is_marked(data, [MARKING_IDS[2], MARKING_IDS[1]], ["labels"]) is False + assert markings.is_marked(data, MARKING_IDS[2], ["labels"]) + assert markings.is_marked(data, ["marking-definition--1234"], ["labels"]) is False + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_no_marking_refs(data): + """Test that a valid content selector with no marking_refs returns True + if there is a granular_marking that asserts that field, False + otherwise.""" + assert markings.is_marked(data, selectors=["type"]) is False + assert markings.is_marked(data, selectors=["labels"]) + + +@pytest.mark.parametrize("data", IS_MARKED_TEST_DATA) +def test_is_marked_no_selectors(data): + """Test that we're ensuring 'selectors' is provided.""" + with pytest.raises(TypeError) as excinfo: + markings.granular_markings.is_marked(data) + assert "'selectors' must be provided" in str(excinfo.value) + + +def test_is_marked_positional_arguments_combinations(): + """Test multiple combinations for inherited and descendant markings.""" + test_sdo = \ + { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45 + } + ], + "x": { + "y": [ + "hello", + 88 + ], + "z": { + "foo1": "bar", + "foo2": 65 + } + }, + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"] + }, + { + "marking_ref": "2", + "selectors": ["c"] + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"] + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"] + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"] + }, + { + "marking_ref": "6", + "selectors": ["x"] + }, + { + "marking_ref": "7", + "selectors": ["x.y"] + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"] + }, + { + "marking_ref": "9", + "selectors": ["x.z"] + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"] + }, + ] + } + + assert markings.is_marked(test_sdo, ["1"], "a", False, False) + assert markings.is_marked(test_sdo, ["1"], "a", True, False) + assert markings.is_marked(test_sdo, ["1"], "a", True, True) + assert markings.is_marked(test_sdo, ["1"], "a", False, True) + + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, "b", inherited=True, descendants=False) is False + assert markings.is_marked(test_sdo, "b", inherited=True, descendants=True) is False + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["2"], "c", False, False) + assert markings.is_marked(test_sdo, ["2"], "c", True, False) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5"], "c", True, True) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5"], "c", False, True) + + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["2"], "c.[0]", True, False) + assert markings.is_marked(test_sdo, ["2"], "c.[0]", True, True) + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, False) + assert markings.is_marked(test_sdo, ["2", "3"], "c.[1]", True, False) + assert markings.is_marked(test_sdo, ["2", "3"], "c.[1]", True, True) + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, True) + + assert markings.is_marked(test_sdo, ["4"], "c.[2]", False, False) + assert markings.is_marked(test_sdo, ["2", "4"], "c.[2]", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5"], "c.[2]", True, True) + assert markings.is_marked(test_sdo, ["4", "5"], "c.[2]", False, True) + + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, False) + assert markings.is_marked(test_sdo, ["2", "4", "5"], "c.[2].g", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5"], "c.[2].g", True, True) + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, True) + + assert markings.is_marked(test_sdo, ["6"], "x", False, False) + assert markings.is_marked(test_sdo, ["6"], "x", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10"], "x", True, True) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10"], "x", False, True) + + assert markings.is_marked(test_sdo, ["7"], "x.y", False, False) + assert markings.is_marked(test_sdo, ["6", "7"], "x.y", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8"], "x.y", True, True) + assert markings.is_marked(test_sdo, ["7", "8"], "x.y", False, True) + + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "7"], "x.y.[0]", True, False) + assert markings.is_marked(test_sdo, ["6", "7"], "x.y.[0]", True, True) + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, False) + assert markings.is_marked(test_sdo, ["6", "7", "8"], "x.y.[1]", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8"], "x.y.[1]", True, True) + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, True) + + assert markings.is_marked(test_sdo, ["9"], "x.z", False, False) + assert markings.is_marked(test_sdo, ["6", "9"], "x.z", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10"], "x.z", True, True) + assert markings.is_marked(test_sdo, ["9", "10"], "x.z", False, True) + + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "9"], "x.z.foo1", True, False) + assert markings.is_marked(test_sdo, ["6", "9"], "x.z.foo1", True, True) + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, False) + assert markings.is_marked(test_sdo, ["6", "9", "10"], "x.z.foo2", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10"], "x.z.foo2", True, True) + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, True) + + +def test_create_sdo_with_invalid_marking(): + with pytest.raises(AssertionError) as excinfo: + Malware( + granular_markings=[ + { + "selectors": ["foo"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + assert str(excinfo.value) == "Selector foo in Malware is not valid!" + + +def test_set_marking_mark_one_selector_multiple_refs(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1] + } + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_set_marking_mark_multiple_selector_one_refs(): + before = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1] + } + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0]], ["description", "modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_set_marking_mark_multiple_selector_multiple_refs_from_none(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["description", "modified"], + "marking_ref": MARKING_IDS[1] + } + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], ["description", "modified"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +def test_set_marking_mark_another_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[1] + }, + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[2] + } + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[1], MARKING_IDS[2]], ["description"]) + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +@pytest.mark.parametrize("marking", [ + ([MARKING_IDS[4], MARKING_IDS[5]], ["foo"]), + ([MARKING_IDS[4], MARKING_IDS[5]], ""), + ([MARKING_IDS[4], MARKING_IDS[5]], []), + ([MARKING_IDS[4], MARKING_IDS[5]], [""]), +]) +def test_set_marking_bad_selector(marking): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + + with pytest.raises(AssertionError): + before = markings.set_markings(before, marking[0], marking[1]) + + assert before == after + + +def test_set_marking_mark_same_property_same_marking(): + before = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + after = Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + } + ], + **MALWARE_KWARGS + ) + before = markings.set_markings(before, [MARKING_IDS[0]], ["description"]) + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + +CLEAR_MARKINGS_TEST_DATA = [ + Malware( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["modified", "description"], + "marking_ref": MARKING_IDS[1] + }, + { + "selectors": ["modified", "description", "type"], + "marking_ref": MARKING_IDS[2] + }, + ], + **MALWARE_KWARGS + ), + dict( + granular_markings=[ + { + "selectors": ["description"], + "marking_ref": MARKING_IDS[0] + }, + { + "selectors": ["modified", "description"], + "marking_ref": MARKING_IDS[1] + }, + { + "selectors": ["modified", "description", "type"], + "marking_ref": MARKING_IDS[2] + }, + ], + **MALWARE_KWARGS + ) +] + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_smoke(data): + """Test clear_marking call does not fail.""" + data = markings.clear_markings(data, "modified") + assert markings.is_marked(data, "modified") is False + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_multiple_selectors(data): + """Test clearing markings for multiple selectors effectively removes associated markings.""" + data = markings.clear_markings(data, ["type", "description"]) + assert markings.is_marked(data, ["type", "description"]) is False + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_one_selector(data): + """Test markings associated with one selector were removed.""" + data = markings.clear_markings(data, "description") + assert markings.is_marked(data, "description") is False + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_all_selectors(data): + data = markings.clear_markings(data, ["description", "type", "modified"]) + assert markings.is_marked(data, "description") is False + assert "granular_markings" not in data + + +@pytest.mark.parametrize("data,selector", [ + (CLEAR_MARKINGS_TEST_DATA[0], "foo"), + (CLEAR_MARKINGS_TEST_DATA[0], ""), + (CLEAR_MARKINGS_TEST_DATA[1], []), + (CLEAR_MARKINGS_TEST_DATA[1], [""]), +]) +def test_clear_marking_bad_selector(data, selector): + """Test bad selector raises exception.""" + with pytest.raises(AssertionError): + markings.clear_markings(data, selector) + + +@pytest.mark.parametrize("data", CLEAR_MARKINGS_TEST_DATA) +def test_clear_marking_not_present(data): + """Test clearing markings for a selector that has no associated markings.""" + with pytest.raises(MarkingNotFoundError): + data = markings.clear_markings(data, ["labels"]) diff --git a/stix2/test/v21/test_identity.py b/stix2/test/v21/test_identity.py new file mode 100644 index 0000000..280dc19 --- /dev/null +++ b/stix2/test/v21/test_identity.py @@ -0,0 +1,77 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import IDENTITY_ID + +EXPECTED = """{ + "type": "identity", + "spec_version": "2.1", + "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.000Z", + "name": "John Smith", + "identity_class": "individual" +}""" + + +def test_identity_example(): + identity = stix2.Identity( + id="identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + created="2015-12-21T19:59:11.000Z", + modified="2015-12-21T19:59:11.000Z", + name="John Smith", + identity_class="individual", + ) + + assert str(identity) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "created": "2015-12-21T19:59:11.000Z", + "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + "identity_class": "individual", + "modified": "2015-12-21T19:59:11.000Z", + "name": "John Smith", + "spec_version": "2.1", + "type": "identity" + }, +]) +def test_parse_identity(data): + identity = stix2.parse(data) + + assert identity.type == 'identity' + assert identity.id == IDENTITY_ID + assert identity.created == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert identity.modified == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert identity.name == "John Smith" + + +def test_parse_no_type(): + with pytest.raises(stix2.exceptions.ParseError): + stix2.parse(""" + { + "id": "identity--311b2d2d-f010-5473-83ec-1edf84858f4c", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.000Z", + "name": "John Smith", + "identity_class": "individual" + }""") + + +def test_identity_with_custom(): + identity = stix2.Identity( + name="John Smith", + identity_class="individual", + custom_properties={'x_foo': 'bar'} + ) + + assert identity.x_foo == "bar" + assert "x_foo" in identity.object_properties() + +# TODO: Add other examples diff --git a/stix2/test/v21/test_indicator.py b/stix2/test/v21/test_indicator.py new file mode 100644 index 0000000..2864ef3 --- /dev/null +++ b/stix2/test/v21/test_indicator.py @@ -0,0 +1,196 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import FAKE_TIME, INDICATOR_ID, INDICATOR_KWARGS + +EXPECTED_INDICATOR = """{ + "type": "indicator", + "spec_version": "2.1", + "id": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "created": "2017-01-01T00:00:01.000Z", + "modified": "2017-01-01T00:00:01.000Z", + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "1970-01-01T00:00:01Z", + "labels": [ + "malicious-activity" + ] +}""" + +EXPECTED_INDICATOR_REPR = "Indicator(" + " ".join(""" + type='indicator', + spec_version='2.1', + id='indicator--01234567-89ab-cdef-0123-456789abcdef', + created='2017-01-01T00:00:01.000Z', + modified='2017-01-01T00:00:01.000Z', + pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + valid_from='1970-01-01T00:00:01Z', + labels=['malicious-activity'] +""".split()) + ")" + + +def test_indicator_with_all_required_properties(): + now = dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + epoch = dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + + ind = stix2.Indicator( + type="indicator", + id=INDICATOR_ID, + created=now, + modified=now, + pattern="[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + valid_from=epoch, + labels=['malicious-activity'], + ) + + assert ind.revoked is False + assert str(ind) == EXPECTED_INDICATOR + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(ind)) + assert rep == EXPECTED_INDICATOR_REPR + + +def test_indicator_autogenerated_properties(indicator): + assert indicator.type == 'indicator' + assert indicator.id == 'indicator--00000000-0000-0000-0000-000000000001' + assert indicator.created == FAKE_TIME + assert indicator.modified == FAKE_TIME + assert indicator.labels == ['malicious-activity'] + assert indicator.pattern == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']" + assert indicator.valid_from == FAKE_TIME + + assert indicator['type'] == 'indicator' + assert indicator['id'] == 'indicator--00000000-0000-0000-0000-000000000001' + assert indicator['created'] == FAKE_TIME + assert indicator['modified'] == FAKE_TIME + assert indicator['labels'] == ['malicious-activity'] + assert indicator['pattern'] == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']" + assert indicator['valid_from'] == FAKE_TIME + + +def test_indicator_type_must_be_indicator(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Indicator(type='xxx', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.prop_name == "type" + assert excinfo.value.reason == "must equal 'indicator'." + assert str(excinfo.value) == "Invalid value for Indicator 'type': must equal 'indicator'." + + +def test_indicator_id_must_start_with_indicator(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Indicator(id='my-prefix--', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.prop_name == "id" + assert excinfo.value.reason == "must start with 'indicator--'." + assert str(excinfo.value) == "Invalid value for Indicator 'id': must start with 'indicator--'." + + +def test_indicator_required_properties(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.Indicator() + + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.properties == ["labels", "pattern"] + assert str(excinfo.value) == "No values for required properties for Indicator: (labels, pattern)." + + +def test_indicator_required_property_pattern(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.Indicator(labels=['malicious-activity']) + + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.properties == ["pattern"] + + +def test_indicator_created_ref_invalid_format(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Indicator(created_by_ref='myprefix--12345678', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.prop_name == "created_by_ref" + assert excinfo.value.reason == "must start with 'identity'." + assert str(excinfo.value) == "Invalid value for Indicator 'created_by_ref': must start with 'identity'." + + +def test_indicator_revoked_invalid(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Indicator(revoked='no', **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.prop_name == "revoked" + assert excinfo.value.reason == "must be a boolean value." + + +def test_cannot_assign_to_indicator_attributes(indicator): + with pytest.raises(stix2.exceptions.ImmutableError) as excinfo: + indicator.valid_from = dt.datetime.now() + + assert str(excinfo.value) == "Cannot modify 'valid_from' property in 'Indicator' after creation." + + +def test_invalid_kwarg_to_indicator(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.Indicator(my_custom_property="foo", **INDICATOR_KWARGS) + + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Indicator: (my_custom_property)." + + +def test_created_modified_time_are_identical_by_default(): + """By default, the created and modified times should be the same.""" + ind = stix2.Indicator(**INDICATOR_KWARGS) + + assert ind.created == ind.modified + + +@pytest.mark.parametrize("data", [ + EXPECTED_INDICATOR, + { + "type": "indicator", + "id": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "created": "2017-01-01T00:00:01Z", + "modified": "2017-01-01T00:00:01Z", + "labels": [ + "malicious-activity" + ], + "pattern": "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']", + "valid_from": "1970-01-01T00:00:01Z" + }, +]) +def test_parse_indicator(data): + idctr = stix2.parse(data) + + assert idctr.type == 'indicator' + assert idctr.id == INDICATOR_ID + assert idctr.created == dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + assert idctr.modified == dt.datetime(2017, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + assert idctr.valid_from == dt.datetime(1970, 1, 1, 0, 0, 1, tzinfo=pytz.utc) + assert idctr.labels[0] == "malicious-activity" + assert idctr.pattern == "[file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e']" + + +def test_invalid_indicator_pattern(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Indicator( + labels=['malicious-activity'], + pattern="file:hashes.MD5 = 'd41d8cd98f00b204e9800998ecf8427e'", + ) + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.prop_name == 'pattern' + assert 'input is missing square brackets' in excinfo.value.reason + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Indicator( + labels=['malicious-activity'], + pattern='[file:hashes.MD5 = "d41d8cd98f00b204e9800998ecf8427e"]', + ) + assert excinfo.value.cls == stix2.Indicator + assert excinfo.value.prop_name == 'pattern' + assert 'mismatched input' in excinfo.value.reason diff --git a/stix2/test/v21/test_intrusion_set.py b/stix2/test/v21/test_intrusion_set.py new file mode 100644 index 0000000..1657da0 --- /dev/null +++ b/stix2/test/v21/test_intrusion_set.py @@ -0,0 +1,78 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import INTRUSION_SET_ID + +EXPECTED = """{ + "type": "intrusion-set", + "spec_version": "2.1", + "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:48.000Z", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Bobcat Breakin", + "description": "Incidents usually feature a shared TTP of a bobcat being released...", + "aliases": [ + "Zookeeper" + ], + "goals": [ + "acquisition-theft", + "harassment", + "damage" + ] +}""" + + +def test_intrusion_set_example(): + intrusion_set = stix2.IntrusionSet( + id="intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", + created_by_ref="identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + created="2016-04-06T20:03:48.000Z", + modified="2016-04-06T20:03:48.000Z", + name="Bobcat Breakin", + description="Incidents usually feature a shared TTP of a bobcat being released...", + aliases=["Zookeeper"], + goals=["acquisition-theft", "harassment", "damage"] + ) + + assert str(intrusion_set) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "aliases": [ + "Zookeeper" + ], + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "Incidents usually feature a shared TTP of a bobcat being released...", + "goals": [ + "acquisition-theft", + "harassment", + "damage" + ], + "id": "intrusion-set--4e78f46f-a023-4e5f-bc24-71b3ca22ec29", + "modified": "2016-04-06T20:03:48.000Z", + "name": "Bobcat Breakin", + "spec_version": "2.1", + "type": "intrusion-set" + }, +]) +def test_parse_intrusion_set(data): + intset = stix2.parse(data) + + assert intset.type == "intrusion-set" + assert intset.id == INTRUSION_SET_ID + assert intset.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert intset.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert intset.goals == ["acquisition-theft", "harassment", "damage"] + assert intset.aliases == ["Zookeeper"] + assert intset.description == "Incidents usually feature a shared TTP of a bobcat being released..." + assert intset.name == "Bobcat Breakin" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_kill_chain_phases.py b/stix2/test/v21/test_kill_chain_phases.py new file mode 100644 index 0000000..220c714 --- /dev/null +++ b/stix2/test/v21/test_kill_chain_phases.py @@ -0,0 +1,61 @@ +"""Tests for stix.ExternalReference""" + +import pytest + +import stix2 + +LMCO_RECON = """{ + "kill_chain_name": "lockheed-martin-cyber-kill-chain", + "phase_name": "reconnaissance" +}""" + + +def test_lockheed_martin_cyber_kill_chain(): + recon = stix2.KillChainPhase( + kill_chain_name="lockheed-martin-cyber-kill-chain", + phase_name="reconnaissance", + ) + + assert str(recon) == LMCO_RECON + + +FOO_PRE_ATTACK = """{ + "kill_chain_name": "foo", + "phase_name": "pre-attack" +}""" + + +def test_kill_chain_example(): + preattack = stix2.KillChainPhase( + kill_chain_name="foo", + phase_name="pre-attack", + ) + + assert str(preattack) == FOO_PRE_ATTACK + + +def test_kill_chain_required_properties(): + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.KillChainPhase() + + assert excinfo.value.cls == stix2.KillChainPhase + assert excinfo.value.properties == ["kill_chain_name", "phase_name"] + + +def test_kill_chain_required_property_chain_name(): + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.KillChainPhase(phase_name="weaponization") + + assert excinfo.value.cls == stix2.KillChainPhase + assert excinfo.value.properties == ["kill_chain_name"] + + +def test_kill_chain_required_property_phase_name(): + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.KillChainPhase(kill_chain_name="lockheed-martin-cyber-kill-chain") + + assert excinfo.value.cls == stix2.KillChainPhase + assert excinfo.value.properties == ["phase_name"] diff --git a/stix2/test/test_language_content.py b/stix2/test/v21/test_language_content.py similarity index 85% rename from stix2/test/test_language_content.py rename to stix2/test/v21/test_language_content.py index d67abd8..f38a16b 100644 --- a/stix2/test/test_language_content.py +++ b/stix2/test/v21/test_language_content.py @@ -12,6 +12,7 @@ LANGUAGE_CONTENT_ID = "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d" TEST_CAMPAIGN = """{ "type": "campaign", + "spec_version": "2.1", "id": "campaign--12a111f0-b824-4baf-a224-83b80237a094", "lang": "en", "created": "2017-02-08T21:31:22.007Z", @@ -22,6 +23,7 @@ TEST_CAMPAIGN = """{ TEST_LANGUAGE_CONTENT = u"""{ "type": "language-content", + "spec_version": "2.1", "id": "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d", "created": "2017-02-08T21:31:22.007Z", "modified": "2017-02-08T21:31:22.007Z", @@ -51,13 +53,13 @@ def test_language_content_campaign(): object_ref=CAMPAIGN_ID, object_modified=now, contents={ - "de": { - "name": "Bank Angriff 1", - "description": "Weitere Informationen über Banküberfall" + 'de': { + 'name': 'Bank Angriff 1', + 'description': 'Weitere Informationen über Banküberfall' }, - "fr": { - "name": "Attaque Bank 1", - "description": "Plus d'informations sur la crise bancaire" + 'fr': { + 'name': 'Attaque Bank 1', + 'description': 'Plus d\'informations sur la crise bancaire' } } ) diff --git a/stix2/test/test_location.py b/stix2/test/v21/test_location.py similarity index 100% rename from stix2/test/test_location.py rename to stix2/test/v21/test_location.py diff --git a/stix2/test/v21/test_malware.py b/stix2/test/v21/test_malware.py new file mode 100644 index 0000000..cf14c19 --- /dev/null +++ b/stix2/test/v21/test_malware.py @@ -0,0 +1,166 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import FAKE_TIME, MALWARE_ID, MALWARE_KWARGS + +EXPECTED_MALWARE = """{ + "type": "malware", + "spec_version": "2.1", + "id": "malware--fedcba98-7654-3210-fedc-ba9876543210", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "name": "Cryptolocker", + "labels": [ + "ransomware" + ], + "is_family": false +}""" + + +def test_malware_with_all_required_properties(): + now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + + mal = stix2.Malware( + type="malware", + id=MALWARE_ID, + created=now, + modified=now, + labels=["ransomware"], + name="Cryptolocker", + is_family=False + ) + + assert str(mal) == EXPECTED_MALWARE + + +def test_malware_autogenerated_properties(malware): + assert malware.type == 'malware' + assert malware.id == 'malware--00000000-0000-0000-0000-000000000001' + assert malware.created == FAKE_TIME + assert malware.modified == FAKE_TIME + assert malware.labels == ['ransomware'] + assert malware.name == "Cryptolocker" + + assert malware['type'] == 'malware' + assert malware['id'] == 'malware--00000000-0000-0000-0000-000000000001' + assert malware['created'] == FAKE_TIME + assert malware['modified'] == FAKE_TIME + assert malware['labels'] == ['ransomware'] + assert malware['name'] == "Cryptolocker" + + +def test_malware_type_must_be_malware(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Malware(type='xxx', **MALWARE_KWARGS) + + assert excinfo.value.cls == stix2.Malware + assert excinfo.value.prop_name == "type" + assert excinfo.value.reason == "must equal 'malware'." + assert str(excinfo.value) == "Invalid value for Malware 'type': must equal 'malware'." + + +def test_malware_id_must_start_with_malware(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Malware(id='my-prefix--', **MALWARE_KWARGS) + + assert excinfo.value.cls == stix2.Malware + assert excinfo.value.prop_name == "id" + assert excinfo.value.reason == "must start with 'malware--'." + assert str(excinfo.value) == "Invalid value for Malware 'id': must start with 'malware--'." + + +def test_malware_required_properties(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.Malware() + + assert excinfo.value.cls == stix2.Malware + assert excinfo.value.properties == ["is_family", "labels", "name"] + + +def test_malware_required_property_name(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.Malware(labels=['ransomware'], is_family=False) + + assert excinfo.value.cls == stix2.Malware + assert excinfo.value.properties == ["name"] + + +def test_cannot_assign_to_malware_attributes(malware): + with pytest.raises(stix2.exceptions.ImmutableError) as excinfo: + malware.name = "Cryptolocker II" + + assert str(excinfo.value) == "Cannot modify 'name' property in 'Malware' after creation." + + +def test_invalid_kwarg_to_malware(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.Malware(my_custom_property="foo", **MALWARE_KWARGS) + + assert excinfo.value.cls == stix2.Malware + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Malware: (my_custom_property)." + + +@pytest.mark.parametrize("data", [ + EXPECTED_MALWARE, + { + "type": "malware", + "spec_version": "2.1", + "id": "malware--fedcba98-7654-3210-fedc-ba9876543210", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "labels": ["ransomware"], + "name": "Cryptolocker", + "is_family": False + }, +]) +def test_parse_malware(data): + mal = stix2.parse(data) + + assert mal.type == 'malware' + assert mal.id == MALWARE_ID + assert mal.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert mal.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert mal.labels == ['ransomware'] + assert mal.name == "Cryptolocker" + + +def test_parse_malware_invalid_labels(): + data = re.compile('\\[.+\\]', re.DOTALL).sub('1', EXPECTED_MALWARE) + with pytest.raises(ValueError) as excinfo: + stix2.parse(data) + assert "Invalid value for Malware 'labels'" in str(excinfo.value) + + +def test_parse_malware_kill_chain_phases(): + kill_chain = """ + "kill_chain_phases": [ + { + "kill_chain_name": "lockheed-martin-cyber-kill-chain", + "phase_name": "reconnaissance" + } + ]""" + data = EXPECTED_MALWARE.replace('malware"', 'malware",%s' % kill_chain) + mal = stix2.parse(data) + assert mal.kill_chain_phases[0].kill_chain_name == "lockheed-martin-cyber-kill-chain" + assert mal.kill_chain_phases[0].phase_name == "reconnaissance" + assert mal['kill_chain_phases'][0]['kill_chain_name'] == "lockheed-martin-cyber-kill-chain" + assert mal['kill_chain_phases'][0]['phase_name'] == "reconnaissance" + + +def test_parse_malware_clean_kill_chain_phases(): + kill_chain = """ + "kill_chain_phases": [ + { + "kill_chain_name": "lockheed-martin-cyber-kill-chain", + "phase_name": 1 + } + ]""" + data = EXPECTED_MALWARE.replace('2.1"', '2.1",%s' % kill_chain) + mal = stix2.parse(data) + assert mal['kill_chain_phases'][0]['phase_name'] == "1" diff --git a/stix2/test/v21/test_markings.py b/stix2/test/v21/test_markings.py new file mode 100644 index 0000000..71143fb --- /dev/null +++ b/stix2/test/v21/test_markings.py @@ -0,0 +1,265 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 +from stix2 import TLP_WHITE + +from .constants import MARKING_DEFINITION_ID + +EXPECTED_TLP_MARKING_DEFINITION = """{ + "type": "marking-definition", + "spec_version": "2.1", + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "created": "2017-01-20T00:00:00Z", + "definition_type": "tlp", + "definition": { + "tlp": "white" + } +}""" + +EXPECTED_STATEMENT_MARKING_DEFINITION = """{ + "type": "marking-definition", + "spec_version": "2.1", + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "created": "2017-01-20T00:00:00Z", + "definition_type": "statement", + "definition": { + "statement": "Copyright 2016, Example Corp" + } +}""" + +EXPECTED_CAMPAIGN_WITH_OBJECT_MARKING = """{ + "type": "campaign", + "spec_version": "2.1", + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "name": "Green Group Attacks Against Finance", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "object_marking_refs": [ + "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" + ] +}""" + +EXPECTED_GRANULAR_MARKING = """{ + "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "selectors": [ + "abc", + "abc.[23]", + "abc.def", + "abc.[2].efg" + ] +}""" + +EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS = """{ + "type": "campaign", + "spec_version": "2.1", + "id": "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "name": "Green Group Attacks Against Finance", + "description": "Campaign by Green Group against a series of targets in the financial services sector.", + "granular_markings": [ + { + "marking_ref": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "selectors": [ + "description" + ] + } + ] +}""" + + +def test_marking_def_example_with_tlp(): + assert str(TLP_WHITE) == EXPECTED_TLP_MARKING_DEFINITION + + +def test_marking_def_example_with_statement_positional_argument(): + marking_definition = stix2.MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="statement", + definition=stix2.StatementMarking(statement="Copyright 2016, Example Corp") + ) + + assert str(marking_definition) == EXPECTED_STATEMENT_MARKING_DEFINITION + + +def test_marking_def_example_with_kwargs_statement(): + kwargs = dict(statement="Copyright 2016, Example Corp") + marking_definition = stix2.MarkingDefinition( + id="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + created="2017-01-20T00:00:00.000Z", + definition_type="statement", + definition=stix2.StatementMarking(**kwargs) + ) + + 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-definition-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", + selectors=["abc", "abc.[23]", "abc.def", "abc.[2].efg"] + ) + + assert str(granular_marking) == EXPECTED_GRANULAR_MARKING + + +def test_granular_example_with_bad_selector(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.GranularMarking( + marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + selectors=["abc[0]"] # missing "." + ) + + assert excinfo.value.cls == stix2.GranularMarking + assert excinfo.value.prop_name == "selectors" + assert excinfo.value.reason == "must adhere to selector syntax." + assert str(excinfo.value) == "Invalid value for GranularMarking 'selectors': must adhere to selector syntax." + + +def test_campaign_with_granular_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.", + granular_markings=[ + stix2.GranularMarking( + marking_ref="marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + selectors=["description"]) + ]) + assert str(campaign) == EXPECTED_CAMPAIGN_WITH_GRANULAR_MARKINGS + + +@pytest.mark.parametrize("data", [ + EXPECTED_TLP_MARKING_DEFINITION, + { + "id": "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9", + "type": "marking-definition", + "created": "2017-01-20T00:00:00Z", + "definition": { + "tlp": "white" + }, + "definition_type": "tlp", + }, +]) +def test_parse_marking_definition(data): + gm = stix2.parse(data) + + assert gm.type == 'marking-definition' + assert gm.id == MARKING_DEFINITION_ID + assert gm.created == dt.datetime(2017, 1, 20, 0, 0, 0, tzinfo=pytz.utc) + assert gm.definition.tlp == "white" + assert gm.definition_type == "tlp" + + +@stix2.common.CustomMarking('x-new-marking-type', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), +]) +class NewMarking(object): + def __init__(self, property2=None, **kwargs): + if "property3" in kwargs and not isinstance(kwargs.get("property3"), int): + raise TypeError("Must be integer!") + + +def test_registered_custom_marking(): + nm = NewMarking(property1='something', property2=55) + + marking_def = stix2.MarkingDefinition( + id="marking-definition--00000000-0000-0000-0000-000000000012", + created="2017-01-22T00:00:00.000Z", + definition_type="x-new-marking-type", + definition=nm + ) + + assert marking_def.type == "marking-definition" + assert marking_def.id == "marking-definition--00000000-0000-0000-0000-000000000012" + assert marking_def.created == dt.datetime(2017, 1, 22, 0, 0, 0, tzinfo=pytz.utc) + assert marking_def.definition.property1 == "something" + assert marking_def.definition.property2 == 55 + assert marking_def.definition_type == "x-new-marking-type" + + +def test_registered_custom_marking_raises_exception(): + with pytest.raises(TypeError) as excinfo: + NewMarking(property1='something', property3='something', allow_custom=True) + + assert str(excinfo.value) == "Must be integer!" + + +def test_not_registered_marking_raises_exception(): + with pytest.raises(ValueError) as excinfo: + # Used custom object on purpose to demonstrate a not-registered marking + @stix2.sdo.CustomObject('x-new-marking-type2', [ + ('property1', stix2.properties.StringProperty(required=True)), + ('property2', stix2.properties.IntegerProperty()), + ]) + class NewObject2(object): + def __init__(self, property2=None, **kwargs): + return + + no = NewObject2(property1='something', property2=55) + + stix2.MarkingDefinition( + id="marking-definition--00000000-0000-0000-0000-000000000012", + created="2017-01-22T00:00:00.000Z", + definition_type="x-new-marking-type2", + definition=no + ) + + assert str(excinfo.value) == "definition_type must be a valid marking type" + + +def test_marking_wrong_type_construction(): + with pytest.raises(ValueError) as excinfo: + # Test passing wrong type for properties. + @stix2.CustomMarking('x-new-marking-type2', ("a", "b")) + class NewObject3(object): + pass + + assert str(excinfo.value) == "Must supply a list, containing tuples. For example, [('property1', IntegerProperty())]" + + +def test_campaign_add_markings(): + 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.", + ) + campaign = campaign.add_markings(TLP_WHITE) + assert campaign.object_marking_refs[0] == TLP_WHITE.id diff --git a/stix2/test/v21/test_memory.py b/stix2/test/v21/test_memory.py new file mode 100644 index 0000000..284c43e --- /dev/null +++ b/stix2/test/v21/test_memory.py @@ -0,0 +1,341 @@ +import os +import shutil + +import pytest + +from stix2 import (Bundle, Campaign, CustomObject, Filter, Identity, Indicator, + Malware, MemorySource, MemoryStore, Relationship, + properties) +from stix2.datastore import make_id + +from .constants import (CAMPAIGN_ID, CAMPAIGN_KWARGS, IDENTITY_ID, + IDENTITY_KWARGS, INDICATOR_ID, INDICATOR_KWARGS, + MALWARE_ID, MALWARE_KWARGS, RELATIONSHIP_IDS) + +IND1 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} +IND2 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} +IND3 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.936Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} +IND4 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} +IND5 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} +IND6 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-31T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} +IND7 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} +IND8 = { + "created": "2017-01-27T13:49:53.935Z", + "id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f", + "labels": [ + "url-watchlist" + ], + "modified": "2017-01-27T13:49:53.935Z", + "name": "Malicious site hosting downloader", + "pattern": "[url:value = 'http://x4z9arb.cn/4712']", + "type": "indicator", + "valid_from": "2017-01-27T13:49:53.935382Z" +} + +STIX_OBJS2 = [IND6, IND7, IND8] +STIX_OBJS1 = [IND1, IND2, IND3, IND4, IND5] + + +@pytest.fixture +def mem_store(): + yield MemoryStore(STIX_OBJS1) + + +@pytest.fixture +def mem_source(): + yield MemorySource(STIX_OBJS1) + + +@pytest.fixture +def rel_mem_store(): + cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + idy = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + ind = Indicator(id=INDICATOR_ID, **INDICATOR_KWARGS) + mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) + rel1 = Relationship(ind, 'indicates', mal, id=RELATIONSHIP_IDS[0]) + rel2 = Relationship(mal, 'targets', idy, id=RELATIONSHIP_IDS[1]) + rel3 = Relationship(cam, 'uses', mal, id=RELATIONSHIP_IDS[2]) + stix_objs = [cam, idy, ind, mal, rel1, rel2, rel3] + yield MemoryStore(stix_objs) + + +@pytest.fixture +def fs_mem_store(request, mem_store): + filename = 'memory_test/mem_store.json' + mem_store.save_to_file(filename) + + def fin(): + # teardown, excecuted regardless of exception + shutil.rmtree(os.path.dirname(filename)) + request.addfinalizer(fin) + + return filename + + +def test_memory_source_get(mem_source): + resp = mem_source.get("indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f") + assert resp["id"] == "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f" + + +def test_memory_source_get_nonexistant_object(mem_source): + resp = mem_source.get("tool--d81f86b8-975b-bc0b-775e-810c5ad45a4f") + assert resp is None + + +def test_memory_store_all_versions(mem_store): + # Add bundle of items to sink + mem_store.add(dict(id="bundle--%s" % make_id(), + objects=STIX_OBJS2, + spec_version="2.0", + type="bundle")) + + resp = mem_store.all_versions("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + assert len(resp) == 1 # MemoryStore can only store 1 version of each object + + +def test_memory_store_query(mem_store): + query = [Filter('type', '=', 'malware')] + resp = mem_store.query(query) + assert len(resp) == 0 + + +def test_memory_store_query_single_filter(mem_store): + query = Filter('id', '=', 'indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f') + resp = mem_store.query(query) + assert len(resp) == 1 + + +def test_memory_store_query_empty_query(mem_store): + resp = mem_store.query() + # sort since returned in random order + resp = sorted(resp, key=lambda k: k['id']) + assert len(resp) == 2 + assert resp[0]['id'] == 'indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f' + assert resp[0]['modified'] == '2017-01-27T13:49:53.935Z' + assert resp[1]['id'] == 'indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f' + assert resp[1]['modified'] == '2017-01-27T13:49:53.936Z' + + +def test_memory_store_query_multiple_filters(mem_store): + mem_store.source.filters.add(Filter('type', '=', 'indicator')) + query = Filter('id', '=', 'indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f') + resp = mem_store.query(query) + assert len(resp) == 1 + + +def test_memory_store_save_load_file(mem_store, fs_mem_store): + filename = fs_mem_store # the fixture fs_mem_store yields filename where the memory store was written to + + # STIX2 contents of mem_store have already been written to file + # (this is done in fixture 'fs_mem_store'), so can already read-in here + contents = open(os.path.abspath(filename)).read() + + assert '"id": "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f",' in contents + assert '"id": "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f",' in contents + + mem_store2 = MemoryStore() + mem_store2.load_from_file(filename) + assert mem_store2.get("indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f") + assert mem_store2.get("indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f") + + +def test_memory_store_add_invalid_object(mem_store): + ind = ('indicator', IND1) # tuple isn't valid + with pytest.raises(TypeError) as excinfo: + mem_store.add(ind) + assert 'stix_data expected to be' in str(excinfo.value) + assert 'a python-stix2 object' in str(excinfo.value) + assert 'JSON formatted STIX' in str(excinfo.value) + assert 'JSON formatted STIX bundle' in str(excinfo.value) + + +def test_memory_store_object_with_custom_property(mem_store): + camp = Campaign(name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True) + + mem_store.add(camp, True) + + camp_r = mem_store.get(camp.id) + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_memory_store_object_with_custom_property_in_bundle(mem_store): + camp = Campaign(name="Scipio Africanus", + objective="Defeat the Carthaginians", + x_empire="Roman", + allow_custom=True) + + bundle = Bundle(camp, allow_custom=True) + mem_store.add(bundle, True) + + bundle_r = mem_store.get(bundle.id) + camp_r = bundle_r['objects'][0] + assert camp_r.id == camp.id + assert camp_r.x_empire == camp.x_empire + + +def test_memory_store_custom_object(mem_store): + @CustomObject('x-new-obj', [ + ('property1', properties.StringProperty(required=True)), + ]) + class NewObj(): + pass + + newobj = NewObj(property1='something') + mem_store.add(newobj, True) + + newobj_r = mem_store.get(newobj.id) + assert newobj_r.id == newobj.id + assert newobj_r.property1 == 'something' + + +def test_relationships(rel_mem_store): + mal = rel_mem_store.get(MALWARE_ID) + resp = rel_mem_store.relationships(mal) + + assert len(resp) == 3 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[1] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_type(rel_mem_store): + mal = rel_mem_store.get(MALWARE_ID) + resp = rel_mem_store.relationships(mal, relationship_type='indicates') + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[0] + + +def test_relationships_by_source(rel_mem_store): + resp = rel_mem_store.relationships(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert resp[0]['id'] == RELATIONSHIP_IDS[1] + + +def test_relationships_by_target(rel_mem_store): + resp = rel_mem_store.relationships(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == RELATIONSHIP_IDS[0] for x in resp) + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_type(rel_mem_store): + resp = rel_mem_store.relationships(MALWARE_ID, relationship_type='uses', target_only=True) + + assert len(resp) == 1 + assert any(x['id'] == RELATIONSHIP_IDS[2] for x in resp) + + +def test_relationships_by_target_and_source(rel_mem_store): + with pytest.raises(ValueError) as excinfo: + rel_mem_store.relationships(MALWARE_ID, target_only=True, source_only=True) + + assert 'not both' in str(excinfo.value) + + +def test_related_to(rel_mem_store): + mal = rel_mem_store.get(MALWARE_ID) + resp = rel_mem_store.related_to(mal) + + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_source(rel_mem_store): + resp = rel_mem_store.related_to(MALWARE_ID, source_only=True) + + assert len(resp) == 1 + assert any(x['id'] == IDENTITY_ID for x in resp) + + +def test_related_to_by_target(rel_mem_store): + resp = rel_mem_store.related_to(MALWARE_ID, target_only=True) + + assert len(resp) == 2 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) diff --git a/stix2/test/test_note.py b/stix2/test/v21/test_note.py similarity index 100% rename from stix2/test/test_note.py rename to stix2/test/v21/test_note.py diff --git a/stix2/test/v21/test_object_markings.py b/stix2/test/v21/test_object_markings.py new file mode 100644 index 0000000..f216355 --- /dev/null +++ b/stix2/test/v21/test_object_markings.py @@ -0,0 +1,552 @@ + +import pytest + +from stix2 import TLP_AMBER, Malware, exceptions, markings + +from .constants import FAKE_TIME, MALWARE_ID +from .constants import MALWARE_KWARGS as MALWARE_KWARGS_CONST +from .constants import MARKING_IDS + +"""Tests for the Data Markings API.""" + +MALWARE_KWARGS = MALWARE_KWARGS_CONST.copy() +MALWARE_KWARGS.update({ + 'id': MALWARE_ID, + 'created': FAKE_TIME, + 'modified': FAKE_TIME, +}) + + +@pytest.mark.parametrize("data", [ + ( + Malware(**MALWARE_KWARGS), + Malware(object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS), + MARKING_IDS[0], + ), + ( + MALWARE_KWARGS, + dict(object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS), + MARKING_IDS[0], + ), + ( + Malware(**MALWARE_KWARGS), + Malware(object_marking_refs=[TLP_AMBER.id], + **MALWARE_KWARGS), + TLP_AMBER, + ), +]) +def test_add_markings_one_marking(data): + before = data[0] + after = data[1] + + before = markings.add_markings(before, data[2], None) + + for m in before["object_marking_refs"]: + assert m in after["object_marking_refs"] + + +def test_add_markings_multiple_marking(): + before = Malware( + **MALWARE_KWARGS + ) + + after = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1]], + **MALWARE_KWARGS + ) + + before = markings.add_markings(before, [MARKING_IDS[0], MARKING_IDS[1]], None) + + for m in before["object_marking_refs"]: + assert m in after["object_marking_refs"] + + +def test_add_markings_combination(): + before = Malware( + **MALWARE_KWARGS + ) + after = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1]], + granular_markings=[ + { + "selectors": ["labels"], + "marking_ref": MARKING_IDS[2] + }, + { + "selectors": ["name"], + "marking_ref": MARKING_IDS[3] + } + ], + **MALWARE_KWARGS + ) + + before = markings.add_markings(before, MARKING_IDS[0], None) + before = markings.add_markings(before, MARKING_IDS[1], None) + before = markings.add_markings(before, MARKING_IDS[2], "labels") + before = markings.add_markings(before, MARKING_IDS[3], "name") + + for m in before["granular_markings"]: + assert m in after["granular_markings"] + + for m in before["object_marking_refs"]: + assert m in after["object_marking_refs"] + + +@pytest.mark.parametrize("data", [ + ([""]), + (""), + ([]), + ([MARKING_IDS[0], 456]) +]) +def test_add_markings_bad_markings(data): + before = Malware( + **MALWARE_KWARGS + ) + with pytest.raises(exceptions.InvalidValueError): + before = markings.add_markings(before, data, None) + + assert "object_marking_refs" not in before + + +GET_MARKINGS_TEST_DATA = \ + { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45 + } + ], + "x": { + "y": [ + "hello", + 88 + ], + "z": { + "foo1": "bar", + "foo2": 65 + } + }, + "object_marking_refs": ["11"], + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"] + }, + { + "marking_ref": "2", + "selectors": ["c"] + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"] + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"] + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"] + }, + { + "marking_ref": "6", + "selectors": ["x"] + }, + { + "marking_ref": "7", + "selectors": ["x.y"] + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"] + }, + { + "marking_ref": "9", + "selectors": ["x.z"] + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"] + }, + ] + } + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_object_marking(data): + assert set(markings.get_markings(data, None)) == set(["11"]) + + +@pytest.mark.parametrize("data", [GET_MARKINGS_TEST_DATA]) +def test_get_markings_object_and_granular_combinations(data): + """Test multiple combinations for inherited and descendant markings.""" + assert set(markings.get_markings(data, "a", False, False)) == set(["1"]) + assert set(markings.get_markings(data, "a", True, False)) == set(["1", "11"]) + assert set(markings.get_markings(data, "a", True, True)) == set(["1", "11"]) + assert set(markings.get_markings(data, "a", False, True)) == set(["1"]) + + assert set(markings.get_markings(data, "b", False, False)) == set([]) + assert set(markings.get_markings(data, "b", True, False)) == set(["11"]) + assert set(markings.get_markings(data, "b", True, True)) == set(["11"]) + assert set(markings.get_markings(data, "b", False, True)) == set([]) + + assert set(markings.get_markings(data, "c", False, False)) == set(["2"]) + assert set(markings.get_markings(data, "c", True, False)) == set(["2", "11"]) + assert set(markings.get_markings(data, "c", True, True)) == set(["2", "3", "4", "5", "11"]) + assert set(markings.get_markings(data, "c", False, True)) == set(["2", "3", "4", "5"]) + + assert set(markings.get_markings(data, "c.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "c.[0]", True, False)) == set(["2", "11"]) + assert set(markings.get_markings(data, "c.[0]", True, True)) == set(["2", "11"]) + assert set(markings.get_markings(data, "c.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "c.[1]", False, False)) == set(["3"]) + assert set(markings.get_markings(data, "c.[1]", True, False)) == set(["2", "3", "11"]) + assert set(markings.get_markings(data, "c.[1]", True, True)) == set(["2", "3", "11"]) + assert set(markings.get_markings(data, "c.[1]", False, True)) == set(["3"]) + + assert set(markings.get_markings(data, "c.[2]", False, False)) == set(["4"]) + assert set(markings.get_markings(data, "c.[2]", True, False)) == set(["2", "4", "11"]) + assert set(markings.get_markings(data, "c.[2]", True, True)) == set(["2", "4", "5", "11"]) + assert set(markings.get_markings(data, "c.[2]", False, True)) == set(["4", "5"]) + + assert set(markings.get_markings(data, "c.[2].g", False, False)) == set(["5"]) + assert set(markings.get_markings(data, "c.[2].g", True, False)) == set(["2", "4", "5", "11"]) + assert set(markings.get_markings(data, "c.[2].g", True, True)) == set(["2", "4", "5", "11"]) + assert set(markings.get_markings(data, "c.[2].g", False, True)) == set(["5"]) + + assert set(markings.get_markings(data, "x", False, False)) == set(["6"]) + assert set(markings.get_markings(data, "x", True, False)) == set(["6", "11"]) + assert set(markings.get_markings(data, "x", True, True)) == set(["6", "7", "8", "9", "10", "11"]) + assert set(markings.get_markings(data, "x", False, True)) == set(["6", "7", "8", "9", "10"]) + + assert set(markings.get_markings(data, "x.y", False, False)) == set(["7"]) + assert set(markings.get_markings(data, "x.y", True, False)) == set(["6", "7", "11"]) + assert set(markings.get_markings(data, "x.y", True, True)) == set(["6", "7", "8", "11"]) + assert set(markings.get_markings(data, "x.y", False, True)) == set(["7", "8"]) + + assert set(markings.get_markings(data, "x.y.[0]", False, False)) == set([]) + assert set(markings.get_markings(data, "x.y.[0]", True, False)) == set(["6", "7", "11"]) + assert set(markings.get_markings(data, "x.y.[0]", True, True)) == set(["6", "7", "11"]) + assert set(markings.get_markings(data, "x.y.[0]", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.y.[1]", False, False)) == set(["8"]) + assert set(markings.get_markings(data, "x.y.[1]", True, False)) == set(["6", "7", "8", "11"]) + assert set(markings.get_markings(data, "x.y.[1]", True, True)) == set(["6", "7", "8", "11"]) + assert set(markings.get_markings(data, "x.y.[1]", False, True)) == set(["8"]) + + assert set(markings.get_markings(data, "x.z", False, False)) == set(["9"]) + assert set(markings.get_markings(data, "x.z", True, False)) == set(["6", "9", "11"]) + assert set(markings.get_markings(data, "x.z", True, True)) == set(["6", "9", "10", "11"]) + assert set(markings.get_markings(data, "x.z", False, True)) == set(["9", "10"]) + + assert set(markings.get_markings(data, "x.z.foo1", False, False)) == set([]) + assert set(markings.get_markings(data, "x.z.foo1", True, False)) == set(["6", "9", "11"]) + assert set(markings.get_markings(data, "x.z.foo1", True, True)) == set(["6", "9", "11"]) + assert set(markings.get_markings(data, "x.z.foo1", False, True)) == set([]) + + assert set(markings.get_markings(data, "x.z.foo2", False, False)) == set(["10"]) + assert set(markings.get_markings(data, "x.z.foo2", True, False)) == set(["6", "9", "10", "11"]) + assert set(markings.get_markings(data, "x.z.foo2", True, True)) == set(["6", "9", "10", "11"]) + assert set(markings.get_markings(data, "x.z.foo2", False, True)) == set(["10"]) + + +@pytest.mark.parametrize("data", [ + ( + Malware(object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS), + Malware(**MALWARE_KWARGS), + ), + ( + dict(object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS), + MALWARE_KWARGS, + ), +]) +def test_remove_markings_object_level(data): + before = data[0] + after = data[1] + + before = markings.remove_markings(before, MARKING_IDS[0], None) + + assert 'object_marking_refs' not in before + assert 'object_marking_refs' not in after + + modified = after['modified'] + after = markings.remove_markings(after, MARKING_IDS[0], None) + modified == after['modified'] + + +@pytest.mark.parametrize("data", [ + ( + Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS), + Malware(object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS), + [MARKING_IDS[0], MARKING_IDS[2]], + ), + ( + dict(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS), + dict(object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS), + [MARKING_IDS[0], MARKING_IDS[2]], + ), + ( + Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], TLP_AMBER.id], + **MALWARE_KWARGS), + Malware(object_marking_refs=[MARKING_IDS[1]], + **MALWARE_KWARGS), + [MARKING_IDS[0], TLP_AMBER], + ), +]) +def test_remove_markings_multiple(data): + before = data[0] + after = data[1] + + before = markings.remove_markings(before, data[2], None) + + assert before['object_marking_refs'] == after['object_marking_refs'] + + +def test_remove_markings_bad_markings(): + before = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ) + with pytest.raises(AssertionError) as excinfo: + markings.remove_markings(before, [MARKING_IDS[4]], None) + assert str(excinfo.value) == "Marking ['%s'] was not found in Malware!" % MARKING_IDS[4] + + +@pytest.mark.parametrize("data", [ + ( + Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS), + Malware(**MALWARE_KWARGS), + ), + ( + dict(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS), + MALWARE_KWARGS, + ), +]) +def test_clear_markings(data): + before = data[0] + after = data[1] + + before = markings.clear_markings(before, None) + + assert 'object_marking_refs' not in before + assert 'object_marking_refs' not in after + + +def test_is_marked_object_and_granular_combinations(): + """Test multiple combinations for inherited and descendant markings.""" + test_sdo = \ + { + "a": 333, + "b": "value", + "c": [ + 17, + "list value", + { + "g": "nested", + "h": 45 + } + ], + "x": { + "y": [ + "hello", + 88 + ], + "z": { + "foo1": "bar", + "foo2": 65 + } + }, + "object_marking_refs": "11", + "granular_markings": [ + { + "marking_ref": "1", + "selectors": ["a"] + }, + { + "marking_ref": "2", + "selectors": ["c"] + }, + { + "marking_ref": "3", + "selectors": ["c.[1]"] + }, + { + "marking_ref": "4", + "selectors": ["c.[2]"] + }, + { + "marking_ref": "5", + "selectors": ["c.[2].g"] + }, + { + "marking_ref": "6", + "selectors": ["x"] + }, + { + "marking_ref": "7", + "selectors": ["x.y"] + }, + { + "marking_ref": "8", + "selectors": ["x.y.[1]"] + }, + { + "marking_ref": "9", + "selectors": ["x.z"] + }, + { + "marking_ref": "10", + "selectors": ["x.z.foo2"] + }, + ] + } + + assert markings.is_marked(test_sdo, ["1"], "a", False, False) + assert markings.is_marked(test_sdo, ["1", "11"], "a", True, False) + assert markings.is_marked(test_sdo, ["1", "11"], "a", True, True) + assert markings.is_marked(test_sdo, ["1"], "a", False, True) + + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["11"], "b", True, False) + assert markings.is_marked(test_sdo, ["11"], "b", True, True) + assert markings.is_marked(test_sdo, "b", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["2"], "c", False, False) + assert markings.is_marked(test_sdo, ["2", "11"], "c", True, False) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5", "11"], "c", True, True) + assert markings.is_marked(test_sdo, ["2", "3", "4", "5"], "c", False, True) + + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["2", "11"], "c.[0]", True, False) + assert markings.is_marked(test_sdo, ["2", "11"], "c.[0]", True, True) + assert markings.is_marked(test_sdo, "c.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, False) + assert markings.is_marked(test_sdo, ["2", "3", "11"], "c.[1]", True, False) + assert markings.is_marked(test_sdo, ["2", "3", "11"], "c.[1]", True, True) + assert markings.is_marked(test_sdo, ["3"], "c.[1]", False, True) + + assert markings.is_marked(test_sdo, ["4"], "c.[2]", False, False) + assert markings.is_marked(test_sdo, ["2", "4", "11"], "c.[2]", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5", "11"], "c.[2]", True, True) + assert markings.is_marked(test_sdo, ["4", "5"], "c.[2]", False, True) + + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, False) + assert markings.is_marked(test_sdo, ["2", "4", "5", "11"], "c.[2].g", True, False) + assert markings.is_marked(test_sdo, ["2", "4", "5", "11"], "c.[2].g", True, True) + assert markings.is_marked(test_sdo, ["5"], "c.[2].g", False, True) + + assert markings.is_marked(test_sdo, ["6"], "x", False, False) + assert markings.is_marked(test_sdo, ["6", "11"], "x", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10", "11"], "x", True, True) + assert markings.is_marked(test_sdo, ["6", "7", "8", "9", "10"], "x", False, True) + + assert markings.is_marked(test_sdo, ["7"], "x.y", False, False) + assert markings.is_marked(test_sdo, ["6", "7", "11"], "x.y", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "11"], "x.y", True, True) + assert markings.is_marked(test_sdo, ["7", "8"], "x.y", False, True) + + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "7", "11"], "x.y.[0]", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "11"], "x.y.[0]", True, True) + assert markings.is_marked(test_sdo, "x.y.[0]", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "11"], "x.y.[1]", True, False) + assert markings.is_marked(test_sdo, ["6", "7", "8", "11"], "x.y.[1]", True, True) + assert markings.is_marked(test_sdo, ["8"], "x.y.[1]", False, True) + + assert markings.is_marked(test_sdo, ["9"], "x.z", False, False) + assert markings.is_marked(test_sdo, ["6", "9", "11"], "x.z", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10", "11"], "x.z", True, True) + assert markings.is_marked(test_sdo, ["9", "10"], "x.z", False, True) + + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=False) is False + assert markings.is_marked(test_sdo, ["6", "9", "11"], "x.z.foo1", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "11"], "x.z.foo1", True, True) + assert markings.is_marked(test_sdo, "x.z.foo1", inherited=False, descendants=True) is False + + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, False) + assert markings.is_marked(test_sdo, ["6", "9", "10", "11"], "x.z.foo2", True, False) + assert markings.is_marked(test_sdo, ["6", "9", "10", "11"], "x.z.foo2", True, True) + assert markings.is_marked(test_sdo, ["10"], "x.z.foo2", False, True) + + assert markings.is_marked(test_sdo, ["11"], None, True, True) + assert markings.is_marked(test_sdo, ["2"], None, True, True) is False + + +@pytest.mark.parametrize("data", [ + ( + Malware(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS), + Malware(**MALWARE_KWARGS), + ), + ( + dict(object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS), + MALWARE_KWARGS, + ), +]) +def test_is_marked_no_markings(data): + marked = data[0] + nonmarked = data[1] + + assert markings.is_marked(marked) + assert markings.is_marked(nonmarked) is False + + +def test_set_marking(): + before = Malware( + object_marking_refs=[MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]], + **MALWARE_KWARGS + ) + after = Malware( + object_marking_refs=[MARKING_IDS[4], MARKING_IDS[5]], + **MALWARE_KWARGS + ) + + before = markings.set_markings(before, [MARKING_IDS[4], MARKING_IDS[5]], None) + + for m in before["object_marking_refs"]: + assert m in [MARKING_IDS[4], MARKING_IDS[5]] + + assert [MARKING_IDS[0], MARKING_IDS[1], MARKING_IDS[2]] not in before["object_marking_refs"] + + for x in before["object_marking_refs"]: + assert x in after["object_marking_refs"] + + +@pytest.mark.parametrize("data", [ + ([]), + ([""]), + (""), + ([MARKING_IDS[4], 687]) +]) +def test_set_marking_bad_input(data): + before = Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ) + after = Malware( + object_marking_refs=[MARKING_IDS[0]], + **MALWARE_KWARGS + ) + with pytest.raises(exceptions.InvalidValueError): + before = markings.set_markings(before, data, None) + + assert before == after diff --git a/stix2/test/v21/test_observed_data.py b/stix2/test/v21/test_observed_data.py new file mode 100644 index 0000000..011a2d5 --- /dev/null +++ b/stix2/test/v21/test_observed_data.py @@ -0,0 +1,1234 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import OBSERVED_DATA_ID + +OBJECTS_REGEX = re.compile('\"objects\": {(?:.*?)(?:(?:[^{]*?)|(?:{[^{]*?}))*}', re.DOTALL) + + +EXPECTED = """{ + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "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", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "name": "foo.exe" + } + } +}""" + + +def test_observed_data_example(): + observed_data = stix2.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + 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", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file" + }, + }, + ) + + assert str(observed_data) == EXPECTED + + +EXPECTED_WITH_REF = """{ + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "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", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "name": "foo.exe" + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": [ + "0" + ] + } + } +}""" + + +def test_observed_data_example_with_refs(): + observed_data = stix2.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + 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", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file" + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["0"] + } + }, + ) + + assert str(observed_data) == EXPECTED_WITH_REF + + +def test_observed_data_example_with_bad_refs(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + 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", + number_observed=50, + objects={ + "0": { + "type": "file", + "name": "foo.exe" + }, + "1": { + "type": "directory", + "path": "/usr/home", + "contains_refs": ["2"] + } + }, + ) + + assert excinfo.value.cls == stix2.ObservedData + assert excinfo.value.prop_name == "objects" + assert excinfo.value.reason == "Invalid object reference for 'Directory:contains_refs': '2' is not a valid object in local scope" + + +def test_observed_data_example_with_non_dictionary(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + 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", + number_observed=50, + objects="file: foo.exe", + ) + + assert excinfo.value.cls == stix2.ObservedData + assert excinfo.value.prop_name == "objects" + assert 'must contain a dictionary' in excinfo.value.reason + + +def test_observed_data_example_with_empty_dictionary(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + 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", + number_observed=50, + objects={}, + ) + + assert excinfo.value.cls == stix2.ObservedData + assert excinfo.value.prop_name == "objects" + assert 'must contain a non-empty dictionary' in excinfo.value.reason + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "type": "observed-data", + "spec_version": "2.1", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "created": "2016-04-06T19:58:16.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "first_observed": "2015-12-21T19:00:00Z", + "last_observed": "2015-12-21T19:00:00Z", + "modified": "2016-04-06T19:58:16.000Z", + "number_observed": 50, + "objects": { + "0": { + "name": "foo.exe", + "type": "file" + } + } + }, +]) +def test_parse_observed_data(data): + odata = stix2.parse(data) + + assert odata.type == 'observed-data' + assert odata.id == OBSERVED_DATA_ID + assert odata.created == dt.datetime(2016, 4, 6, 19, 58, 16, tzinfo=pytz.utc) + assert odata.modified == dt.datetime(2016, 4, 6, 19, 58, 16, tzinfo=pytz.utc) + assert odata.first_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert odata.last_observed == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert odata.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert odata.objects["0"].type == "file" + + +@pytest.mark.parametrize("data", [ + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "payload_bin": "VBORw0KGgoAAAANSUhEUgAAADI==" + }""", + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + "hashes": { + "MD5": "6826f9a05da08134006557758bb3afbb" + } + }""", +]) +def test_parse_artifact_valid(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + odata = stix2.parse(odata_str) + assert odata.objects["0"].type == "artifact" + + +@pytest.mark.parametrize("data", [ + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "payload_bin": "abcVBORw0KGgoAAAANSUhEUgAAADI==" + }""", + """"0": { + "type": "artifact", + "mime_type": "image/jpeg", + "url": "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + "hashes": { + "MD5": "a" + } + }""", +]) +def test_parse_artifact_invalid(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + with pytest.raises(ValueError): + stix2.parse(odata_str) + + +def test_artifact_example_dependency_error(): + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.Artifact(url="http://example.com/sirvizio.exe") + + assert excinfo.value.dependencies == [("hashes", "url")] + assert str(excinfo.value) == "The property dependencies for Artifact: (hashes, url) are not met." + + +@pytest.mark.parametrize("data", [ + """"0": { + "type": "autonomous-system", + "number": 15139, + "name": "Slime Industries", + "rir": "ARIN" + }""", +]) +def test_parse_autonomous_system_valid(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + odata = stix2.parse(odata_str) + assert odata.objects["0"].type == "autonomous-system" + assert odata.objects["0"].number == 15139 + assert odata.objects["0"].name == "Slime Industries" + assert odata.objects["0"].rir == "ARIN" + + +@pytest.mark.parametrize("data", [ + """{ + "type": "email-addr", + "value": "john@example.com", + "display_name": "John Doe", + "belongs_to_ref": "0" + }""", +]) +def test_parse_email_address(data): + odata = stix2.parse_observable(data, {"0": "user-account"}) + assert odata.type == "email-addr" + + odata_str = re.compile('"belongs_to_ref": "0"', re.DOTALL).sub('"belongs_to_ref": "3"', data) + with pytest.raises(stix2.exceptions.InvalidObjRefError): + stix2.parse_observable(odata_str, {"0": "user-account"}) + + +@pytest.mark.parametrize("data", [ + """ + { + "type": "email-message", + "is_multipart": true, + "content_type": "multipart/mixed", + "date": "2016-06-19T14:20:40.000Z", + "from_ref": "1", + "to_refs": [ + "2" + ], + "cc_refs": [ + "3" + ], + "subject": "Check out this picture of a cat!", + "additional_header_fields": { + "Content-Disposition": "inline", + "X-Mailer": "Mutt/1.5.23", + "X-Originating-IP": "198.51.100.3" + }, + "body_multipart": [ + { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!" + }, + { + "content_type": "image/png", + "content_disposition": "attachment; filename=\\"tabby.png\\"", + "body_raw_ref": "4" + }, + { + "content_type": "application/zip", + "content_disposition": "attachment; filename=\\"tabby_pics.zip\\"", + "body_raw_ref": "5" + } + ] + } + """ +]) +def test_parse_email_message(data): + valid_refs = { + "0": "email-message", + "1": "email-addr", + "2": "email-addr", + "3": "email-addr", + "4": "artifact", + "5": "file", + } + odata = stix2.parse_observable(data, valid_refs) + assert odata.type == "email-message" + assert odata.body_multipart[0].content_disposition == "inline" + + +@pytest.mark.parametrize("data", [ + """ + { + "type": "email-message", + "from_ref": "0", + "to_refs": ["1"], + "is_multipart": true, + "date": "1997-11-21T15:55:06.000Z", + "subject": "Saying Hello", + "body": "Cats are funny!" + } + """ +]) +def test_parse_email_message_not_multipart(data): + valid_refs = { + "0": "email-addr", + "1": "email-addr", + } + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.parse_observable(data, valid_refs) + + assert excinfo.value.cls == stix2.EmailMessage + assert excinfo.value.dependencies == [("is_multipart", "body")] + + +@pytest.mark.parametrize("data", [ + """"0": { + "type": "file", + "hashes": { + "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a" + } + }, + "1": { + "type": "file", + "hashes": { + "SHA-256": "19c549ec2628b989382f6b280cbd7bb836a0b461332c0fe53511ce7d584b89d3" + } + }, + "2": { + "type": "file", + "hashes": { + "SHA-256": "0969de02ecf8a5f003e3f6d063d848c8a193aada092623f8ce408c15bcb5f038" + } + }, + "3": { + "type": "file", + "name": "foo.zip", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + }, + "mime_type": "application/zip", + "extensions": { + "archive-ext": { + "contains_refs": [ + "0", + "1", + "2" + ], + "version": "5.0" + } + } + }""", +]) +def test_parse_file_archive(data): + odata_str = OBJECTS_REGEX.sub('"objects": { %s }' % data, EXPECTED) + odata = stix2.parse(odata_str) + assert odata.objects["3"].extensions['archive-ext'].version == "5.0" + + +@pytest.mark.parametrize("data", [ + """ + { + "type": "email-message", + "is_multipart": true, + "content_type": "multipart/mixed", + "date": "2016-06-19T14:20:40.000Z", + "from_ref": "1", + "to_refs": [ + "2" + ], + "cc_refs": [ + "3" + ], + "subject": "Check out this picture of a cat!", + "additional_header_fields": { + "Content-Disposition": "inline", + "X-Mailer": "Mutt/1.5.23", + "X-Originating-IP": "198.51.100.3" + }, + "body_multipart": [ + { + "content_type": "text/plain; charset=utf-8", + "content_disposition": "inline", + "body": "Cats are funny!" + }, + { + "content_type": "image/png", + "content_disposition": "attachment; filename=\\"tabby.png\\"" + }, + { + "content_type": "application/zip", + "content_disposition": "attachment; filename=\\"tabby_pics.zip\\"", + "body_raw_ref": "5" + } + ] + } + """ +]) +def test_parse_email_message_with_at_least_one_error(data): + valid_refs = { + "0": "email-message", + "1": "email-addr", + "2": "email-addr", + "3": "email-addr", + "4": "artifact", + "5": "file", + } + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.parse_observable(data, valid_refs) + + assert excinfo.value.cls == stix2.EmailMIMEComponent + assert excinfo.value.properties == ["body", "body_raw_ref"] + assert "At least one of the" in str(excinfo.value) + assert "must be populated" in str(excinfo.value) + + +@pytest.mark.parametrize("data", [ + """ + { + "type": "network-traffic", + "src_ref": "0", + "dst_ref": "1", + "protocols": [ + "tcp" + ] + } + """ +]) +def test_parse_basic_tcp_traffic(data): + odata = stix2.parse_observable(data, {"0": "ipv4-addr", "1": "ipv4-addr"}) + + assert odata.type == "network-traffic" + assert odata.src_ref == "0" + assert odata.dst_ref == "1" + assert odata.protocols == ["tcp"] + + +@pytest.mark.parametrize("data", [ + """ + { + "type": "network-traffic", + "src_port": 2487, + "dst_port": 1723, + "protocols": [ + "ipv4", + "pptp" + ], + "src_byte_count": 35779, + "dst_byte_count": 935750, + "encapsulates_refs": [ + "4" + ] + } + """ +]) +def test_parse_basic_tcp_traffic_with_error(data): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.parse_observable(data, {"4": "network-traffic"}) + + assert excinfo.value.cls == stix2.NetworkTraffic + assert excinfo.value.properties == ["dst_ref", "src_ref"] + + +EXPECTED_PROCESS_OD = """{ + "created": "2016-04-06T19:58:16.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "first_observed": "2015-12-21T19:00:00Z", + "id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + "last_observed": "2015-12-21T19:00:00Z", + "modified": "2016-04-06T19:58:16.000Z", + "number_observed": 50, + "objects": { + "0": { + "type": "file", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100fSHA" + }, + }, + "1": { + "type": "process", + "pid": 1221, + "name": "gedit-bin", + "created": "2016-01-20T14:11:25.55Z", + "arguments" :[ + "--new-window" + ], + "binary_ref": "0" + } + }, + "type": "observed-data" +}""" + + +def test_observed_data_with_process_example(): + observed_data = stix2.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + 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", + number_observed=50, + objects={ + "0": { + "type": "file", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + }, + }, + "1": { + "type": "process", + "pid": 1221, + "name": "gedit-bin", + "created": "2016-01-20T14:11:25.55Z", + "arguments": [ + "--new-window" + ], + "binary_ref": "0" + } + }) + + assert observed_data.objects["0"].type == "file" + assert observed_data.objects["0"].hashes["SHA-256"] == "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + assert observed_data.objects["1"].type == "process" + assert observed_data.objects["1"].pid == 1221 + assert observed_data.objects["1"].name == "gedit-bin" + assert observed_data.objects["1"].arguments[0] == "--new-window" + + +# creating cyber observables directly + +def test_artifact_example(): + art = stix2.Artifact(mime_type="image/jpeg", + url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + hashes={ + "MD5": "6826f9a05da08134006557758bb3afbb" + }) + assert art.mime_type == "image/jpeg" + assert art.url == "https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg" + assert art.hashes["MD5"] == "6826f9a05da08134006557758bb3afbb" + + +def test_artifact_mutual_exclusion_error(): + with pytest.raises(stix2.exceptions.MutuallyExclusivePropertiesError) as excinfo: + stix2.Artifact(mime_type="image/jpeg", + url="https://upload.wikimedia.org/wikipedia/commons/b/b4/JPEG_example_JPG_RIP_100.jpg", + hashes={ + "MD5": "6826f9a05da08134006557758bb3afbb" + }, + payload_bin="VBORw0KGgoAAAANSUhEUgAAADI==") + + assert excinfo.value.cls == stix2.Artifact + assert excinfo.value.properties == ["payload_bin", "url"] + assert 'are mutually exclusive' in str(excinfo.value) + + +def test_directory_example(): + dir = stix2.Directory(_valid_refs={"1": "file"}, + path='/usr/lib', + created="2015-12-21T19:00:00Z", + modified="2015-12-24T19:00:00Z", + accessed="2015-12-21T20:00:00Z", + contains_refs=["1"]) + + assert dir.path == '/usr/lib' + assert dir.created == dt.datetime(2015, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert dir.modified == dt.datetime(2015, 12, 24, 19, 0, 0, tzinfo=pytz.utc) + assert dir.accessed == dt.datetime(2015, 12, 21, 20, 0, 0, tzinfo=pytz.utc) + assert dir.contains_refs == ["1"] + + +def test_directory_example_ref_error(): + with pytest.raises(stix2.exceptions.InvalidObjRefError) as excinfo: + stix2.Directory(_valid_refs=[], + path='/usr/lib', + created="2015-12-21T19:00:00Z", + modified="2015-12-24T19:00:00Z", + accessed="2015-12-21T20:00:00Z", + contains_refs=["1"]) + + assert excinfo.value.cls == stix2.Directory + assert excinfo.value.prop_name == "contains_refs" + + +def test_domain_name_example(): + dn = stix2.DomainName(_valid_refs={"1": 'domain-name'}, + value="example.com", + resolves_to_refs=["1"]) + + assert dn.value == "example.com" + assert dn.resolves_to_refs == ["1"] + + +def test_domain_name_example_invalid_ref_type(): + with pytest.raises(stix2.exceptions.InvalidObjRefError) as excinfo: + stix2.DomainName(_valid_refs={"1": "file"}, + value="example.com", + resolves_to_refs=["1"]) + + assert excinfo.value.cls == stix2.DomainName + assert excinfo.value.prop_name == "resolves_to_refs" + + +def test_file_example(): + f = stix2.File(name="qwerty.dll", + hashes={ + "SHA-256": "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a"}, + size=100, + magic_number_hex="1C", + mime_type="application/msword", + created="2016-12-21T19:00:00Z", + modified="2016-12-24T19:00:00Z", + accessed="2016-12-21T20:00:00Z", + is_encrypted=True, + encryption_algorithm="AES128-CBC", + decryption_key="fred" + ) + + assert f.name == "qwerty.dll" + assert f.size == 100 + assert f.magic_number_hex == "1C" + assert f.hashes["SHA-256"] == "ceafbfd424be2ca4a5f0402cae090dda2fb0526cf521b60b60077c0f622b285a" + assert f.mime_type == "application/msword" + assert f.created == dt.datetime(2016, 12, 21, 19, 0, 0, tzinfo=pytz.utc) + assert f.modified == dt.datetime(2016, 12, 24, 19, 0, 0, tzinfo=pytz.utc) + assert f.accessed == dt.datetime(2016, 12, 21, 20, 0, 0, tzinfo=pytz.utc) + assert f.is_encrypted + assert f.encryption_algorithm == "AES128-CBC" + assert f.decryption_key == "fred" # does the key have a format we can test for? + + +def test_file_example_with_NTFSExt(): + f = stix2.File(name="abc.txt", + extensions={ + "ntfs-ext": { + "alternate_data_streams": [ + { + "name": "second.stream", + "size": 25536 + } + ] + } + }) + + assert f.name == "abc.txt" + assert f.extensions["ntfs-ext"].alternate_data_streams[0].size == 25536 + + +def test_file_example_with_empty_NTFSExt(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.File(name="abc.txt", + extensions={ + "ntfs-ext": { + } + }) + + assert excinfo.value.cls == stix2.NTFSExt + assert excinfo.value.properties == sorted(list(stix2.NTFSExt._properties.keys())) + + +def test_file_example_with_PDFExt(): + f = stix2.File(name="qwerty.dll", + extensions={ + "pdf-ext": { + "version": "1.7", + "document_info_dict": { + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02" + }, + "pdfid0": "DFCE52BD827ECF765649852119D", + "pdfid1": "57A1E0F9ED2AE523E313C" + } + }) + + assert f.name == "qwerty.dll" + assert f.extensions["pdf-ext"].version == "1.7" + assert f.extensions["pdf-ext"].document_info_dict["Title"] == "Sample document" + + +def test_file_example_with_PDFExt_Object(): + f = stix2.File(name="qwerty.dll", + extensions={ + "pdf-ext": + stix2.PDFExt(version="1.7", + document_info_dict={ + "Title": "Sample document", + "Author": "Adobe Systems Incorporated", + "Creator": "Adobe FrameMaker 5.5.3 for Power Macintosh", + "Producer": "Acrobat Distiller 3.01 for Power Macintosh", + "CreationDate": "20070412090123-02" + }, + pdfid0="DFCE52BD827ECF765649852119D", + pdfid1="57A1E0F9ED2AE523E313C") + + }) + + assert f.name == "qwerty.dll" + assert f.extensions["pdf-ext"].version == "1.7" + assert f.extensions["pdf-ext"].document_info_dict["Title"] == "Sample document" + + +def test_file_example_with_RasterImageExt_Object(): + f = stix2.File(name="qwerty.jpeg", + extensions={ + "raster-image-ext": { + "bits_per_pixel": 123, + "exif_tags": { + "Make": "Nikon", + "Model": "D7000", + "XResolution": 4928, + "YResolution": 3264 + } + } + }) + assert f.name == "qwerty.jpeg" + assert f.extensions["raster-image-ext"].bits_per_pixel == 123 + assert f.extensions["raster-image-ext"].exif_tags["XResolution"] == 4928 + + +RASTER_IMAGE_EXT = """{ +"type": "observed-data", +"id": "observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", +"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", +"number_observed": 1, +"objects": { + "0": { + "type": "file", + "name": "picture.jpg", + "hashes": { + "SHA-256": "35a01331e9ad96f751278b891b6ea09699806faedfa237d40513d92ad1b7100f" + }, + "extensions": { + "raster-image-ext": { + "image_height": 768, + "image_width": 1024, + "bits_per_pixel": 72, + "image_compression_algorithm": "JPEG", + "exif_tags": { + "Make": "Nikon", + "Model": "D7000", + "XResolution": 4928, + "YResolution": 3264 + } + } + } + } +} +} +""" + + +def test_raster_image_ext_parse(): + obj = stix2.parse(RASTER_IMAGE_EXT) + assert obj.objects["0"].extensions['raster-image-ext'].image_width == 1024 + + +def test_raster_images_ext_create(): + ext = stix2.RasterImageExt(image_width=1024) + assert "image_width" in str(ext) + + +def test_file_example_with_WindowsPEBinaryExt(): + f = stix2.File(name="qwerty.dll", + extensions={ + "windows-pebinary-ext": { + "pe_type": "exe", + "machine_hex": "014c", + "number_of_sections": 4, + "time_date_stamp": "2016-01-22T12:31:12Z", + "pointer_to_symbol_table_hex": "74726144", + "number_of_symbols": 4542568, + "size_of_optional_header": 224, + "characteristics_hex": "818f", + "optional_header": { + "magic_hex": "010b", + "major_linker_version": 2, + "minor_linker_version": 25, + "size_of_code": 512, + "size_of_initialized_data": 283648, + "size_of_uninitialized_data": 0, + "address_of_entry_point": 4096, + "base_of_code": 4096, + "base_of_data": 8192, + "image_base": 14548992, + "section_alignment": 4096, + "file_alignment": 4096, + "major_os_version": 1, + "minor_os_version": 0, + "major_image_version": 0, + "minor_image_version": 0, + "major_subsystem_version": 4, + "minor_subsystem_version": 0, + "win32_version_value_hex": "00", + "size_of_image": 299008, + "size_of_headers": 4096, + "checksum_hex": "00", + "subsystem_hex": "03", + "dll_characteristics_hex": "00", + "size_of_stack_reserve": 100000, + "size_of_stack_commit": 8192, + "size_of_heap_reserve": 100000, + "size_of_heap_commit": 4096, + "loader_flags_hex": "abdbffde", + "number_of_rva_and_sizes": 3758087646 + }, + "sections": [ + { + "name": "CODE", + "entropy": 0.061089 + }, + { + "name": "DATA", + "entropy": 7.980693 + }, + { + "name": "NicolasB", + "entropy": 0.607433 + }, + { + "name": ".idata", + "entropy": 0.607433 + } + ] + } + + }) + assert f.name == "qwerty.dll" + assert f.extensions["windows-pebinary-ext"].sections[2].entropy == 0.607433 + + +def test_file_example_encryption_error(): + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.File(name="qwerty.dll", + is_encrypted=False, + encryption_algorithm="AES128-CBC") + + assert excinfo.value.cls == stix2.File + assert excinfo.value.dependencies == [("is_encrypted", "encryption_algorithm")] + assert "property dependencies" in str(excinfo.value) + assert "are not met" in str(excinfo.value) + + with pytest.raises(stix2.exceptions.DependentPropertiesError) as excinfo: + stix2.File(name="qwerty.dll", + encryption_algorithm="AES128-CBC") + + +def test_ip4_address_example(): + ip4 = stix2.IPv4Address(_valid_refs={"4": "mac-addr", "5": "mac-addr"}, + value="198.51.100.3", + resolves_to_refs=["4", "5"]) + + assert ip4.value == "198.51.100.3" + assert ip4.resolves_to_refs == ["4", "5"] + + +def test_ip4_address_example_cidr(): + ip4 = stix2.IPv4Address(value="198.51.100.0/24") + + assert ip4.value == "198.51.100.0/24" + + +def test_ip6_address_example(): + ip6 = stix2.IPv6Address(value="2001:0db8:85a3:0000:0000:8a2e:0370:7334") + + assert ip6.value == "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + + +def test_mac_address_example(): + ip6 = stix2.MACAddress(value="d2:fb:49:24:37:18") + + assert ip6.value == "d2:fb:49:24:37:18" + + +def test_network_traffic_example(): + nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr", "1": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + dst_ref="1") + assert nt.protocols == ["tcp"] + assert nt.src_ref == "0" + assert nt.dst_ref == "1" + + +def test_network_traffic_http_request_example(): + h = stix2.HTTPRequestExt(request_method="get", + request_value="/download.html", + request_version="http/1.1", + request_header={ + "Accept-Encoding": "gzip,deflate", + "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", + "Host": "www.example.com" + }) + nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'http-request-ext': h}) + assert nt.extensions['http-request-ext'].request_method == "get" + assert nt.extensions['http-request-ext'].request_value == "/download.html" + assert nt.extensions['http-request-ext'].request_version == "http/1.1" + assert nt.extensions['http-request-ext'].request_header['Accept-Encoding'] == "gzip,deflate" + assert nt.extensions['http-request-ext'].request_header['User-Agent'] == "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113" + assert nt.extensions['http-request-ext'].request_header['Host'] == "www.example.com" + + +def test_network_traffic_icmp_example(): + h = stix2.ICMPExt(icmp_type_hex="08", + icmp_code_hex="00") + nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'icmp-ext': h}) + assert nt.extensions['icmp-ext'].icmp_type_hex == "08" + assert nt.extensions['icmp-ext'].icmp_code_hex == "00" + + +def test_network_traffic_socket_example(): + h = stix2.SocketExt(is_listening=True, + address_family="AF_INET", + protocol_family="PF_INET", + socket_type="SOCK_STREAM") + nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'socket-ext': h}) + assert nt.extensions['socket-ext'].is_listening + assert nt.extensions['socket-ext'].address_family == "AF_INET" + assert nt.extensions['socket-ext'].protocol_family == "PF_INET" + assert nt.extensions['socket-ext'].socket_type == "SOCK_STREAM" + + +def test_network_traffic_tcp_example(): + h = stix2.TCPExt(src_flags_hex="00000002") + nt = stix2.NetworkTraffic(_valid_refs={"0": "ipv4-addr"}, + protocols="tcp", + src_ref="0", + extensions={'tcp-ext': h}) + assert nt.extensions['tcp-ext'].src_flags_hex == "00000002" + + +def test_mutex_example(): + m = stix2.Mutex(name="barney") + + assert m.name == "barney" + + +def test_process_example(): + p = stix2.Process(_valid_refs={"0": "file"}, + pid=1221, + name="gedit-bin", + created="2016-01-20T14:11:25.55Z", + arguments=["--new-window"], + binary_ref="0") + + assert p.name == "gedit-bin" + assert p.arguments == ["--new-window"] + + +def test_process_example_empty_error(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.Process() + + assert excinfo.value.cls == stix2.Process + properties_of_process = list(stix2.Process._properties.keys()) + properties_of_process.remove("type") + assert excinfo.value.properties == sorted(properties_of_process) + msg = "At least one of the ({1}) properties for {0} must be populated." + msg = msg.format(stix2.Process.__name__, + ", ".join(sorted(properties_of_process))) + assert str(excinfo.value) == msg + + +def test_process_example_empty_with_extensions(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.Process(extensions={ + "windows-process-ext": {} + }) + + assert excinfo.value.cls == stix2.WindowsProcessExt + properties_of_extension = list(stix2.WindowsProcessExt._properties.keys()) + assert excinfo.value.properties == sorted(properties_of_extension) + + +def test_process_example_windows_process_ext(): + proc = stix2.Process(pid=314, + name="foobar.exe", + extensions={ + "windows-process-ext": { + "aslr_enabled": True, + "dep_enabled": True, + "priority": "HIGH_PRIORITY_CLASS", + "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309" + } + }) + assert proc.extensions["windows-process-ext"].aslr_enabled + assert proc.extensions["windows-process-ext"].dep_enabled + assert proc.extensions["windows-process-ext"].priority == "HIGH_PRIORITY_CLASS" + assert proc.extensions["windows-process-ext"].owner_sid == "S-1-5-21-186985262-1144665072-74031268-1309" + + +def test_process_example_windows_process_ext_empty(): + with pytest.raises(stix2.exceptions.AtLeastOnePropertyError) as excinfo: + stix2.Process(pid=1221, + name="gedit-bin", + extensions={ + "windows-process-ext": {} + }) + + assert excinfo.value.cls == stix2.WindowsProcessExt + properties_of_extension = list(stix2.WindowsProcessExt._properties.keys()) + assert excinfo.value.properties == sorted(properties_of_extension) + + +def test_process_example_extensions_empty(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Process(extensions={}) + + assert excinfo.value.cls == stix2.Process + assert excinfo.value.prop_name == 'extensions' + assert 'non-empty dictionary' in excinfo.value.reason + + +def test_process_example_with_WindowsProcessExt_Object(): + p = stix2.Process(extensions={ + "windows-process-ext": stix2.WindowsProcessExt(aslr_enabled=True, + dep_enabled=True, + priority="HIGH_PRIORITY_CLASS", + owner_sid="S-1-5-21-186985262-1144665072-74031268-1309") # noqa + }) + + assert p.extensions["windows-process-ext"].dep_enabled + assert p.extensions["windows-process-ext"].owner_sid == "S-1-5-21-186985262-1144665072-74031268-1309" + + +def test_process_example_with_WindowsServiceExt(): + p = stix2.Process(extensions={ + "windows-service-ext": { + "service_name": "sirvizio", + "display_name": "Sirvizio", + "start_type": "SERVICE_AUTO_START", + "service_type": "SERVICE_WIN32_OWN_PROCESS", + "service_status": "SERVICE_RUNNING" + } + }) + + assert p.extensions["windows-service-ext"].service_name == "sirvizio" + assert p.extensions["windows-service-ext"].service_type == "SERVICE_WIN32_OWN_PROCESS" + + +def test_process_example_with_WindowsProcessServiceExt(): + p = stix2.Process(extensions={ + "windows-service-ext": { + "service_name": "sirvizio", + "display_name": "Sirvizio", + "start_type": "SERVICE_AUTO_START", + "service_type": "SERVICE_WIN32_OWN_PROCESS", + "service_status": "SERVICE_RUNNING" + }, + "windows-process-ext": { + "aslr_enabled": True, + "dep_enabled": True, + "priority": "HIGH_PRIORITY_CLASS", + "owner_sid": "S-1-5-21-186985262-1144665072-74031268-1309" + } + }) + + assert p.extensions["windows-service-ext"].service_name == "sirvizio" + assert p.extensions["windows-service-ext"].service_type == "SERVICE_WIN32_OWN_PROCESS" + assert p.extensions["windows-process-ext"].dep_enabled + assert p.extensions["windows-process-ext"].owner_sid == "S-1-5-21-186985262-1144665072-74031268-1309" + + +def test_software_example(): + s = stix2.Software(name="Word", + cpe="cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*", + version="2002", + vendor="Microsoft") + + assert s.name == "Word" + assert s.cpe == "cpe:2.3:a:microsoft:word:2000:*:*:*:*:*:*:*" + assert s.version == "2002" + assert s.vendor == "Microsoft" + + +def test_url_example(): + s = stix2.URL(value="https://example.com/research/index.html") + + assert s.type == "url" + assert s.value == "https://example.com/research/index.html" + + +def test_user_account_example(): + a = stix2.UserAccount(user_id="1001", + account_login="jdoe", + account_type="unix", + display_name="John Doe", + is_service_account=False, + is_privileged=False, + can_escalate_privs=True, + account_created="2016-01-20T12:31:12Z", + password_last_changed="2016-01-20T14:27:43Z", + account_first_login="2016-01-20T14:26:07Z", + account_last_login="2016-07-22T16:08:28Z") + + assert a.user_id == "1001" + assert a.account_login == "jdoe" + assert a.account_type == "unix" + assert a.display_name == "John Doe" + assert not a.is_service_account + assert not a.is_privileged + assert a.can_escalate_privs + assert a.account_created == dt.datetime(2016, 1, 20, 12, 31, 12, tzinfo=pytz.utc) + assert a.password_last_changed == dt.datetime(2016, 1, 20, 14, 27, 43, tzinfo=pytz.utc) + assert a.account_first_login == dt.datetime(2016, 1, 20, 14, 26, 7, tzinfo=pytz.utc) + assert a.account_last_login == dt.datetime(2016, 7, 22, 16, 8, 28, tzinfo=pytz.utc) + + +def test_user_account_unix_account_ext_example(): + u = stix2.UNIXAccountExt(gid=1001, + groups=["wheel"], + home_dir="/home/jdoe", + shell="/bin/bash") + a = stix2.UserAccount(user_id="1001", + account_login="jdoe", + account_type="unix", + extensions={'unix-account-ext': u}) + assert a.extensions['unix-account-ext'].gid == 1001 + assert a.extensions['unix-account-ext'].groups == ["wheel"] + assert a.extensions['unix-account-ext'].home_dir == "/home/jdoe" + assert a.extensions['unix-account-ext'].shell == "/bin/bash" + + +def test_windows_registry_key_example(): + with pytest.raises(ValueError): + v = stix2.WindowsRegistryValueType(name="Foo", + data="qwerty", + data_type="string") + + v = stix2.WindowsRegistryValueType(name="Foo", + data="qwerty", + data_type="REG_SZ") + w = stix2.WindowsRegistryKey(key="hkey_local_machine\\system\\bar\\foo", + values=[v]) + assert w.key == "hkey_local_machine\\system\\bar\\foo" + assert w.values[0].name == "Foo" + assert w.values[0].data == "qwerty" + assert w.values[0].data_type == "REG_SZ" + + +def test_x509_certificate_example(): + x509 = stix2.X509Certificate( + issuer="C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com", # noqa + validity_not_before="2016-03-12T12:00:00Z", + validity_not_after="2016-08-21T12:00:00Z", + subject="C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org") # noqa + + assert x509.type == "x509-certificate" + assert x509.issuer == "C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com" # noqa + assert x509.subject == "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org" # noqa + + +def test_new_version_with_related_objects(): + data = stix2.ObservedData( + first_observed="2016-03-12T12:00:00Z", + last_observed="2016-03-12T12:00:00Z", + number_observed=1, + objects={ + 'src_ip': { + 'type': 'ipv4-addr', + 'value': '127.0.0.1/32' + }, + 'domain': { + 'type': 'domain-name', + 'value': 'example.com', + 'resolves_to_refs': ['src_ip'] + } + } + ) + new_version = data.new_version(last_observed="2017-12-12T12:00:00Z") + assert new_version.last_observed.year == 2017 + assert new_version.objects['domain'].resolves_to_refs[0] == 'src_ip' diff --git a/stix2/test/test_opinion.py b/stix2/test/v21/test_opinion.py similarity index 100% rename from stix2/test/test_opinion.py rename to stix2/test/v21/test_opinion.py diff --git a/stix2/test/v21/test_pattern_expressions.py b/stix2/test/v21/test_pattern_expressions.py new file mode 100644 index 0000000..14e3774 --- /dev/null +++ b/stix2/test/v21/test_pattern_expressions.py @@ -0,0 +1,380 @@ +import datetime + +import pytest + +import stix2 + + +def test_create_comparison_expression(): + + exp = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'", + stix2.HashConstant("aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", "SHA-256")) # noqa + assert str(exp) == "file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f'" + + +def test_boolean_expression(): + exp1 = stix2.MatchesComparisonExpression("email-message:from_ref.value", + stix2.StringConstant(".+\\@example\\.com$")) + exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name", + stix2.StringConstant("^Final Report.+\\.exe$")) + exp = stix2.AndBooleanExpression([exp1, exp2]) + assert str(exp) == "email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$'" # noqa + + +def test_boolean_expression_with_parentheses(): + exp1 = stix2.MatchesComparisonExpression(stix2.ObjectPath("email-message", + [stix2.ReferenceObjectPathComponent("from_ref"), + stix2.BasicObjectPathComponent("value")]), + stix2.StringConstant(".+\\@example\\.com$")) + exp2 = stix2.MatchesComparisonExpression("email-message:body_multipart[*].body_raw_ref.name", + stix2.StringConstant("^Final Report.+\\.exe$")) + exp = stix2.ParentheticalExpression(stix2.AndBooleanExpression([exp1, exp2])) + assert str(exp) == "(email-message:from_ref.value MATCHES '.+\\\\@example\\\\.com$' AND email-message:body_multipart[*].body_raw_ref.name MATCHES '^Final Report.+\\\\.exe$')" # noqa + + +def test_hash_followed_by_registryKey_expression_python_constant(): + hash_exp = stix2.EqualityComparisonExpression("file:hashes.MD5", + stix2.HashConstant("79054025255fb1a26e4bc422aef54eb4", "MD5")) + o_exp1 = stix2.ObservationExpression(hash_exp) + reg_exp = stix2.EqualityComparisonExpression(stix2.ObjectPath("windows-registry-key", ["key"]), + stix2.StringConstant("HKEY_LOCAL_MACHINE\\foo\\bar")) + o_exp2 = stix2.ObservationExpression(reg_exp) + fb_exp = stix2.FollowedByObservationExpression([o_exp1, o_exp2]) + para_exp = stix2.ParentheticalExpression(fb_exp) + qual_exp = stix2.WithinQualifier(300) + exp = stix2.QualifiedObservationExpression(para_exp, qual_exp) + assert str(exp) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS" # noqa + + +def test_hash_followed_by_registryKey_expression(): + hash_exp = stix2.EqualityComparisonExpression("file:hashes.MD5", + stix2.HashConstant("79054025255fb1a26e4bc422aef54eb4", "MD5")) + o_exp1 = stix2.ObservationExpression(hash_exp) + reg_exp = stix2.EqualityComparisonExpression(stix2.ObjectPath("windows-registry-key", ["key"]), + stix2.StringConstant("HKEY_LOCAL_MACHINE\\foo\\bar")) + o_exp2 = stix2.ObservationExpression(reg_exp) + fb_exp = stix2.FollowedByObservationExpression([o_exp1, o_exp2]) + para_exp = stix2.ParentheticalExpression(fb_exp) + qual_exp = stix2.WithinQualifier(stix2.IntegerConstant(300)) + exp = stix2.QualifiedObservationExpression(para_exp, qual_exp) + assert str(exp) == "([file:hashes.MD5 = '79054025255fb1a26e4bc422aef54eb4'] FOLLOWEDBY [windows-registry-key:key = 'HKEY_LOCAL_MACHINE\\\\foo\\\\bar']) WITHIN 300 SECONDS" # noqa + + +def test_file_observable_expression(): + exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'", + stix2.HashConstant( + "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", + 'SHA-256')) + exp2 = stix2.EqualityComparisonExpression("file:mime_type", stix2.StringConstant("application/x-pdf")) + bool_exp = stix2.AndBooleanExpression([exp1, exp2]) + exp = stix2.ObservationExpression(bool_exp) + assert str(exp) == "[file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f' AND file:mime_type = 'application/x-pdf']" # noqa + + +@pytest.mark.parametrize("observation_class, op", [ + (stix2.AndObservationExpression, 'AND'), + (stix2.OrObservationExpression, 'OR'), +]) +def test_multiple_file_observable_expression(observation_class, op): + exp1 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'", + stix2.HashConstant( + "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c", + 'SHA-256')) + exp2 = stix2.EqualityComparisonExpression("file:hashes.MD5", + stix2.HashConstant("cead3f77f6cda6ec00f57d76c9a6879f", "MD5")) + bool1_exp = stix2.OrBooleanExpression([exp1, exp2]) + exp3 = stix2.EqualityComparisonExpression("file:hashes.'SHA-256'", + stix2.HashConstant( + "aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f", + 'SHA-256')) + op1_exp = stix2.ObservationExpression(bool1_exp) + op2_exp = stix2.ObservationExpression(exp3) + exp = observation_class([op1_exp, op2_exp]) + assert str(exp) == "[file:hashes.'SHA-256' = 'bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c' OR file:hashes.MD5 = 'cead3f77f6cda6ec00f57d76c9a6879f'] {} [file:hashes.'SHA-256' = 'aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f']".format(op) # noqa + + +def test_root_types(): + ast = stix2.ObservationExpression( + stix2.AndBooleanExpression( + [stix2.ParentheticalExpression( + stix2.OrBooleanExpression([ + stix2.EqualityComparisonExpression("a:b", stix2.StringConstant("1")), + stix2.EqualityComparisonExpression("b:c", stix2.StringConstant("2"))])), + stix2.EqualityComparisonExpression(u"b:d", stix2.StringConstant("3"))])) + assert str(ast) == "[(a:b = '1' OR b:c = '2') AND b:d = '3']" + + +def test_artifact_payload(): + exp1 = stix2.EqualityComparisonExpression("artifact:mime_type", + "application/vnd.tcpdump.pcap") + exp2 = stix2.MatchesComparisonExpression("artifact:payload_bin", + stix2.StringConstant("\\xd4\\xc3\\xb2\\xa1\\x02\\x00\\x04\\x00")) + and_exp = stix2.AndBooleanExpression([exp1, exp2]) + exp = stix2.ObservationExpression(and_exp) + assert str(exp) == "[artifact:mime_type = 'application/vnd.tcpdump.pcap' AND artifact:payload_bin MATCHES '\\\\xd4\\\\xc3\\\\xb2\\\\xa1\\\\x02\\\\x00\\\\x04\\\\x00']" # noqa + + +def test_greater_than_python_constant(): + exp1 = stix2.GreaterThanComparisonExpression("file:extensions.windows-pebinary-ext.sections[*].entropy", + 7.0) + exp = stix2.ObservationExpression(exp1) + assert str(exp) == "[file:extensions.windows-pebinary-ext.sections[*].entropy > 7.0]" + + +def test_greater_than(): + exp1 = stix2.GreaterThanComparisonExpression("file:extensions.windows-pebinary-ext.sections[*].entropy", + stix2.FloatConstant(7.0)) + exp = stix2.ObservationExpression(exp1) + assert str(exp) == "[file:extensions.windows-pebinary-ext.sections[*].entropy > 7.0]" + + +def test_less_than(): + exp = stix2.LessThanComparisonExpression("file:size", + 1024) + assert str(exp) == "file:size < 1024" + + +def test_greater_than_or_equal(): + exp = stix2.GreaterThanEqualComparisonExpression("file:size", + 1024) + assert str(exp) == "file:size >= 1024" + + +def test_less_than_or_equal(): + exp = stix2.LessThanEqualComparisonExpression("file:size", + 1024) + assert str(exp) == "file:size <= 1024" + + +def test_not(): + exp = stix2.LessThanComparisonExpression("file:size", + 1024, + negated=True) + assert str(exp) == "file:size NOT < 1024" + + +def test_and_observable_expression(): + exp1 = stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("user-account:account_type", + "unix"), + stix2.EqualityComparisonExpression("user-account:user_id", + stix2.StringConstant("1007")), + stix2.EqualityComparisonExpression("user-account:account_login", + "Peter")]) + exp2 = stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("user-account:account_type", + "unix"), + stix2.EqualityComparisonExpression("user-account:user_id", + stix2.StringConstant("1008")), + stix2.EqualityComparisonExpression("user-account:account_login", + "Paul")]) + exp3 = stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("user-account:account_type", + "unix"), + stix2.EqualityComparisonExpression("user-account:user_id", + stix2.StringConstant("1009")), + stix2.EqualityComparisonExpression("user-account:account_login", + "Mary")]) + exp = stix2.AndObservationExpression([stix2.ObservationExpression(exp1), + stix2.ObservationExpression(exp2), + stix2.ObservationExpression(exp3)]) + assert str(exp) == "[user-account:account_type = 'unix' AND user-account:user_id = '1007' AND user-account:account_login = 'Peter'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1008' AND user-account:account_login = 'Paul'] AND [user-account:account_type = 'unix' AND user-account:user_id = '1009' AND user-account:account_login = 'Mary']" # noqa + + +def test_invalid_and_observable_expression(): + with pytest.raises(ValueError) as excinfo: + stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("user-account:display_name", + "admin"), + stix2.EqualityComparisonExpression("email-addr:display_name", + stix2.StringConstant("admin"))]) + assert "All operands to an 'AND' expression must have the same object type" in str(excinfo) + + +def test_hex(): + exp_and = stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("file:mime_type", + "image/bmp"), + stix2.EqualityComparisonExpression("file:magic_number_hex", + stix2.HexConstant("ffd8"))]) + exp = stix2.ObservationExpression(exp_and) + assert str(exp) == "[file:mime_type = 'image/bmp' AND file:magic_number_hex = h'ffd8']" + + +def test_multiple_qualifiers(): + exp_and = stix2.AndBooleanExpression([stix2.EqualityComparisonExpression("network-traffic:dst_ref.type", + "domain-name"), + stix2.EqualityComparisonExpression("network-traffic:dst_ref.value", + "example.com")]) + exp_ob = stix2.ObservationExpression(exp_and) + qual_rep = stix2.RepeatQualifier(5) + qual_within = stix2.WithinQualifier(stix2.IntegerConstant(1800)) + exp = stix2.QualifiedObservationExpression(stix2.QualifiedObservationExpression(exp_ob, qual_rep), qual_within) + assert str(exp) == "[network-traffic:dst_ref.type = 'domain-name' AND network-traffic:dst_ref.value = 'example.com'] REPEATS 5 TIMES WITHIN 1800 SECONDS" # noqa + + +def test_set_op(): + exp = stix2.ObservationExpression(stix2.IsSubsetComparisonExpression("network-traffic:dst_ref.value", + "2001:0db8:dead:beef:0000:0000:0000:0000/64")) + assert str(exp) == "[network-traffic:dst_ref.value ISSUBSET '2001:0db8:dead:beef:0000:0000:0000:0000/64']" + + +def test_timestamp(): + ts = stix2.TimestampConstant('2014-01-13T07:03:17Z') + assert str(ts) == "t'2014-01-13T07:03:17Z'" + + +def test_boolean(): + exp = stix2.EqualityComparisonExpression("email-message:is_multipart", + True) + assert str(exp) == "email-message:is_multipart = true" + + +def test_binary(): + const = stix2.BinaryConstant("dGhpcyBpcyBhIHRlc3Q=") + exp = stix2.EqualityComparisonExpression("artifact:payload_bin", + const) + assert str(exp) == "artifact:payload_bin = b'dGhpcyBpcyBhIHRlc3Q='" + + +def test_list(): + exp = stix2.InComparisonExpression("process:name", + ['proccy', 'proximus', 'badproc']) + assert str(exp) == "process:name IN ('proccy', 'proximus', 'badproc')" + + +def test_list2(): + # alternate way to construct an "IN" Comparison Expression + exp = stix2.EqualityComparisonExpression("process:name", + ['proccy', 'proximus', 'badproc']) + assert str(exp) == "process:name IN ('proccy', 'proximus', 'badproc')" + + +def test_invalid_constant_type(): + with pytest.raises(ValueError) as excinfo: + 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: + stix2.IntegerConstant('foo') + assert 'must be an integer' in str(excinfo) + + +def test_invalid_timestamp_constant(): + with pytest.raises(ValueError) as excinfo: + 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: + stix2.FloatConstant('foo') + assert 'must be a float' in str(excinfo) + + +@pytest.mark.parametrize("data, result", [ + (True, True), + (False, False), + ('True', True), + ('False', False), + ('true', True), + ('false', False), + ('t', True), + ('f', False), + ('T', True), + ('F', False), + (1, True), + (0, False), +]) +def test_boolean_constant(data, result): + boolean = stix2.BooleanConstant(data) + assert boolean.value == result + + +def test_invalid_boolean_constant(): + with pytest.raises(ValueError) as excinfo: + stix2.BooleanConstant('foo') + assert 'must be a boolean' in str(excinfo) + + +@pytest.mark.parametrize("hashtype, data", [ + ('MD5', 'zzz'), + ('ssdeep', 'zzz=='), +]) +def test_invalid_hash_constant(hashtype, data): + with pytest.raises(ValueError) as excinfo: + 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: + 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: + stix2.BinaryConstant('foo') + assert 'must contain a base64' in str(excinfo) + + +def test_escape_quotes_and_backslashes(): + exp = stix2.MatchesComparisonExpression("file:name", + "^Final Report.+\\.exe$") + assert str(exp) == "file:name MATCHES '^Final Report.+\\\\.exe$'" + + +def test_like(): + exp = stix2.LikeComparisonExpression("directory:path", + "C:\\Windows\\%\\foo") + assert str(exp) == "directory:path LIKE 'C:\\\\Windows\\\\%\\\\foo'" + + +def test_issuperset(): + exp = stix2.IsSupersetComparisonExpression("ipv4-addr:value", + "198.51.100.0/24") + assert str(exp) == "ipv4-addr:value ISSUPERSET '198.51.100.0/24'" + + +def test_repeat_qualifier(): + qual = stix2.RepeatQualifier(stix2.IntegerConstant(5)) + assert str(qual) == 'REPEATS 5 TIMES' + + +def test_invalid_repeat_qualifier(): + with pytest.raises(ValueError) as excinfo: + 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: + stix2.WithinQualifier('foo') + assert 'is not a valid argument for a Within Qualifier' in str(excinfo) + + +def test_startstop_qualifier(): + qual = stix2.StartStopQualifier(stix2.TimestampConstant('2016-06-01T00:00:00Z'), + datetime.datetime(2017, 3, 12, 8, 30, 0)) + assert str(qual) == "START t'2016-06-01T00:00:00Z' STOP t'2017-03-12T08:30:00Z'" + + qual2 = stix2.StartStopQualifier(datetime.date(2016, 6, 1), + stix2.TimestampConstant('2016-07-01T00:00:00Z')) + assert str(qual2) == "START t'2016-06-01T00:00:00Z' STOP t'2016-07-01T00:00:00Z'" + + +def test_invalid_startstop_qualifier(): + with pytest.raises(ValueError) as excinfo: + 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: + 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(): + str_const = stix2.StringConstant('Foo') + result = stix2.patterns.make_constant(str_const) + assert result is str_const diff --git a/stix2/test/v21/test_pickle.py b/stix2/test/v21/test_pickle.py new file mode 100644 index 0000000..9e2cc9a --- /dev/null +++ b/stix2/test/v21/test_pickle.py @@ -0,0 +1,17 @@ +import pickle + +import stix2 + + +def test_pickling(): + """ + Ensure a pickle/unpickle cycle works okay. + """ + identity = stix2.Identity( + id="identity--d66cb89d-5228-4983-958c-fa84ef75c88c", + name="alice", + description="this is a pickle test", + identity_class="some_class" + ) + + pickle.loads(pickle.dumps(identity)) diff --git a/stix2/test/v21/test_properties.py b/stix2/test/v21/test_properties.py new file mode 100644 index 0000000..cd7723a --- /dev/null +++ b/stix2/test/v21/test_properties.py @@ -0,0 +1,364 @@ +import pytest + +from stix2 import CustomObject, EmailMIMEComponent, ExtensionsProperty, TCPExt +from stix2.exceptions import AtLeastOnePropertyError, DictionaryKeyError +from stix2.v20.properties import (BinaryProperty, BooleanProperty, + DictionaryProperty, EmbeddedObjectProperty, + EnumProperty, FloatProperty, HashesProperty, + HexProperty, IDProperty, IntegerProperty, + ListProperty, Property, ReferenceProperty, + StringProperty, TimestampProperty, + TypeProperty) + +from .constants import FAKE_TIME + + +def test_property(): + p = Property() + + assert p.required is False + assert p.clean('foo') == 'foo' + assert p.clean(3) == 3 + + +def test_basic_clean(): + class Prop(Property): + + def clean(self, value): + if value == 42: + return value + else: + raise ValueError("Must be 42") + + p = Prop() + + assert p.clean(42) == 42 + with pytest.raises(ValueError): + p.clean(41) + + +def test_property_default(): + class Prop(Property): + + def default(self): + return 77 + + p = Prop() + + assert p.default() == 77 + + +def test_fixed_property(): + p = Property(fixed="2.0") + + assert p.clean("2.0") + with pytest.raises(ValueError): + assert p.clean("x") is False + with pytest.raises(ValueError): + assert p.clean(2.0) is False + + assert p.default() == "2.0" + assert p.clean(p.default()) + + +def test_list_property(): + p = ListProperty(StringProperty) + + assert p.clean(['abc', 'xyz']) + with pytest.raises(ValueError): + p.clean([]) + + +def test_string_property(): + prop = StringProperty() + + assert prop.clean('foobar') + assert prop.clean(1) + assert prop.clean([1, 2, 3]) + + +def test_type_property(): + prop = TypeProperty('my-type') + + assert prop.clean('my-type') + with pytest.raises(ValueError): + prop.clean('not-my-type') + assert prop.clean(prop.default()) + + +def test_id_property(): + idprop = IDProperty('my-type') + + assert idprop.clean('my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') + with pytest.raises(ValueError) as excinfo: + idprop.clean('not-my-type--90aaca8a-1110-5d32-956d-ac2f34a1bd8c') + assert str(excinfo.value) == "must start with 'my-type--'." + with pytest.raises(ValueError) as excinfo: + idprop.clean('my-type--foo') + assert str(excinfo.value) == "must have a valid UUID after the prefix." + + assert idprop.clean(idprop.default()) + + +@pytest.mark.parametrize("value", [ + 2, + -1, + 3.14, + False, +]) +def test_integer_property_valid(value): + int_prop = IntegerProperty() + assert int_prop.clean(value) is not None + + +@pytest.mark.parametrize("value", [ + "something", + StringProperty(), +]) +def test_integer_property_invalid(value): + int_prop = IntegerProperty() + with pytest.raises(ValueError): + int_prop.clean(value) + + +@pytest.mark.parametrize("value", [ + 2, + -1, + 3.14, + False, +]) +def test_float_property_valid(value): + int_prop = FloatProperty() + assert int_prop.clean(value) is not None + + +@pytest.mark.parametrize("value", [ + "something", + StringProperty(), +]) +def test_float_property_invalid(value): + int_prop = FloatProperty() + with pytest.raises(ValueError): + int_prop.clean(value) + + +@pytest.mark.parametrize("value", [ + True, + False, + 'True', + 'False', + 'true', + 'false', + 'TRUE', + 'FALSE', + 'T', + 'F', + 't', + 'f', + 1, + 0, +]) +def test_boolean_property_valid(value): + bool_prop = BooleanProperty() + + assert bool_prop.clean(value) is not None + + +@pytest.mark.parametrize("value", [ + 'abc', + ['false'], + {'true': 'true'}, + 2, + -1, +]) +def test_boolean_property_invalid(value): + bool_prop = BooleanProperty() + with pytest.raises(ValueError): + bool_prop.clean(value) + + +def test_reference_property(): + ref_prop = ReferenceProperty() + + assert ref_prop.clean("my-type--3a331bfe-0566-55e1-a4a0-9a2cd355a300") + with pytest.raises(ValueError): + ref_prop.clean("foo") + + +@pytest.mark.parametrize("value", [ + '2017-01-01T12:34:56Z', + '2017-01-01 12:34:56', + 'Jan 1 2017 12:34:56', +]) +def test_timestamp_property_valid(value): + ts_prop = TimestampProperty() + assert ts_prop.clean(value) == FAKE_TIME + + +def test_timestamp_property_invalid(): + ts_prop = TimestampProperty() + with pytest.raises(ValueError): + ts_prop.clean(1) + with pytest.raises(ValueError): + ts_prop.clean("someday sometime") + + +def test_binary_property(): + bin_prop = BinaryProperty() + + assert bin_prop.clean("TG9yZW0gSXBzdW0=") + with pytest.raises(ValueError): + bin_prop.clean("foobar") + + +def test_hex_property(): + hex_prop = HexProperty() + + assert hex_prop.clean("4c6f72656d20497073756d") + with pytest.raises(ValueError): + hex_prop.clean("foobar") + + +@pytest.mark.parametrize("d", [ + {'description': 'something'}, + [('abc', 1), ('bcd', 2), ('cde', 3)], +]) +def test_dictionary_property_valid(d): + dict_prop = DictionaryProperty() + assert dict_prop.clean(d) + + +@pytest.mark.parametrize("d", [ + [{'a': 'something'}, "Invalid dictionary key a: (shorter than 3 characters)."], + [{'a'*300: 'something'}, "Invalid dictionary key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaa: (longer than 256 characters)."], + [{'Hey!': 'something'}, "Invalid dictionary key Hey!: (contains characters other thanlowercase a-z, " + "uppercase A-Z, numerals 0-9, hyphen (-), or underscore (_))."], +]) +def test_dictionary_property_invalid_key(d): + dict_prop = DictionaryProperty() + + with pytest.raises(DictionaryKeyError) as excinfo: + dict_prop.clean(d[0]) + + assert str(excinfo.value) == d[1] + + +@pytest.mark.parametrize("d", [ + ({}, "The dictionary property must contain a non-empty dictionary"), + # TODO: This error message could be made more helpful. The error is caused + # because `json.loads()` doesn't like the *single* quotes around the key + # name, even though they are valid in a Python dictionary. While technically + # accurate (a string is not a dictionary), if we want to be able to load + # string-encoded "dictionaries" that are, we need a better error message + # or an alternative to `json.loads()` ... and preferably *not* `eval()`. :-) + # Changing the following to `'{"description": "something"}'` does not cause + # any ValueError to be raised. + ("{'description': 'something'}", "The dictionary property must contain a dictionary"), +]) +def test_dictionary_property_invalid(d): + dict_prop = DictionaryProperty() + + with pytest.raises(ValueError) as excinfo: + dict_prop.clean(d[0]) + assert str(excinfo.value) == d[1] + + +def test_property_list_of_dictionary(): + @CustomObject('x-new-obj', [ + ('property1', ListProperty(DictionaryProperty(), required=True)), + ]) + class NewObj(): + pass + + test_obj = NewObj(property1=[{'foo': 'bar'}]) + assert test_obj.property1[0]['foo'] == 'bar' + + +@pytest.mark.parametrize("value", [ + {"sha256": "6db12788c37247f2316052e142f42f4b259d6561751e5f401a1ae2a6df9c674b"}, + [('MD5', '2dfb1bcc980200c6706feee399d41b3f'), ('RIPEMD-160', 'b3a8cd8a27c90af79b3c81754f267780f443dfef')], +]) +def test_hashes_property_valid(value): + hash_prop = HashesProperty() + assert hash_prop.clean(value) + + +@pytest.mark.parametrize("value", [ + {"MD5": "a"}, + {"SHA-256": "2dfb1bcc980200c6706feee399d41b3f"}, +]) +def test_hashes_property_invalid(value): + hash_prop = HashesProperty() + + with pytest.raises(ValueError): + hash_prop.clean(value) + + +def test_embedded_property(): + emb_prop = EmbeddedObjectProperty(type=EmailMIMEComponent) + mime = EmailMIMEComponent( + content_type="text/plain; charset=utf-8", + content_disposition="inline", + body="Cats are funny!" + ) + assert emb_prop.clean(mime) + + with pytest.raises(ValueError): + emb_prop.clean("string") + + +@pytest.mark.parametrize("value", [ + ['a', 'b', 'c'], + ('a', 'b', 'c'), + 'b', +]) +def test_enum_property_valid(value): + enum_prop = EnumProperty(value) + assert enum_prop.clean('b') + + +def test_enum_property_invalid(): + enum_prop = EnumProperty(['a', 'b', 'c']) + with pytest.raises(ValueError): + enum_prop.clean('z') + + +def test_extension_property_valid(): + ext_prop = ExtensionsProperty(enclosing_type='file') + assert ext_prop({ + 'windows-pebinary-ext': { + 'pe_type': 'exe' + }, + }) + + +@pytest.mark.parametrize("data", [ + 1, + {'foobar-ext': { + 'pe_type': 'exe' + }}, +]) +def test_extension_property_invalid(data): + ext_prop = ExtensionsProperty(enclosing_type='file') + with pytest.raises(ValueError): + ext_prop.clean(data) + + +def test_extension_property_invalid_type(): + ext_prop = ExtensionsProperty(enclosing_type='indicator') + with pytest.raises(ValueError) as excinfo: + ext_prop.clean({ + 'windows-pebinary-ext': { + 'pe_type': 'exe' + }} + ) + assert 'no extensions defined' in str(excinfo.value) + + +def test_extension_at_least_one_property_constraint(): + with pytest.raises(AtLeastOnePropertyError): + TCPExt() diff --git a/stix2/test/v21/test_relationship.py b/stix2/test/v21/test_relationship.py new file mode 100644 index 0000000..21a2ec5 --- /dev/null +++ b/stix2/test/v21/test_relationship.py @@ -0,0 +1,162 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import (FAKE_TIME, INDICATOR_ID, MALWARE_ID, RELATIONSHIP_ID, + RELATIONSHIP_KWARGS) + +EXPECTED_RELATIONSHIP = """{ + "type": "relationship", + "spec_version": "2.1", + "id": "relationship--00000000-1111-2222-3333-444444444444", + "created": "2016-04-06T20:06:37.000Z", + "modified": "2016-04-06T20:06:37.000Z", + "relationship_type": "indicates", + "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210" +}""" + + +def test_relationship_all_required_properties(): + now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + + rel = stix2.Relationship( + type='relationship', + id=RELATIONSHIP_ID, + created=now, + modified=now, + relationship_type='indicates', + source_ref=INDICATOR_ID, + target_ref=MALWARE_ID, + ) + assert str(rel) == EXPECTED_RELATIONSHIP + + +def test_relationship_autogenerated_properties(relationship): + assert relationship.type == 'relationship' + assert relationship.id == 'relationship--00000000-0000-0000-0000-000000000001' + assert relationship.created == FAKE_TIME + assert relationship.modified == FAKE_TIME + assert relationship.relationship_type == 'indicates' + assert relationship.source_ref == INDICATOR_ID + assert relationship.target_ref == MALWARE_ID + + assert relationship['type'] == 'relationship' + assert relationship['id'] == 'relationship--00000000-0000-0000-0000-000000000001' + assert relationship['created'] == FAKE_TIME + assert relationship['modified'] == FAKE_TIME + assert relationship['relationship_type'] == 'indicates' + assert relationship['source_ref'] == INDICATOR_ID + assert relationship['target_ref'] == MALWARE_ID + + +def test_relationship_type_must_be_relationship(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Relationship(type='xxx', **RELATIONSHIP_KWARGS) + + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.prop_name == "type" + assert excinfo.value.reason == "must equal 'relationship'." + assert str(excinfo.value) == "Invalid value for Relationship 'type': must equal 'relationship'." + + +def test_relationship_id_must_start_with_relationship(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Relationship(id='my-prefix--', **RELATIONSHIP_KWARGS) + + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.prop_name == "id" + assert excinfo.value.reason == "must start with 'relationship--'." + assert str(excinfo.value) == "Invalid value for Relationship 'id': must start with 'relationship--'." + + +def test_relationship_required_property_relationship_type(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.Relationship() + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.properties == ["relationship_type", "source_ref", "target_ref"] + + +def test_relationship_missing_some_required_properties(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.Relationship(relationship_type='indicates') + + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.properties == ["source_ref", "target_ref"] + + +def test_relationship_required_properties_target_ref(): + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + stix2.Relationship( + relationship_type='indicates', + source_ref=INDICATOR_ID + ) + + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.properties == ["target_ref"] + + +def test_cannot_assign_to_relationship_attributes(relationship): + with pytest.raises(stix2.exceptions.ImmutableError) as excinfo: + relationship.relationship_type = "derived-from" + + assert str(excinfo.value) == "Cannot modify 'relationship_type' property in 'Relationship' after creation." + + +def test_invalid_kwarg_to_relationship(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.Relationship(my_custom_property="foo", **RELATIONSHIP_KWARGS) + + assert excinfo.value.cls == stix2.Relationship + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Relationship: (my_custom_property)." + + +def test_create_relationship_from_objects_rather_than_ids(indicator, malware): + rel = stix2.Relationship( + relationship_type="indicates", + source_ref=indicator, + target_ref=malware, + ) + + assert rel.relationship_type == 'indicates' + assert rel.source_ref == 'indicator--00000000-0000-0000-0000-000000000001' + assert rel.target_ref == 'malware--00000000-0000-0000-0000-000000000003' + assert rel.id == 'relationship--00000000-0000-0000-0000-000000000005' + + +def test_create_relationship_with_positional_args(indicator, malware): + rel = stix2.Relationship(indicator, 'indicates', malware) + + assert rel.relationship_type == 'indicates' + assert rel.source_ref == 'indicator--00000000-0000-0000-0000-000000000001' + assert rel.target_ref == 'malware--00000000-0000-0000-0000-000000000003' + assert rel.id == 'relationship--00000000-0000-0000-0000-000000000005' + + +@pytest.mark.parametrize("data", [ + EXPECTED_RELATIONSHIP, + { + "created": "2016-04-06T20:06:37Z", + "id": "relationship--00000000-1111-2222-3333-444444444444", + "modified": "2016-04-06T20:06:37Z", + "relationship_type": "indicates", + "source_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "target_ref": "malware--fedcba98-7654-3210-fedc-ba9876543210", + "spec_version": "2.1", + "type": "relationship" + }, +]) +def test_parse_relationship(data): + rel = stix2.parse(data) + + assert rel.type == 'relationship' + assert rel.id == RELATIONSHIP_ID + assert rel.created == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert rel.modified == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert rel.relationship_type == "indicates" + assert rel.source_ref == "indicator--01234567-89ab-cdef-0123-456789abcdef" + assert rel.target_ref == "malware--fedcba98-7654-3210-fedc-ba9876543210" diff --git a/stix2/test/v21/test_report.py b/stix2/test/v21/test_report.py new file mode 100644 index 0000000..da5a7ab --- /dev/null +++ b/stix2/test/v21/test_report.py @@ -0,0 +1,132 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import INDICATOR_KWARGS, REPORT_ID + +EXPECTED = """{ + "type": "report", + "spec_version": "2.1", + "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", + "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", + "created": "2015-12-21T19:59:11.000Z", + "modified": "2015-12-21T19:59:11.000Z", + "name": "The Black Vine Cyberespionage Group", + "description": "A simple report with an indicator and campaign", + "published": "2016-01-20T17:00:00Z", + "object_refs": [ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ], + "labels": [ + "campaign" + ] +}""" + + +def test_report_example(): + report = stix2.Report( + id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", + created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", + created="2015-12-21T19:59:11.000Z", + modified="2015-12-21T19:59:11.000Z", + name="The Black Vine Cyberespionage Group", + description="A simple report with an indicator and campaign", + published="2016-01-20T17:00:00Z", + labels=["campaign"], + object_refs=[ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ], + ) + + assert str(report) == EXPECTED + + +def test_report_example_objects_in_object_refs(): + report = stix2.Report( + id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", + created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", + created="2015-12-21T19:59:11.000Z", + modified="2015-12-21T19:59:11.000Z", + name="The Black Vine Cyberespionage Group", + description="A simple report with an indicator and campaign", + published="2016-01-20T17:00:00Z", + labels=["campaign"], + object_refs=[ + stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ], + ) + + assert str(report) == EXPECTED + + +def test_report_example_objects_in_object_refs_with_bad_id(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Report( + id="report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", + created_by_ref="identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", + created="2015-12-21T19:59:11.000Z", + modified="2015-12-21T19:59:11.000Z", + name="The Black Vine Cyberespionage Group", + description="A simple report with an indicator and campaign", + published="2016-01-20T17:00:00Z", + labels=["campaign"], + object_refs=[ + stix2.Indicator(id="indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", **INDICATOR_KWARGS), + "campaign-83422c77-904c-4dc1-aff5-5c38f3a2c55c", # the "bad" id, missing a "-" + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ], + ) + + assert excinfo.value.cls == stix2.Report + assert excinfo.value.prop_name == "object_refs" + assert excinfo.value.reason == "must match --." + assert str(excinfo.value) == "Invalid value for Report 'object_refs': must match --." + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "created": "2015-12-21T19:59:11.000Z", + "created_by_ref": "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283", + "description": "A simple report with an indicator and campaign", + "id": "report--84e4d88f-44ea-4bcd-bbf3-b2c1c320bcb3", + "labels": [ + "campaign" + ], + "modified": "2015-12-21T19:59:11.000Z", + "name": "The Black Vine Cyberespionage Group", + "object_refs": [ + "indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a" + ], + "published": "2016-01-20T17:00:00Z", + "spec_version": "2.1", + "type": "report" + }, +]) +def test_parse_report(data): + rept = stix2.parse(data) + + assert rept.type == 'report' + assert rept.id == REPORT_ID + assert rept.created == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert rept.modified == dt.datetime(2015, 12, 21, 19, 59, 11, tzinfo=pytz.utc) + assert rept.created_by_ref == "identity--a463ffb3-1bd9-4d94-b02d-74e4f1658283" + assert rept.object_refs == ["indicator--26ffb872-1dd9-446e-b6f5-d58527e5b5d2", + "campaign--83422c77-904c-4dc1-aff5-5c38f3a2c55c", + "relationship--f82356ae-fe6c-437c-9c24-6b64314ae68a"] + assert rept.description == "A simple report with an indicator and campaign" + assert rept.labels == ["campaign"] + assert rept.name == "The Black Vine Cyberespionage Group" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_sighting.py b/stix2/test/v21/test_sighting.py new file mode 100644 index 0000000..209403e --- /dev/null +++ b/stix2/test/v21/test_sighting.py @@ -0,0 +1,115 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import INDICATOR_ID, SIGHTING_ID, SIGHTING_KWARGS + +EXPECTED_SIGHTING = """{ + "type": "sighting", + "spec_version": "2.1", + "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", + "created": "2016-04-06T20:06:37.000Z", + "modified": "2016-04-06T20:06:37.000Z", + "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "where_sighted_refs": [ + "identity--8cc7afd6-5455-4d2b-a736-e614ee631d99" + ] +}""" + +BAD_SIGHTING = """{ + "created": "2016-04-06T20:06:37.000Z", + "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", + "modified": "2016-04-06T20:06:37.000Z", + "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "spec_version": "2.1", + "type": "sighting", + "where_sighted_refs": [ + "malware--8cc7afd6-5455-4d2b-a736-e614ee631d99" + ] +}""" + + +def test_sighting_all_required_properties(): + now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + + s = stix2.Sighting( + type='sighting', + id=SIGHTING_ID, + created=now, + modified=now, + sighting_of_ref=INDICATOR_ID, + where_sighted_refs=["identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"] + ) + assert str(s) == EXPECTED_SIGHTING + + +def test_sighting_bad_where_sighted_refs(): + now = dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Sighting( + type='sighting', + id=SIGHTING_ID, + created=now, + modified=now, + sighting_of_ref=INDICATOR_ID, + where_sighted_refs=["malware--8cc7afd6-5455-4d2b-a736-e614ee631d99"] + ) + + assert excinfo.value.cls == stix2.Sighting + assert excinfo.value.prop_name == "where_sighted_refs" + assert excinfo.value.reason == "must start with 'identity'." + assert str(excinfo.value) == "Invalid value for Sighting 'where_sighted_refs': must start with 'identity'." + + +def test_sighting_type_must_be_sightings(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.Sighting(type='xxx', **SIGHTING_KWARGS) + + assert excinfo.value.cls == stix2.Sighting + assert excinfo.value.prop_name == "type" + assert excinfo.value.reason == "must equal 'sighting'." + assert str(excinfo.value) == "Invalid value for Sighting 'type': must equal 'sighting'." + + +def test_invalid_kwarg_to_sighting(): + with pytest.raises(stix2.exceptions.ExtraPropertiesError) as excinfo: + stix2.Sighting(my_custom_property="foo", **SIGHTING_KWARGS) + + assert excinfo.value.cls == stix2.Sighting + assert excinfo.value.properties == ['my_custom_property'] + assert str(excinfo.value) == "Unexpected properties for Sighting: (my_custom_property)." + + +def test_create_sighting_from_objects_rather_than_ids(malware): # noqa: F811 + rel = stix2.Sighting(sighting_of_ref=malware) + + assert rel.sighting_of_ref == 'malware--00000000-0000-0000-0000-000000000001' + assert rel.id == 'sighting--00000000-0000-0000-0000-000000000003' + + +@pytest.mark.parametrize("data", [ + EXPECTED_SIGHTING, + { + "created": "2016-04-06T20:06:37Z", + "id": "sighting--bfbc19db-ec35-4e45-beed-f8bde2a772fb", + "modified": "2016-04-06T20:06:37Z", + "sighting_of_ref": "indicator--01234567-89ab-cdef-0123-456789abcdef", + "type": "sighting", + "where_sighted_refs": [ + "identity--8cc7afd6-5455-4d2b-a736-e614ee631d99" + ] + }, +]) +def test_parse_sighting(data): + sighting = stix2.parse(data) + + assert sighting.type == 'sighting' + assert sighting.id == SIGHTING_ID + assert sighting.created == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert sighting.modified == dt.datetime(2016, 4, 6, 20, 6, 37, tzinfo=pytz.utc) + assert sighting.sighting_of_ref == "indicator--01234567-89ab-cdef-0123-456789abcdef" + assert sighting.where_sighted_refs == ["identity--8cc7afd6-5455-4d2b-a736-e614ee631d99"] diff --git a/stix2/test/v21/test_threat_actor.py b/stix2/test/v21/test_threat_actor.py new file mode 100644 index 0000000..c0001f0 --- /dev/null +++ b/stix2/test/v21/test_threat_actor.py @@ -0,0 +1,67 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import THREAT_ACTOR_ID + +EXPECTED = """{ + "type": "threat-actor", + "spec_version": "2.1", + "id": "threat-actor--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": "Evil Org", + "description": "The Evil Org threat actor group", + "labels": [ + "crime-syndicate" + ] +}""" + + +def test_threat_actor_example(): + threat_actor = stix2.ThreatActor( + id="threat-actor--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="Evil Org", + description="The Evil Org threat actor group", + labels=["crime-syndicate"], + ) + + assert str(threat_actor) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48.000Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "description": "The Evil Org threat actor group", + "id": "threat-actor--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "labels": [ + "crime-syndicate" + ], + "modified": "2016-04-06T20:03:48.000Z", + "name": "Evil Org", + "spec_version": "2.1", + "type": "threat-actor" + }, +]) +def test_parse_threat_actor(data): + actor = stix2.parse(data) + + assert actor.type == 'threat-actor' + assert actor.id == THREAT_ACTOR_ID + assert actor.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert actor.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert actor.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert actor.description == "The Evil Org threat actor group" + assert actor.name == "Evil Org" + assert actor.labels == ["crime-syndicate"] + +# TODO: Add other examples diff --git a/stix2/test/v21/test_tool.py b/stix2/test/v21/test_tool.py new file mode 100644 index 0000000..7920e3d --- /dev/null +++ b/stix2/test/v21/test_tool.py @@ -0,0 +1,97 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import TOOL_ID + +EXPECTED = """{ + "type": "tool", + "spec_version": "2.1", + "id": "tool--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": "VNC", + "labels": [ + "remote-access" + ] +}""" + +EXPECTED_WITH_REVOKED = """{ + "type": "tool", + "spec_version": "2.1", + "id": "tool--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": "VNC", + "revoked": false, + "labels": [ + "remote-access" + ] +}""" + + +def test_tool_example(): + tool = stix2.Tool( + id="tool--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="VNC", + labels=["remote-access"], + ) + + assert str(tool) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "created": "2016-04-06T20:03:48Z", + "created_by_ref": "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff", + "id": "tool--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + "labels": [ + "remote-access" + ], + "modified": "2016-04-06T20:03:48Z", + "name": "VNC", + "spec_version": "2.1", + "type": "tool" + }, +]) +def test_parse_tool(data): + tool = stix2.parse(data) + + assert tool.type == 'tool' + assert tool.id == TOOL_ID + assert tool.created == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert tool.modified == dt.datetime(2016, 4, 6, 20, 3, 48, tzinfo=pytz.utc) + assert tool.created_by_ref == "identity--f431f809-377b-45e0-aa1c-6a4751cae5ff" + assert tool.labels == ["remote-access"] + assert tool.name == "VNC" + + +def test_tool_no_workbench_wrappers(): + tool = stix2.Tool(name='VNC', labels=['remote-access']) + with pytest.raises(AttributeError): + tool.created_by() + + +def test_tool_serialize_with_defaults(): + tool = stix2.Tool( + id="tool--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="VNC", + labels=["remote-access"], + ) + + assert tool.serialize(pretty=True, include_optional_defaults=True) == EXPECTED_WITH_REVOKED + + +# TODO: Add other examples diff --git a/stix2/test/v21/test_utils.py b/stix2/test/v21/test_utils.py new file mode 100644 index 0000000..885c4d9 --- /dev/null +++ b/stix2/test/v21/test_utils.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- + +import datetime as dt +from io import StringIO + +import pytest +import pytz + +import stix2.utils + +amsterdam = pytz.timezone('Europe/Amsterdam') +eastern = pytz.timezone('US/Eastern') + + +@pytest.mark.parametrize('dttm, timestamp', [ + (dt.datetime(2017, 1, 1, tzinfo=pytz.utc), '2017-01-01T00:00:00Z'), + (amsterdam.localize(dt.datetime(2017, 1, 1)), '2016-12-31T23:00:00Z'), + (eastern.localize(dt.datetime(2017, 1, 1, 12, 34, 56)), '2017-01-01T17:34:56Z'), + (eastern.localize(dt.datetime(2017, 7, 1)), '2017-07-01T04:00:00Z'), + (dt.datetime(2017, 7, 1), '2017-07-01T00:00:00Z'), + (dt.datetime(2017, 7, 1, 0, 0, 0, 1), '2017-07-01T00:00:00.000001Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='millisecond'), '2017-07-01T00:00:00.000Z'), + (stix2.utils.STIXdatetime(2017, 7, 1, 0, 0, 0, 1, precision='second'), '2017-07-01T00:00:00Z'), +]) +def test_timestamp_formatting(dttm, timestamp): + assert stix2.utils.format_datetime(dttm) == timestamp + + +@pytest.mark.parametrize('timestamp, dttm', [ + (dt.datetime(2017, 1, 1, 0, tzinfo=pytz.utc), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + (dt.date(2017, 1, 1), dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T00:00:00Z', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T02:00:00+2:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), + ('2017-01-01T00:00:00', dt.datetime(2017, 1, 1, 0, 0, 0, tzinfo=pytz.utc)), +]) +def test_parse_datetime(timestamp, dttm): + assert stix2.utils.parse_into_datetime(timestamp) == dttm + + +@pytest.mark.parametrize('timestamp, dttm, precision', [ + ('2017-01-01T01:02:03.000001', dt.datetime(2017, 1, 1, 1, 2, 3, 0, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.001', dt.datetime(2017, 1, 1, 1, 2, 3, 1000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.1', dt.datetime(2017, 1, 1, 1, 2, 3, 100000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, 450000, tzinfo=pytz.utc), 'millisecond'), + ('2017-01-01T01:02:03.45', dt.datetime(2017, 1, 1, 1, 2, 3, tzinfo=pytz.utc), 'second'), +]) +def test_parse_datetime_precision(timestamp, dttm, precision): + assert stix2.utils.parse_into_datetime(timestamp, precision) == dttm + + +@pytest.mark.parametrize('ts', [ + 'foobar', + 1, +]) +def test_parse_datetime_invalid(ts): + with pytest.raises(ValueError): + stix2.utils.parse_into_datetime('foobar') + + +@pytest.mark.parametrize('data', [ + {"a": 1}, + '{"a": 1}', + StringIO(u'{"a": 1}'), + [("a", 1,)], +]) +def test_get_dict(data): + assert stix2.utils._get_dict(data) + + +@pytest.mark.parametrize('data', [ + 1, + [1], + ['a', 1], + "foobar", +]) +def test_get_dict_invalid(data): + with pytest.raises(ValueError): + stix2.utils._get_dict(data) + + +@pytest.mark.parametrize('stix_id, type', [ + ('malware--d69c8146-ab35-4d50-8382-6fc80e641d43', 'malware'), + ('intrusion-set--899ce53f-13a0-479b-a0e4-67d46e241542', 'intrusion-set') +]) +def test_get_type_from_id(stix_id, type): + assert stix2.utils.get_type_from_id(stix_id) == type + + +def test_deduplicate(stix_objs1): + unique = stix2.utils.deduplicate(stix_objs1) + + # Only 3 objects are unique + # 2 id's vary + # 2 modified times vary for a particular id + + assert len(unique) == 3 + + ids = [obj['id'] for obj in unique] + mods = [obj['modified'] for obj in unique] + + assert "indicator--d81f86b8-975b-bc0b-775e-810c5ad45a4f" in ids + assert "indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f" in ids + assert "2017-01-27T13:49:53.935Z" in mods + assert "2017-01-27T13:49:53.936Z" in mods + + +@pytest.mark.parametrize('object, tuple_to_find, expected_index', [ + (stix2.ObservedData( + id="observed-data--b67d30ff-02ac-498a-92f9-32f845f448cf", + 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", + number_observed=50, + objects={ + "0": { + "name": "foo.exe", + "type": "file" + }, + "1": { + "type": "ipv4-addr", + "value": "198.51.100.3" + }, + "2": { + "type": "network-traffic", + "src_ref": "1", + "protocols": [ + "tcp", + "http" + ], + "extensions": { + "http-request-ext": { + "request_method": "get", + "request_value": "/download.html", + "request_version": "http/1.1", + "request_header": { + "Accept-Encoding": "gzip,deflate", + "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113", + "Host": "www.example.com" + } + } + } + } + }, + ), ('1', {"type": "ipv4-addr", "value": "198.51.100.3"}), 1), + ({ + "type": "x-example", + "id": "x-example--d5413db2-c26c-42e0-b0e0-ec800a310bfb", + "created": "2018-06-11T01:25:22.063Z", + "modified": "2018-06-11T01:25:22.063Z", + "dictionary": { + "key": { + "key_one": "value", + "key_two": "value" + } + } + }, ('key', {'key_one': 'value', 'key_two': 'value'}), 0), + ({ + "type": "language-content", + "id": "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d", + "created": "2017-02-08T21:31:22.007Z", + "modified": "2017-02-08T21:31:22.007Z", + "object_ref": "campaign--12a111f0-b824-4baf-a224-83b80237a094", + "object_modified": "2017-02-08T21:31:22.007Z", + "contents": { + "de": { + "name": "Bank Angriff 1", + "description": "Weitere Informationen über Banküberfall" + }, + "fr": { + "name": "Attaque Bank 1", + "description": "Plus d'informations sur la crise bancaire" + } + } + }, ('fr', {"name": "Attaque Bank 1", "description": "Plus d'informations sur la crise bancaire"}), 1) +]) +def test_find_property_index(object, tuple_to_find, expected_index): + assert stix2.utils.find_property_index( + object, + *tuple_to_find + ) == expected_index + + +@pytest.mark.parametrize('dict_value, tuple_to_find, expected_index', [ + ({ + "contents": { + "de": { + "name": "Bank Angriff 1", + "description": "Weitere Informationen über Banküberfall" + }, + "fr": { + "name": "Attaque Bank 1", + "description": "Plus d'informations sur la crise bancaire" + }, + "es": { + "name": "Ataque al Banco", + "description": "Mas informacion sobre el ataque al banco" + } + } + }, ('es', {"name": "Ataque al Banco", "description": "Mas informacion sobre el ataque al banco"}), 1), # Sorted alphabetically + ({ + 'my_list': [ + {"key_one": 1}, + {"key_two": 2} + ] + }, ('key_one', 1), 0) +]) +def test_iterate_over_values(dict_value, tuple_to_find, expected_index): + assert stix2.utils._find_property_in_seq(dict_value.values(), *tuple_to_find) == expected_index diff --git a/stix2/test/v21/test_versioning.py b/stix2/test/v21/test_versioning.py new file mode 100644 index 0000000..254090d --- /dev/null +++ b/stix2/test/v21/test_versioning.py @@ -0,0 +1,252 @@ +import pytest + +import stix2 + +from .constants import CAMPAIGN_MORE_KWARGS + + +def test_making_new_version(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + + campaign_v2 = campaign_v1.new_version(name="fred") + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name != campaign_v2.name + assert campaign_v2.name == "fred" + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + + +def test_making_new_version_with_unset(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + + campaign_v2 = campaign_v1.new_version(description=None) + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name == campaign_v2.name + with pytest.raises(AttributeError): + assert campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + + +def test_making_new_version_with_embedded_object(): + campaign_v1 = stix2.Campaign( + external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-163" + }], + **CAMPAIGN_MORE_KWARGS + ) + + campaign_v2 = campaign_v1.new_version(external_references=[{ + "source_name": "capec", + "external_id": "CAPEC-164" + }]) + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name == campaign_v2.name + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + assert campaign_v1.external_references[0].external_id != campaign_v2.external_references[0].external_id + + +def test_revoke(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + + campaign_v2 = campaign_v1.revoke() + + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.created_by_ref == campaign_v2.created_by_ref + assert campaign_v1.created == campaign_v2.created + assert campaign_v1.name == campaign_v2.name + assert campaign_v1.description == campaign_v2.description + assert campaign_v1.modified < campaign_v2.modified + + assert campaign_v2.revoked + + +def test_versioning_error_invalid_property(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + + with pytest.raises(stix2.exceptions.UnmodifiablePropertyError) as excinfo: + campaign_v1.new_version(type="threat-actor") + + assert str(excinfo.value) == "These properties cannot be changed when making a new version: type." + + +def test_versioning_error_bad_modified_value(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + campaign_v1.new_version(modified="2015-04-06T20:03:00.000Z") + + assert excinfo.value.cls == stix2.Campaign + assert excinfo.value.prop_name == "modified" + assert excinfo.value.reason == "The new modified datetime cannot be before than or equal to the current modified datetime." \ + "It cannot be equal, as according to STIX 2 specification, objects that are different " \ + "but have the same id and modified timestamp do not have defined consumer behavior." + + msg = "Invalid value for {0} '{1}': {2}" + msg = msg.format(stix2.Campaign.__name__, "modified", + "The new modified datetime cannot be before than or equal to the current modified datetime." + "It cannot be equal, as according to STIX 2 specification, objects that are different " + "but have the same id and modified timestamp do not have defined consumer behavior.") + assert str(excinfo.value) == msg + + +def test_versioning_error_usetting_required_property(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + + with pytest.raises(stix2.exceptions.MissingPropertiesError) as excinfo: + campaign_v1.new_version(name=None) + + assert excinfo.value.cls == stix2.Campaign + assert excinfo.value.properties == ["name"] + + msg = "No values for required properties for {0}: ({1})." + msg = msg.format(stix2.Campaign.__name__, "name") + assert str(excinfo.value) == msg + + +def test_versioning_error_new_version_of_revoked(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v2 = campaign_v1.revoke() + + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: + campaign_v2.new_version(name="barney") + assert str(excinfo.value) == "Cannot create a new version of a revoked object." + + assert excinfo.value.called_by == "new_version" + assert str(excinfo.value) == "Cannot create a new version of a revoked object." + + +def test_versioning_error_revoke_of_revoked(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v2 = campaign_v1.revoke() + + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: + campaign_v2.revoke() + assert str(excinfo.value) == "Cannot revoke an already revoked object." + + assert excinfo.value.called_by == "revoke" + assert str(excinfo.value) == "Cannot revoke an already revoked object." + + +def test_making_new_version_dict(): + campaign_v1 = CAMPAIGN_MORE_KWARGS + campaign_v2 = stix2.utils.new_version(CAMPAIGN_MORE_KWARGS, name="fred") + + assert campaign_v1['id'] == campaign_v2['id'] + assert campaign_v1['created_by_ref'] == campaign_v2['created_by_ref'] + assert campaign_v1['created'] == campaign_v2['created'] + assert campaign_v1['name'] != campaign_v2['name'] + assert campaign_v2['name'] == "fred" + assert campaign_v1['description'] == campaign_v2['description'] + assert stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified'] + + +def test_versioning_error_dict_bad_modified_value(): + with pytest.raises(stix2.exceptions.InvalidValueError) as excinfo: + stix2.utils.new_version(CAMPAIGN_MORE_KWARGS, modified="2015-04-06T20:03:00.000Z") + + assert excinfo.value.cls == dict + assert excinfo.value.prop_name == "modified" + assert excinfo.value.reason == "The new modified datetime cannot be before than or equal to the current modified datetime." \ + "It cannot be equal, as according to STIX 2 specification, objects that are different " \ + "but have the same id and modified timestamp do not have defined consumer behavior." + + +def test_versioning_error_dict_no_modified_value(): + campaign_v1 = { + 'type': 'campaign', + 'id': "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f", + 'created': "2016-04-06T20:03:00.000Z", + 'name': "Green Group Attacks Against Finance", + } + campaign_v2 = stix2.utils.new_version(campaign_v1, modified="2017-04-06T20:03:00.000Z") + + assert str(campaign_v2['modified']) == "2017-04-06T20:03:00.000Z" + + +def test_making_new_version_invalid_cls(): + campaign_v1 = "This is a campaign." + with pytest.raises(ValueError) as excinfo: + stix2.utils.new_version(campaign_v1, name="fred") + + assert 'cannot create new version of object of this type' in str(excinfo.value) + + +def test_revoke_dict(): + campaign_v1 = CAMPAIGN_MORE_KWARGS + campaign_v2 = stix2.utils.revoke(campaign_v1) + + assert campaign_v1['id'] == campaign_v2['id'] + assert campaign_v1['created_by_ref'] == campaign_v2['created_by_ref'] + assert campaign_v1['created'] == campaign_v2['created'] + assert campaign_v1['name'] == campaign_v2['name'] + assert campaign_v1['description'] == campaign_v2['description'] + assert stix2.utils.parse_into_datetime(campaign_v1['modified'], precision='millisecond') < campaign_v2['modified'] + + assert campaign_v2['revoked'] + + +def test_versioning_error_revoke_of_revoked_dict(): + campaign_v1 = CAMPAIGN_MORE_KWARGS + campaign_v2 = stix2.utils.revoke(campaign_v1) + + with pytest.raises(stix2.exceptions.RevokeError) as excinfo: + stix2.utils.revoke(campaign_v2) + + assert excinfo.value.called_by == "revoke" + + +def test_revoke_invalid_cls(): + campaign_v1 = "This is a campaign." + with pytest.raises(ValueError) as excinfo: + stix2.utils.revoke(campaign_v1) + + assert 'cannot revoke object of this type' in str(excinfo.value) + + +def test_remove_custom_stix_property(): + mal = stix2.Malware(name="ColePowers", + labels=["rootkit"], + is_family=False, + x_custom="armada", + allow_custom=True) + + mal_nc = stix2.utils.remove_custom_stix(mal) + + assert "x_custom" not in mal_nc + assert stix2.utils.parse_into_datetime(mal["modified"], precision="millisecond") < stix2.utils.parse_into_datetime(mal_nc["modified"], + precision="millisecond") + + +def test_remove_custom_stix_object(): + @stix2.CustomObject("x-animal", [ + ("species", stix2.properties.StringProperty(required=True)), + ("animal_class", stix2.properties.StringProperty()), + ]) + class Animal(object): + pass + + animal = Animal(species="lion", animal_class="mammal") + + nc = stix2.utils.remove_custom_stix(animal) + + assert nc is None + + +def test_remove_custom_stix_no_custom(): + campaign_v1 = stix2.Campaign(**CAMPAIGN_MORE_KWARGS) + campaign_v2 = stix2.utils.remove_custom_stix(campaign_v1) + + assert len(campaign_v1.keys()) == len(campaign_v2.keys()) + assert campaign_v1.id == campaign_v2.id + assert campaign_v1.description == campaign_v2.description diff --git a/stix2/test/v21/test_vulnerability.py b/stix2/test/v21/test_vulnerability.py new file mode 100644 index 0000000..daaef12 --- /dev/null +++ b/stix2/test/v21/test_vulnerability.py @@ -0,0 +1,69 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +from .constants import VULNERABILITY_ID + +EXPECTED = """{ + "type": "vulnerability", + "spec_version": "2.1", + "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "name": "CVE-2016-1234", + "external_references": [ + { + "source_name": "cve", + "external_id": "CVE-2016-1234" + } + ] +}""" + + +def test_vulnerability_example(): + vulnerability = stix2.Vulnerability( + id="vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + created="2016-05-12T08:17:27.000Z", + modified="2016-05-12T08:17:27.000Z", + name="CVE-2016-1234", + external_references=[ + stix2.ExternalReference(source_name='cve', + external_id="CVE-2016-1234"), + ], + ) + + assert str(vulnerability) == EXPECTED + + +@pytest.mark.parametrize("data", [ + EXPECTED, + { + "created": "2016-05-12T08:17:27Z", + "external_references": [ + { + "external_id": "CVE-2016-1234", + "source_name": "cve" + } + ], + "id": "vulnerability--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "modified": "2016-05-12T08:17:27Z", + "name": "CVE-2016-1234", + "spec_version": "2.1", + "type": "vulnerability" + }, +]) +def test_parse_vulnerability(data): + vuln = stix2.parse(data) + + assert vuln.type == 'vulnerability' + assert vuln.id == VULNERABILITY_ID + assert vuln.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert vuln.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert vuln.name == "CVE-2016-1234" + assert vuln.external_references[0].external_id == "CVE-2016-1234" + assert vuln.external_references[0].source_name == "cve" + +# TODO: Add other examples diff --git a/stix2/test/v21/test_workbench.py b/stix2/test/v21/test_workbench.py new file mode 100644 index 0000000..b8e511e --- /dev/null +++ b/stix2/test/v21/test_workbench.py @@ -0,0 +1,316 @@ +import os + +import stix2 +from stix2 import Bundle +from stix2.workbench import (AttackPattern, Campaign, CourseOfAction, + ExternalReference, FileSystemSource, Filter, + Identity, Indicator, IntrusionSet, Malware, + MarkingDefinition, ObservedData, Relationship, + Report, StatementMarking, ThreatActor, Tool, + Vulnerability, add_data_source, all_versions, + attack_patterns, campaigns, courses_of_action, + create, get, identities, indicators, + intrusion_sets, malware, observed_data, query, + reports, save, set_default_created, + set_default_creator, set_default_external_refs, + set_default_object_marking_refs, threat_actors, + tools, vulnerabilities) + +from .constants import (ATTACK_PATTERN_ID, ATTACK_PATTERN_KWARGS, CAMPAIGN_ID, + CAMPAIGN_KWARGS, COURSE_OF_ACTION_ID, + COURSE_OF_ACTION_KWARGS, IDENTITY_ID, IDENTITY_KWARGS, + INDICATOR_ID, INDICATOR_KWARGS, INTRUSION_SET_ID, + INTRUSION_SET_KWARGS, MALWARE_ID, MALWARE_KWARGS, + OBSERVED_DATA_ID, OBSERVED_DATA_KWARGS, REPORT_ID, + REPORT_KWARGS, THREAT_ACTOR_ID, THREAT_ACTOR_KWARGS, + TOOL_ID, TOOL_KWARGS, VULNERABILITY_ID, + VULNERABILITY_KWARGS) + + +def test_workbench_environment(): + + # Create a STIX object + ind = create(Indicator, id=INDICATOR_ID, **INDICATOR_KWARGS) + save(ind) + + resp = get(INDICATOR_ID) + assert resp['labels'][0] == 'malicious-activity' + + resp = all_versions(INDICATOR_ID) + assert len(resp) == 1 + + # Search on something other than id + q = [Filter('type', '=', 'vulnerability')] + resp = query(q) + assert len(resp) == 0 + + +def test_workbench_get_all_attack_patterns(): + mal = AttackPattern(id=ATTACK_PATTERN_ID, **ATTACK_PATTERN_KWARGS) + save(mal) + + resp = attack_patterns() + assert len(resp) == 1 + assert resp[0].id == ATTACK_PATTERN_ID + + +def test_workbench_get_all_campaigns(): + cam = Campaign(id=CAMPAIGN_ID, **CAMPAIGN_KWARGS) + save(cam) + + resp = campaigns() + assert len(resp) == 1 + assert resp[0].id == CAMPAIGN_ID + + +def test_workbench_get_all_courses_of_action(): + coa = CourseOfAction(id=COURSE_OF_ACTION_ID, **COURSE_OF_ACTION_KWARGS) + save(coa) + + resp = courses_of_action() + assert len(resp) == 1 + assert resp[0].id == COURSE_OF_ACTION_ID + + +def test_workbench_get_all_identities(): + idty = Identity(id=IDENTITY_ID, **IDENTITY_KWARGS) + save(idty) + + resp = identities() + assert len(resp) == 1 + assert resp[0].id == IDENTITY_ID + + +def test_workbench_get_all_indicators(): + resp = indicators() + assert len(resp) == 1 + assert resp[0].id == INDICATOR_ID + + +def test_workbench_get_all_intrusion_sets(): + ins = IntrusionSet(id=INTRUSION_SET_ID, **INTRUSION_SET_KWARGS) + save(ins) + + resp = intrusion_sets() + assert len(resp) == 1 + assert resp[0].id == INTRUSION_SET_ID + + +def test_workbench_get_all_malware(): + mal = Malware(id=MALWARE_ID, **MALWARE_KWARGS) + save(mal) + + resp = malware() + assert len(resp) == 1 + assert resp[0].id == MALWARE_ID + + +def test_workbench_get_all_observed_data(): + od = ObservedData(id=OBSERVED_DATA_ID, **OBSERVED_DATA_KWARGS) + save(od) + + resp = observed_data() + assert len(resp) == 1 + assert resp[0].id == OBSERVED_DATA_ID + + +def test_workbench_get_all_reports(): + rep = Report(id=REPORT_ID, **REPORT_KWARGS) + save(rep) + + resp = reports() + assert len(resp) == 1 + assert resp[0].id == REPORT_ID + + +def test_workbench_get_all_threat_actors(): + thr = ThreatActor(id=THREAT_ACTOR_ID, **THREAT_ACTOR_KWARGS) + save(thr) + + resp = threat_actors() + assert len(resp) == 1 + assert resp[0].id == THREAT_ACTOR_ID + + +def test_workbench_get_all_tools(): + tool = Tool(id=TOOL_ID, **TOOL_KWARGS) + save(tool) + + resp = tools() + assert len(resp) == 1 + assert resp[0].id == TOOL_ID + + +def test_workbench_get_all_vulnerabilities(): + vuln = Vulnerability(id=VULNERABILITY_ID, **VULNERABILITY_KWARGS) + save(vuln) + + resp = vulnerabilities() + assert len(resp) == 1 + assert resp[0].id == VULNERABILITY_ID + + +def test_workbench_add_to_bundle(): + vuln = Vulnerability(**VULNERABILITY_KWARGS) + bundle = Bundle(vuln) + assert bundle.objects[0].name == 'Heartbleed' + + +def test_workbench_relationships(): + rel = Relationship(INDICATOR_ID, 'indicates', MALWARE_ID) + save(rel) + + ind = get(INDICATOR_ID) + resp = ind.relationships() + assert len(resp) == 1 + assert resp[0].relationship_type == 'indicates' + assert resp[0].source_ref == INDICATOR_ID + assert resp[0].target_ref == MALWARE_ID + + +def test_workbench_created_by(): + intset = IntrusionSet(name="Breach 123", created_by_ref=IDENTITY_ID) + save(intset) + creator = intset.created_by() + assert creator.id == IDENTITY_ID + + +def test_workbench_related(): + rel1 = Relationship(MALWARE_ID, 'targets', IDENTITY_ID) + rel2 = Relationship(CAMPAIGN_ID, 'uses', MALWARE_ID) + save([rel1, rel2]) + + resp = get(MALWARE_ID).related() + assert len(resp) == 3 + assert any(x['id'] == CAMPAIGN_ID for x in resp) + assert any(x['id'] == INDICATOR_ID for x in resp) + assert any(x['id'] == IDENTITY_ID for x in resp) + + resp = get(MALWARE_ID).related(relationship_type='indicates') + assert len(resp) == 1 + + +def test_workbench_related_with_filters(): + malware = Malware(labels=["ransomware"], name="CryptorBit", created_by_ref=IDENTITY_ID, + is_family=False) + rel = Relationship(malware.id, 'variant-of', MALWARE_ID) + save([malware, rel]) + + filters = [Filter('created_by_ref', '=', IDENTITY_ID)] + resp = get(MALWARE_ID).related(filters=filters) + + assert len(resp) == 1 + assert resp[0].name == malware.name + assert resp[0].created_by_ref == IDENTITY_ID + + # filters arg can also be single filter + resp = get(MALWARE_ID).related(filters=filters[0]) + assert len(resp) == 1 + + +def test_add_data_source(): + fs_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stix2_data") + fs = FileSystemSource(fs_path) + add_data_source(fs) + + resp = tools() + assert len(resp) == 3 + resp_ids = [tool.id for tool in resp] + assert TOOL_ID in resp_ids + assert 'tool--03342581-f790-4f03-ba41-e82e67392e23' in resp_ids + assert 'tool--242f3da3-4425-4d11-8f5c-b842886da966' in resp_ids + + +def test_additional_filter(): + resp = tools(Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5')) + assert len(resp) == 2 + + +def test_additional_filters_list(): + resp = tools([Filter('created_by_ref', '=', 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5'), + Filter('name', '=', 'Windows Credential Editor')]) + assert len(resp) == 1 + + +def test_default_creator(): + set_default_creator(IDENTITY_ID) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert 'created_by_ref' not in CAMPAIGN_KWARGS + assert campaign.created_by_ref == IDENTITY_ID + + +def test_default_created_timestamp(): + timestamp = "2018-03-19T01:02:03.000Z" + set_default_created(timestamp) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert 'created' not in CAMPAIGN_KWARGS + assert stix2.utils.format_datetime(campaign.created) == timestamp + assert stix2.utils.format_datetime(campaign.modified) == timestamp + + +def test_default_external_refs(): + ext_ref = ExternalReference(source_name="ACME Threat Intel", + description="Threat report") + set_default_external_refs(ext_ref) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert campaign.external_references[0].source_name == "ACME Threat Intel" + assert campaign.external_references[0].description == "Threat report" + + +def test_default_object_marking_refs(): + stmt_marking = StatementMarking("Copyright 2016, Example Corp") + mark_def = MarkingDefinition(definition_type="statement", + definition=stmt_marking) + set_default_object_marking_refs(mark_def) + campaign = Campaign(**CAMPAIGN_KWARGS) + + assert campaign.object_marking_refs[0] == mark_def.id + + +def test_workbench_custom_property_object_in_observable_extension(): + ntfs = stix2.NTFSExt( + allow_custom=True, + sid=1, + x_foo='bar', + ) + artifact = stix2.File( + name='test', + extensions={'ntfs-ext': ntfs}, + ) + observed_data = ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=0, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data) + + +def test_workbench_custom_property_dict_in_observable_extension(): + artifact = stix2.File( + allow_custom=True, + name='test', + extensions={ + 'ntfs-ext': { + 'allow_custom': True, + 'sid': 1, + 'x_foo': 'bar', + } + }, + ) + observed_data = ObservedData( + allow_custom=True, + first_observed="2015-12-21T19:00:00Z", + last_observed="2015-12-21T19:00:00Z", + number_observed=0, + objects={"0": artifact}, + ) + + assert observed_data.objects['0'].extensions['ntfs-ext'].x_foo == "bar" + assert '"x_foo": "bar"' in str(observed_data)