diff --git a/stix2/confidence/scales.py b/stix2/confidence/scales.py index 6d8882d..4880f33 100644 --- a/stix2/confidence/scales.py +++ b/stix2/confidence/scales.py @@ -228,7 +228,7 @@ def admiralty_credibility_to_value(scale_value): """ if scale_value == "6 - Truth cannot be judged": - pass # TODO: Ask what happens here! + raise ValueError("STIX Confidence value cannot be determined for %s" % scale_value) # TODO: What happens here? elif scale_value == "5 - Improbable": return 10 elif scale_value == "4 - Doubtful": @@ -270,7 +270,7 @@ def value_to_admiralty_credibility(confidence_value): ValueError: If `confidence_value` is out of bounds. """ - # TODO: Ask what happens with "6 - Truth cannot be judged" ! + # TODO: Case "6 - Truth cannot be judged" if 19 >= confidence_value >= 0: return "5 - Improbable" elif 39 >= confidence_value >= 20: diff --git a/stix2/test/constants.py b/stix2/test/constants.py index 839b547..7ced397 100644 --- a/stix2/test/constants.py +++ b/stix2/test/constants.py @@ -10,9 +10,12 @@ 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" diff --git a/stix2/test/test_confidence.py b/stix2/test/test_confidence.py new file mode 100644 index 0000000..b4146a5 --- /dev/null +++ b/stix2/test/test_confidence.py @@ -0,0 +1,288 @@ +import pytest + +from stix2.confidence.scales import (admiralty_credibility_to_value, + dni_to_value, none_low_med_high_to_value, + value_to_admiralty_credibility, + value_to_dni, + value_to_none_low_medium_high, + value_to_wep, value_to_zero_ten, + wep_to_value, zero_ten_to_value) + +CONFIDENCE_ERROR_STR = "STIX Confidence value cannot be determined for %s" +RANGE_ERROR_STR = "Range of values out of bounds: %s" + + +def _between(x, val, y): + return x >= val >= y + + +def test_confidence_range_none_low_med_high(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_none_low_medium_high(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if val == 0: + assert value_to_none_low_medium_high(val) == "None" + elif _between(29, val, 1): + assert value_to_none_low_medium_high(val) == "Low" + elif _between(69, val, 30): + assert value_to_none_low_medium_high(val) == "Med" + elif _between(100, val, 70): + assert value_to_none_low_medium_high(val) == "High" + else: + pytest.fail("Unexpected behavior %s" % val) + + +@pytest.mark.parametrize("scale_value,result", [ + ("None", 0), + ("Low", 15), + ("Med", 50), + ("High", 85) +]) +def test_confidence_scale_valid_none_low_med_high(scale_value, result): + val = none_low_med_high_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize("scale_value", [ + "Super", + "none", + "" +]) +def test_confidence_scale_invalid_none_low_med_high(scale_value): + with pytest.raises(ValueError) as excinfo: + none_low_med_high_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_zero_ten(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_zero_ten(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if _between(4, val, 0): + assert value_to_zero_ten(val) == "0" + elif _between(14, val, 5): + assert value_to_zero_ten(val) == "1" + elif _between(24, val, 15): + assert value_to_zero_ten(val) == "2" + elif _between(34, val, 25): + assert value_to_zero_ten(val) == "3" + elif _between(44, val, 35): + assert value_to_zero_ten(val) == "4" + elif _between(54, val, 45): + assert value_to_zero_ten(val) == "5" + elif _between(64, val, 55): + assert value_to_zero_ten(val) == "6" + elif _between(74, val, 65): + assert value_to_zero_ten(val) == "7" + elif _between(84, val, 75): + assert value_to_zero_ten(val) == "8" + elif _between(94, val, 85): + assert value_to_zero_ten(val) == "9" + elif _between(100, val, 95): + assert value_to_zero_ten(val) == "10" + else: + pytest.fail("Unexpected behavior %s" % val) + + +@pytest.mark.parametrize("scale_value,result", [ + ("0", 0), + ("1", 10), + ("2", 20), + ("3", 30), + ("4", 40), + ("5", 50), + ("6", 60), + ("7", 70), + ("8", 80), + ("9", 90), + ("10", 100) +]) +def test_confidence_scale_valid_zero_ten(scale_value, result): + val = zero_ten_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize("scale_value", [ + "11", + 8, + "" +]) +def test_confidence_scale_invalid_zero_ten(scale_value): + with pytest.raises(ValueError) as excinfo: + zero_ten_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_admiralty_credibility(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_admiralty_credibility(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if _between(19, val, 0): + assert value_to_admiralty_credibility(val) == "5 - Improbable" + elif _between(39, val, 20): + assert value_to_admiralty_credibility(val) == "4 - Doubtful" + elif _between(59, val, 40): + assert value_to_admiralty_credibility(val) == "3 - Possibly True" + elif _between(79, val, 60): + assert value_to_admiralty_credibility(val) == "2 - Probably True" + elif _between(100, val, 80): + assert value_to_admiralty_credibility(val) == "1 - Confirmed by other sources" + else: + pytest.fail("Unexpected behavior %s" % val) + + +@pytest.mark.parametrize("scale_value,result", [ + ("5 - Improbable", 10), + ("4 - Doubtful", 30), + ("3 - Possibly True", 50), + ("2 - Probably True", 70), + ("1 - Confirmed by other sources", 90) +]) +def test_confidence_scale_valid_admiralty_credibility(scale_value, result): + val = admiralty_credibility_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize("scale_value", [ + "5 - improbable", + "6 - Truth cannot be judged", + "" +]) +def test_confidence_scale_invalid_admiralty_credibility(scale_value): + with pytest.raises(ValueError) as excinfo: + admiralty_credibility_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_wep(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_wep(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if val == 0: + assert value_to_wep(val) == "Impossible" + elif _between(19, val, 1): + assert value_to_wep(val) == "Highly Unlikely/Almost Certainly Not" + elif _between(39, val, 20): + assert value_to_wep(val) == "Unlikely/Probably Not" + elif _between(59, val, 40): + assert value_to_wep(val) == "Even Chance" + elif _between(79, val, 60): + assert value_to_wep(val) == "Likely/Probable" + elif _between(99, val, 80): + assert value_to_wep(val) == "Highly likely/Almost Certain" + elif val == 100: + assert value_to_wep(val) == "Certain" + else: + pytest.fail("Unexpected behavior %s" % val) + + +@pytest.mark.parametrize("scale_value,result", [ + ("Impossible", 0), + ("Highly Unlikely/Almost Certainly Not", 10), + ("Unlikely/Probably Not", 30), + ("Even Chance", 50), + ("Likely/Probable", 70), + ("Highly likely/Almost Certain", 90), + ("Certain", 100) +]) +def test_confidence_scale_valid_wep(scale_value, result): + val = wep_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize("scale_value", [ + "Unlikely / Probably Not", + "Almost certain", + "" +]) +def test_confidence_scale_invalid_wep(scale_value): + with pytest.raises(ValueError) as excinfo: + wep_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value + + +def test_confidence_range_dni(): + confidence_range = range(-1, 101) + + for val in confidence_range: + if val < 0 or val > 100: + with pytest.raises(ValueError) as excinfo: + value_to_dni(val) + + assert str(excinfo.value) == RANGE_ERROR_STR % val + continue + + if _between(9, val, 0): + assert value_to_dni(val) == "Almost No Chance / Remote" + elif _between(19, val, 10): + assert value_to_dni(val) == "Very Unlikely / Highly Improbable" + elif _between(39, val, 20): + assert value_to_dni(val) == "Unlikely / Improbable" + elif _between(59, val, 40): + assert value_to_dni(val) == "Roughly Even Change / Roughly Even Odds" + elif _between(79, val, 60): + assert value_to_dni(val) == "Likely / Probable" + elif _between(89, val, 80): + assert value_to_dni(val) == "Very Likely / Highly Probable" + elif _between(100, val, 90): + assert value_to_dni(val) == "Almost Certain / Nearly Certain" + else: + pytest.fail("Unexpected behavior %s" % val) + + +@pytest.mark.parametrize("scale_value,result", [ + ("Almost No Chance / Remote", 5), + ("Very Unlikely / Highly Improbable", 15), + ("Unlikely / Improbable", 30), + ("Roughly Even Change / Roughly Even Odds", 50), + ("Likely / Probable", 70), + ("Very Likely / Highly Probable", 85), + ("Almost Certain / Nearly Certain", 95) +]) +def test_confidence_scale_valid_dni(scale_value, result): + val = dni_to_value(scale_value) + assert val == result + + +@pytest.mark.parametrize("scale_value", [ + "Almost Certain/Nearly Certain", + "Almost Certain / nearly Certain", + "" +]) +def test_confidence_scale_invalid_none_dni(scale_value): + with pytest.raises(ValueError) as excinfo: + dni_to_value(scale_value) + + assert str(excinfo.value) == CONFIDENCE_ERROR_STR % scale_value diff --git a/stix2/test/test_language_content.py b/stix2/test/test_language_content.py new file mode 100644 index 0000000..2343d69 --- /dev/null +++ b/stix2/test/test_language_content.py @@ -0,0 +1,68 @@ +import datetime as dt + +import pytest +import pytz + +import stix2 + +CAMPAIGN_ID = "campaign--12a111f0-b824-4baf-a224-83b80237a094" + +LANGUAGE_CONTENT_ID = "language-content--b86bd89f-98bb-4fa9-8cb2-9ad421da981d" + +TEST_CAMPAIGN = """{ + "type": "campaign", + "id": "campaign--12a111f0-b824-4baf-a224-83b80237a094", + "lang": "en", + "created": "2017-02-08T21:31:22.007Z", + "modified": "2017-02-08T21:31:22.007Z", + "name": "Bank Attack", + "description": "More information about bank attack" +}""" + +TEST_LANGUAGE_CONTENT = """{ + "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" + } + } +}""" + + +@pytest.mark.xfail(reason="Dictionary keys are too short") +def test_language_content_campaign(): + now = dt.datetime(2017, 2, 8, 21, 31, 22, microsecond=7000, tzinfo=pytz.utc) + + lc = stix2.LanguageContent( + type='language-content', + id=LANGUAGE_CONTENT_ID, + created=now, + modified=now, + object_ref=CAMPAIGN_ID, + object_modified=now, + 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" + } + } + ) + + camp = stix2.parse(TEST_CAMPAIGN) + + assert str(lc) in TEST_LANGUAGE_CONTENT + assert lc.modified == camp.modified diff --git a/stix2/test/test_location.py b/stix2/test/test_location.py index e69de29..8ce44d7 100644 --- a/stix2/test/test_location.py +++ b/stix2/test/test_location.py @@ -0,0 +1,82 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import LOCATION_ID + + +EXPECTED_LOCATION_1 = """{ + "type": "location", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "latitude": 48.8566, + "longitude": 2.3522 +}""" + +EXPECTED_LOCATION_1_REPR = "Location(" + " ".join(""" + type='location', + id='location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64', + created='2016-04-06T20:03:00.000Z', + modified='2016-04-06T20:03:00.000Z', + latitude=48.8566, + longitude=2.3522""".split()) + ")" + +EXPECTED_LOCATION_2 = """{ + "type": "location", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "region": "north-america" +} +""" + +EXPECTED_LOCATION_2_REPR = "Location(" + " ".join(""" + type='location', + id='location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64', + created='2016-04-06T20:03:00.000Z', + modified='2016-04-06T20:03:00.000Z', + region='north-america'""".split()) + ")" + + +def test_location_with_some_required_properties(): + now = dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + + loc = stix2.Location( + type="location", + id=LOCATION_ID, + created=now, + modified=now, + latitude=48.8566, + longitude=2.3522 + ) + + assert str(loc) == EXPECTED_LOCATION_1 + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(loc)) + assert rep == EXPECTED_LOCATION_1_REPR + + +@pytest.mark.parametrize("data", [ + EXPECTED_LOCATION_2, + { + "type": "location", + "id": "location--a6e9345f-5a15-4c29-8bb3-7dcc5d168d64", + "created": "2016-04-06T20:03:00.000Z", + "modified": "2016-04-06T20:03:00.000Z", + "region": "north-america" + } +]) +def test_parse_location(data): + location = stix2.parse(data) + + assert location.type == 'location' + assert location.id == LOCATION_ID + assert location.created == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert location.modified == dt.datetime(2016, 4, 6, 20, 3, 0, tzinfo=pytz.utc) + assert location.region == 'north-america' + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(location)) + assert rep == EXPECTED_LOCATION_2_REPR diff --git a/stix2/test/test_note.py b/stix2/test/test_note.py index e69de29..df117d8 100644 --- a/stix2/test/test_note.py +++ b/stix2/test/test_note.py @@ -0,0 +1,110 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import CAMPAIGN_ID, NOTE_ID + +DESCRIPTION = ('This note indicates the various steps taken by the threat' + ' analyst team to investigate this specific campaign. Step' + ' 1) Do a scan 2) Review scanned results for identified ' + 'hosts not known by external intel... etc') + +EXPECTED_NOTE = """{ + "type": "note", + "id": "note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "summary": "Tracking Team Note#1", + "description": "%s", + "authors": [ + "John Doe" + ], + "object_refs": [ + "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" + ], + "external_references": [ + { + "source_name": "job-tracker", + "external_id": "job-id-1234" + } + ] +}""" % DESCRIPTION + +EXPECTED_OPINION_REPR = "Note(" + " ".join((""" + type='note', + id='note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061', + created='2016-05-12T08:17:27.000Z', + modified='2016-05-12T08:17:27.000Z', + summary='Tracking Team Note#1', + description='%s', + authors=['John Doe'], + object_refs=['campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f'], + external_references=[ExternalReference(source_name='job-tracker', external_id='job-id-1234')] +""" % DESCRIPTION).split()) + ")" + + +def test_note_with_required_properties(): + now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + + note = stix2.Note( + type='note', + id=NOTE_ID, + created=now, + modified=now, + summary='Tracking Team Note#1', + object_refs=[CAMPAIGN_ID], + authors=['John Doe'], + description=DESCRIPTION, + external_references=[ + { + 'source_name': 'job-tracker', + 'external_id': 'job-id-1234' + } + ] + ) + + assert str(note) == EXPECTED_NOTE + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(note)) + assert rep == EXPECTED_OPINION_REPR + + +@pytest.mark.parametrize("data", [ + EXPECTED_NOTE, + { + "type": "note", + "id": "note--0c7b5b88-8ff7-4a4d-aa9d-feb398cd0061", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "summary": "Tracking Team Note#1", + "description": DESCRIPTION, + "authors": [ + "John Doe" + ], + "object_refs": [ + "campaign--8e2e2d2b-17d4-4cbf-938f-98ee46b3cd3f" + ], + "external_references": [ + { + "source_name": "job-tracker", + "external_id": "job-id-1234" + } + ] + } +]) +def test_parse_note(data): + note = stix2.parse(data) + + assert note.type == 'note' + assert note.id == NOTE_ID + assert note.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert note.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert note.object_refs[0] == CAMPAIGN_ID + assert note.authors[0] == 'John Doe' + assert note.summary == 'Tracking Team Note#1' + assert note.description == DESCRIPTION + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(note)) + assert rep == EXPECTED_OPINION_REPR diff --git a/stix2/test/test_opinion.py b/stix2/test/test_opinion.py index e69de29..7de415a 100644 --- a/stix2/test/test_opinion.py +++ b/stix2/test/test_opinion.py @@ -0,0 +1,82 @@ +import datetime as dt +import re + +import pytest +import pytz + +import stix2 + +from .constants import OPINION_ID + +DESCRIPTION = ('This doesn\'t seem like it is feasible. We\'ve seen how ' + 'PandaCat has attacked Spanish infrastructure over the ' + 'last 3 years, so this change in targeting seems too great' + ' to be viable. The methods used are more commonly ' + 'associated with the FlameDragonCrew.') + +EXPECTED_OPINION = """{ + "type": "opinion", + "id": "opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "description": "%s", + "object_refs": [ + "relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471" + ], + "opinion": "strongly-disagree" +}""" % DESCRIPTION + +EXPECTED_OPINION_REPR = "Opinion(" + " ".join((""" + type='opinion', + id='opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7', + created='2016-05-12T08:17:27.000Z', + modified='2016-05-12T08:17:27.000Z', + description="%s", + object_refs=['relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471'], + opinion='strongly-disagree'""" % DESCRIPTION).split()) + ")" + + +def test_opinion_with_required_properties(): + now = dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + + opi = stix2.Opinion( + type='opinion', + id=OPINION_ID, + created=now, + modified=now, + object_refs=['relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471'], + opinion='strongly-disagree', + description=DESCRIPTION + ) + + assert str(opi) == EXPECTED_OPINION + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(opi)) + assert rep == EXPECTED_OPINION_REPR + + +@pytest.mark.parametrize("data", [ + EXPECTED_OPINION, + { + "type": "opinion", + "id": "opinion--b01efc25-77b4-4003-b18b-f6e24b5cd9f7", + "created": "2016-05-12T08:17:27.000Z", + "modified": "2016-05-12T08:17:27.000Z", + "description": DESCRIPTION, + "object_refs": [ + "relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471" + ], + "opinion": "strongly-disagree" + } +]) +def test_parse_opinion(data): + opinion = stix2.parse(data) + + assert opinion.type == 'opinion' + assert opinion.id == OPINION_ID + assert opinion.created == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert opinion.modified == dt.datetime(2016, 5, 12, 8, 17, 27, tzinfo=pytz.utc) + assert opinion.opinion == 'strongly-disagree' + assert opinion.object_refs[0] == 'relationship--16d2358f-3b0d-4c88-b047-0da2f7ed4471' + assert opinion.description == DESCRIPTION + rep = re.sub(r"(\[|=| )u('|\"|\\\'|\\\")", r"\g<1>\g<2>", repr(opinion)) + assert rep == EXPECTED_OPINION_REPR