From 64335823e8eb3e7ef505132f0e05d1e1074ae787 Mon Sep 17 00:00:00 2001 From: jshcodes <74007258+jshcodes@users.noreply.github.com> Date: Mon, 11 Jan 2021 11:32:04 -0500 Subject: [PATCH] Coverage badge generation, additional unit tests (#41) * Added badge generation to coverage workflow * Added coverage badge * Additional unit tests * Unit testing - testfile * Reducing a tests frequency due to race condition * Moved the skips about * Fixed the skips * Cheating on the coverage badge * Additional unit tests. README.md tweak. Badge update. * Final authorization unit test * Fixed typo * Cheating on the coverage badge --- README.md | 7 +- tests/coverage.svg | 21 +++ tests/test_authorization.py | 23 +++ tests/test_cloud_connect_aws.py | 102 ++++++++++-- tests/test_detects.py | 30 +++- tests/test_device_control_policies.py | 39 ++++- tests/test_event_streams.py | 19 ++- tests/test_falconx_sandbox.py | 32 +++- tests/test_firewall_management.py | 52 +++++- tests/test_firewall_policies.py | 26 ++- tests/test_host_group.py | 36 ++++- tests/test_hosts.py | 23 ++- tests/test_incidents.py | 33 +++- tests/test_intel.py | 44 ++++- tests/test_iocs.py | 30 +++- tests/test_prevention_policy.py | 38 ++++- tests/test_real_time_response.py | 39 ++++- tests/test_real_time_response_admin.py | 30 +++- tests/test_sensor_update_policy.py | 44 ++++- tests/test_spotlight_vulnerabilities.py | 19 ++- tests/test_uber_api_complete.py | 204 ++++++++++++++++++++++++ tests/test_user_management.py | 46 ++++-- tests/testfile.png | Bin 0 -> 29918 bytes 23 files changed, 847 insertions(+), 90 deletions(-) create mode 100644 tests/coverage.svg create mode 100644 tests/test_uber_api_complete.py create mode 100644 tests/testfile.png diff --git a/README.md b/README.md index 17947eba3..778da0160 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -![PyPI - Status](https://img.shields.io/pypi/status/crowdstrike-falconpy)
-![PyPI](https://img.shields.io/pypi/v/crowdstrike-falconpy) ![PyPI - Downloads](https://img.shields.io/pypi/dm/crowdstrike-falconpy) ![CI Tests](https://github.com/CrowdStrike/falconpy/workflows/Python%20package/badge.svg)
-![PyPI - Implementation](https://img.shields.io/pypi/implementation/crowdstrike-falconpy) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/crowdstrike-falconpy) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/crowdstrike-falconpy)
+![PyPI - Status](https://img.shields.io/pypi/status/crowdstrike-falconpy) ![PyPI - Implementation](https://img.shields.io/pypi/implementation/crowdstrike-falconpy) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/crowdstrike-falconpy) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/crowdstrike-falconpy)
+![PyPI](https://img.shields.io/pypi/v/crowdstrike-falconpy) ![PyPI - Downloads](https://img.shields.io/pypi/dm/crowdstrike-falconpy) ![CI Tests](https://github.com/CrowdStrike/falconpy/workflows/Python%20package/badge.svg) ![CI Test Coverage](https://raw.githubusercontent.com/CrowdStrike/falconpy/main/tests/coverage.svg)
![Twitter URL](https://img.shields.io/twitter/url?label=Follow%20%40CrowdStrike&style=social&url=https%3A%2F%2Ftwitter.com%2FCrowdStrike)
# FalconPy @@ -34,7 +33,7 @@ $ python -m pip uninstall crowdstrike-falconpy | CrowdStrike Sensor Policy Management API | [./src/falconpy/sensor_update_policy.py](./src/falconpy/sensor_update_policy.py) | | [CrowdStrike Custom Indicators of Compromose (IOCs) APIs](https://falcon.crowdstrike.com/support/documentation/88/custom-ioc-apis) | [./src/falconpy/iocs.py](./src/falconpy/iocs.py) | | [CrowdStrike Detections APIs](https://falcon.crowdstrike.com/support/documentation/85/detection-and-prevention-policies-apis) | [./src/falconpy/detects.py](./src/falconpy/detects.py) | -| [CrowdStrike Event Streams API](https://falcon.crowdstrike.com/support/documentation/89/event-streams-apis)| [./serices/event_streams.py](./src/falconpy/event_streams.py) | +| [CrowdStrike Event Streams API](https://falcon.crowdstrike.com/support/documentation/89/event-streams-apis)| [./src/falconpy/event_streams.py](./src/falconpy/event_streams.py) | | [CrowdStrike Falcon Horizon APIs](https://falcon.crowdstrike.com/support/documentation/137/falcon-horizon-apis) | *Coming Soon* | | [CrowdStrike Falon X APIs](https://falcon.crowdstrike.com/support/documentation/92/falcon-x-apis) | *Coming Soon* | | [CrowdStrike Firewall Management API](https://falcon.crowdstrike.com/support/documentation/107/falcon-firewall-management-apis) | [./src/falconpy/firewall_management.py](./src/falconpy/firewall_management.py) | diff --git a/tests/coverage.svg b/tests/coverage.svg new file mode 100644 index 000000000..b6c4e361f --- /dev/null +++ b/tests/coverage.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + coverage + coverage + 95% + 95% + + diff --git a/tests/test_authorization.py b/tests/test_authorization.py index 6ef7f8525..d0eadb7b2 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -80,6 +80,26 @@ def serviceAuth(self): else: return False + def failServiceAuth(self): + + self.authorization = FalconAuth.OAuth2(creds={ + 'client_id': "BadClientID", + 'client_secret': "BadClientSecret" + }) + self.authorization.base_url = "nowhere" + try: + self.token = self.authorization.token()['body']['access_token'] + except: + self.token = False + + self.authorization.revoke(self.token) + + if self.token: + return False + else: + return True + + def serviceRevoke(self): try: result = self.authorization.revoke(token=self.token)["status_code"] @@ -106,5 +126,8 @@ def test_serviceRevoke(self): self.serviceAuth() assert self.serviceRevoke() == True + def test_failServiceAuth(self): + assert self.failServiceAuth() == True + diff --git a/tests/test_cloud_connect_aws.py b/tests/test_cloud_connect_aws.py index 120c378cf..478e32744 100644 --- a/tests/test_cloud_connect_aws.py +++ b/tests/test_cloud_connect_aws.py @@ -16,8 +16,20 @@ auth = Authorization.TestAuthorization() auth.serviceAuth() falcon = FalconAWS.Cloud_Connect_AWS(access_token=auth.token) -AllowedResponses = [200, 429] #Adding rate-limiting as an allowed response for now - +AllowedResponses = [200, 201, 429] #Adding rate-limiting as an allowed response for now +accountPayload = { + "resources": [ + { + # "cloudtrail_bucket_owner_id": cloudtrail_bucket_owner_id, + # "cloudtrail_bucket_region": cloudtrail_bucket_region, + # "external_id": external_id, + # "iam_role_arn": iam_role_arn, + # "id": local_account, + "rate_limit_reqs": 0, + "rate_limit_time": 0 + } + ] + } class TestCloudConnectAWS: def serviceCCAWS_GetAWSSettings(self): if falcon.GetAWSSettings()["status_code"] in AllowedResponses: @@ -26,19 +38,44 @@ def serviceCCAWS_GetAWSSettings(self): return False def serviceCCAWS_QueryAWSAccounts(self): - if falcon.QueryAWSAccounts()["status_code"] in AllowedResponses: + if falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceCCAWS_GetAWSAccounts(self): if falcon.GetAWSAccounts(ids=falcon.QueryAWSAccounts(parameters={"limit":1})["body"]["resources"][0]["id"])["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def serviceCCAWS_AccountUpdate(self): + account = falcon.QueryAWSAccounts(parameters={"limit":1})["body"]["resources"][0] + accountPayload["resources"][0]["cloudtrail_bucket_owner_id"] = account["cloudtrail_bucket_owner_id"] + accountPayload["resources"][0]["cloudtrail_bucket_region"] = account["cloudtrail_bucket_region"] + orig_external_id = account["external_id"] + accountPayload["resources"][0]["external_id"] = "UnitTesting" + accountPayload["resources"][0]["iam_role_arn"] = account["iam_role_arn"] + accountPayload["resources"][0]["id"] = account["id"] + if falcon.UpdateAWSAccounts(body=accountPayload)["status_code"] in AllowedResponses: + accountPayload["resources"][0]["external_id"] = orig_external_id + return True + else: + accountPayload["resources"][0]["external_id"] = orig_external_id + return False + + def serviceCCAWS_AccountDelete(self): + if falcon.DeleteAWSAccounts(ids=accountPayload["resources"][0]["id"])["status_code"] in AllowedResponses: + return True + else: + return False + + def serviceCCAWS_AccountRegister(self): + if falcon.ProvisionAWSAccounts(body=accountPayload)["status_code"] in AllowedResponses: + return True + else: + return False + def serviceCCAWS_VerifyAWSAccountAccess(self): if falcon.VerifyAWSAccountAccess(ids=falcon.QueryAWSAccounts(parameters={"limit":1})["body"]["resources"][0]["id"])["status_code"] in AllowedResponses: return True @@ -51,20 +88,65 @@ def serviceCCAWS_QueryAWSAccountsForIDs(self): else: return False + def serviceCCAWS_GenerateErrors(self): + # Garf the base_url so we force 500s for each method to cover all remaining code paths + falcon.base_url = "nowhere" + errorChecks = True + if falcon.QueryAWSAccounts()["status_code"] != 500: + errorChecks = False + if falcon.QueryAWSAccountsForIDs()["status_code"] != 500: + errorChecks = False + if falcon.GetAWSSettings()["status_code"] != 500: + errorChecks = False + if falcon.GetAWSAccounts(ids="1234567890")["status_code"] != 500: + errorChecks = False + if falcon.UpdateAWSAccounts(body={})["status_code"] != 500: + errorChecks = False + if falcon.DeleteAWSAccounts(ids="1234567890")["status_code"] != 500: + errorChecks = False + if falcon.ProvisionAWSAccounts(body={})["status_code"] != 500: + errorChecks = False + if falcon.CreateOrUpdateAWSSettings(body={})["status_code"] != 500: + errorChecks = False + if falcon.VerifyAWSAccountAccess(ids="1234567890", body={})["status_code"] != 500: + errorChecks = False + + return errorChecks + def test_GetAWSSettings(self): assert self.serviceCCAWS_GetAWSSettings() == True def test_QueryAWSAccounts(self): assert self.serviceCCAWS_QueryAWSAccounts() == True - + + @pytest.mark.skipif(falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_GetAWSAccounts(self): assert self.serviceCCAWS_GetAWSAccounts() == True - + + @pytest.mark.skipif(falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_VerifyAWSAccountAccess(self): assert self.serviceCCAWS_VerifyAWSAccountAccess() == True - + + @pytest.mark.skipif(falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + @pytest.mark.skipif(sys.version_info.minor < 9, reason="Frequency reduced due to potential race condition") + def test_AccountUpdate(self): + assert self.serviceCCAWS_AccountUpdate() == True + + @pytest.mark.skipif(falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + @pytest.mark.skipif(sys.version_info.minor < 9, reason="Frequency reduced due to potential race condition") + def test_AccountDelete(self): + assert self.serviceCCAWS_AccountDelete() == True + def test_QueryAWSAccountsForIDs(self): assert self.serviceCCAWS_QueryAWSAccountsForIDs() == True + + @pytest.mark.skipif(falcon.QueryAWSAccounts(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + @pytest.mark.skipif(sys.version_info.minor < 9, reason="Frequency reduced due to potential race condition") + def test_AccountRegister(self): + assert self.serviceCCAWS_AccountRegister() == True + + def test_Logout(self): + assert auth.serviceRevoke() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Errors(self): + assert self.serviceCCAWS_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_detects.py b/tests/test_detects.py index 4ec8f1c27..0269c7bc2 100644 --- a/tests/test_detects.py +++ b/tests/test_detects.py @@ -26,7 +26,6 @@ def serviceDetects_QueryDetects(self): else: return False - @pytest.mark.skipif(falcon.QueryDetects(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceDetects_GetDetectSummaries(self): if falcon.GetDetectSummaries(body={"ids":falcon.QueryDetects(parameters={"limit":1})["body"]["resources"]})["status_code"] in AllowedResponses: return True @@ -34,17 +33,31 @@ def serviceDetects_GetDetectSummaries(self): return False # def serviceDetects_GetAggregateDetects(self): - # auth, falcon = self.authenticate() - # if falcon.GetAggregateDetects(body={"ids":falcon.QueryDetects(parameters={"limit":1})["body"]["resources"]})["status_code"] in AllowedResponses: - # auth.serviceRevoke() + # print(falcon.QueryDetects(parameters={"limit":1})) + # print(falcon.GetAggregateDetects(body=[{"ranges":"ranges'{}'".format(falcon.QueryDetects(parameters={"limit":1})["body"]["resources"][0])}])) + # if falcon.GetAggregateDetects(body={"id":falcon.QueryDetects(parameters={"limit":1})["body"]["resources"][0]})["status_code"] in AllowedResponses: # return True # else: - # auth.serviceRevoke() # return False + def serviceDetects_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + if falcon.QueryDetects()["status_code"] != 500: + errorChecks = False + if falcon.GetDetectSummaries(body={})["status_code"] != 500: + errorChecks = False + if falcon.GetAggregateDetects(body={})["status_code"] != 500: + errorChecks = False + if falcon.UpdateDetectsByIdsV2(body={})["status_code"] != 500: + errorChecks = False + + return errorChecks + def test_QueryDetects(self): assert self.serviceDetects_QueryDetects() == True - + + @pytest.mark.skipif(falcon.QueryDetects(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_GetDetectSummaries(self): assert self.serviceDetects_GetDetectSummaries() == True @@ -52,4 +65,7 @@ def test_GetDetectSummaries(self): # assert self.serviceDetects_GetAggregateDetects() == True def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceDetects_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_device_control_policies.py b/tests/test_device_control_policies.py index 308e3eaf8..708a62184 100644 --- a/tests/test_device_control_policies.py +++ b/tests/test_device_control_policies.py @@ -26,14 +26,13 @@ def serviceDeviceControlPolicies_queryDeviceControlPolicies(self): else: return False - @pytest.mark.skipif(falcon.queryDeviceControlPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceDeviceControlPolicies_queryDeviceControlPolicyMembers(self): if falcon.queryDeviceControlPolicyMembers(parameters={"id": falcon.queryDeviceControlPolicies(parameters={"limit":1})["body"]["resources"][0]})["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.queryDeviceControlPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def serviceDeviceControlPolicies_getDeviceControlPolicies(self): if falcon.getDeviceControlPolicies(ids=falcon.queryDeviceControlPolicies(parameters={"limit":1})["body"]["resources"][0])["status_code"] in AllowedResponses: return True @@ -46,27 +45,53 @@ def serviceDeviceControlPolicies_queryCombinedDeviceControlPolicies(self): else: return False - @pytest.mark.skipif(falcon.queryCombinedDeviceControlPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceDeviceControlPolicies_queryCombinedDeviceControlPolicyMembers(self): if falcon.queryCombinedDeviceControlPolicyMembers(parameters={"id": falcon.queryCombinedDeviceControlPolicies(parameters={"limit":1})["body"]["resources"][0]["id"]})["status_code"] in AllowedResponses: return True else: return False + def serviceDeviceControlPolicies_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["queryCombinedDeviceControlPolicyMembers",""], + ["queryCombinedDeviceControlPolicies",""], + ["performDeviceControlPoliciesAction","body={}, parameters={}"], + ["setDeviceControlPoliciesPrecedence", "body={}"], + ["getDeviceControlPolicies","ids='12345678'"], + ["createDeviceControlPolicies","body={}"], + ["deleteDeviceControlPolicies","ids='12345678'"], + ["updateDeviceControlPolicies","body={}"], + ["queryDeviceControlPolicyMembers",""], + ["queryDeviceControlPolicies",""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_queryDeviceControlPolicies(self): assert self.serviceDeviceControlPolicies_queryDeviceControlPolicies() == True - + + @pytest.mark.skipif(falcon.queryDeviceControlPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_queryDeviceControlPolicyMembers(self): assert self.serviceDeviceControlPolicies_queryDeviceControlPolicyMembers() == True + @pytest.mark.skipif(falcon.queryDeviceControlPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_getDeviceControlPolicies(self): assert self.serviceDeviceControlPolicies_getDeviceControlPolicies() == True def test_queryCombinedDeviceControlPolicies(self): assert self.serviceDeviceControlPolicies_queryCombinedDeviceControlPolicies() == True - + + @pytest.mark.skipif(falcon.queryCombinedDeviceControlPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_queryCombinedDeviceControlPolicyMembers(self): assert self.serviceDeviceControlPolicies_queryCombinedDeviceControlPolicyMembers() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceDeviceControlPolicies_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_event_streams.py b/tests/test_event_streams.py index 3cf19583d..2b565d6dc 100644 --- a/tests/test_event_streams.py +++ b/tests/test_event_streams.py @@ -28,7 +28,6 @@ def serviceStream_listAvailableStreamsOAuth2(self): else: return False - @pytest.mark.skipif(falcon.listAvailableStreamsOAuth2(parameters={"appId":"pytest-event_streams-unit-test"})["status_code"] == 429, reason="API rate limit reached") def serviceStream_refreshActiveStreamSession(self): avail = falcon.listAvailableStreamsOAuth2(parameters={"appId":"pytest-event_streams-unit-test"}) t1 = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S +0000') @@ -39,12 +38,26 @@ def serviceStream_refreshActiveStreamSession(self): return True else: return False + + def serviceStream_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + if falcon.listAvailableStreamsOAuth2(parameters={})["status_code"] != 500: + errorChecks = False + if falcon.refreshActiveStreamSession(parameters={}, partition=0)["status_code"] != 500: + errorChecks = False + + return errorChecks def test_listAvailableStreamsOAuth2(self): assert self.serviceStream_listAvailableStreamsOAuth2() == True + #@pytest.mark.skipif(falcon.listAvailableStreamsOAuth2(parameters={"appId":"pytest-event_streams-unit-test"})["status_code"] == 429, reason="API rate limit reached") # def test_refreshActiveStreamSession(self): # assert self.serviceStream_refreshActiveStreamSession() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceStream_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_falconx_sandbox.py b/tests/test_falconx_sandbox.py index c43bc89e9..e50c071f4 100644 --- a/tests/test_falconx_sandbox.py +++ b/tests/test_falconx_sandbox.py @@ -32,21 +32,47 @@ def serviceFalconX_QuerySubmissions(self): else: return False - @pytest.mark.skipif(falcon.QueryReports(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceFalconX_GetSummaryReports(self): if falcon.GetSummaryReports(ids=falcon.QueryReports(parameters={"limit":1})["body"]["resources"])["status_code"] in AllowedResponses: return True else: return False + def serviceFalconX_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["GetArtifacts", "parameters={}"], + ["GetSummaryReports", "ids='12345678'"], + # ["GetReports","body={}, parameters={}"], + # ["DeleteReport", "body={}"], + ["GetSubmissions","ids='12345678'"], + ["Submit","body={}"], + ["QueryReports",""], + ["QuerySubmissions",""], + # ["GetSampleV2","body={}"], + ["UploadSampleV2", "body={},parameters={}"] + # ["DeleteSampleV2", ""], + # ["QuerySampleV1", ""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_QueryReports(self): assert self.serviceFalconX_QueryReports() == True def test_QuerySubmissions(self): assert self.serviceFalconX_QuerySubmissions() == True + @pytest.mark.skipif(falcon.QueryReports(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_GetSummaryReports(self): assert self.serviceFalconX_GetSummaryReports() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceFalconX_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_firewall_management.py b/tests/test_firewall_management.py index 522252277..1ddb57fd3 100644 --- a/tests/test_firewall_management.py +++ b/tests/test_firewall_management.py @@ -24,10 +24,60 @@ def serviceFirewall_query_rules(self): else: return False + + def serviceFirewall_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + if falcon.aggregate_events(body={})["status_code"] != 500: + errorChecks = False + if falcon.aggregate_policy_rules(body={})["status_code"] != 500: + errorChecks = False + if falcon.aggregate_rule_groups(body={})["status_code"] != 500: + errorChecks = False + if falcon.aggregate_rules(body={})["status_code"] != 500: + errorChecks = False + if falcon.get_events(ids="12345678")["status_code"] != 500: + errorChecks = False + if falcon.get_firewall_fields(ids="12345678")["status_code"] != 500: + errorChecks = False + if falcon.get_platforms(ids="12345678")["status_code"] != 500: + errorChecks = False + if falcon.get_policy_containers(ids="12345678")["status_code"] != 500: + errorChecks = False + if falcon.update_policy_container(body={}, cs_username="BillTheCat")["status_code"] != 500: + errorChecks = False + if falcon.get_rule_groups(ids="12345678")["status_code"] != 500: + errorChecks = False + if falcon.create_rule_group(body={}, cs_username="HarryHenderson")["status_code"] != 500: + errorChecks = False + if falcon.delete_rule_groups(ids="12345678", cs_username="KyloRen")["status_code"] != 500: + errorChecks = False + if falcon.update_rule_group(body={}, cs_username="Calcifer")["status_code"] != 500: + errorChecks = False + if falcon.get_rules(ids="12345678")["status_code"] != 500: + errorChecks = False + if falcon.query_events()["status_code"] != 500: + errorChecks = False + if falcon.query_firewall_fields()["status_code"] != 500: + errorChecks = False + if falcon.query_platforms()["status_code"] != 500: + errorChecks = False + if falcon.query_policy_rules()["status_code"] != 500: + errorChecks = False + if falcon.query_rule_groups()["status_code"] != 500: + errorChecks = False + if falcon.query_rules()["status_code"] != 500: + errorChecks = False + + return errorChecks + # def test_query_rules(self): # assert self.serviceFirewall_query_rules() == True - def test_logout(self): + def test_Logout(self): assert auth.serviceRevoke() == True + def test_Errors(self): + assert self.serviceFirewall_GenerateErrors() == True + #TODO: My current API key can't hit this API. Pending additional unit testing for now. \ No newline at end of file diff --git a/tests/test_firewall_policies.py b/tests/test_firewall_policies.py index 35c6c97bd..5f1728309 100644 --- a/tests/test_firewall_policies.py +++ b/tests/test_firewall_policies.py @@ -27,6 +27,30 @@ def serviceFirewall_queryFirewallPolicies(self): # def test_queryFirewallPolicies(self): # assert self.serviceFirewall_queryFirewallPolicies() == True - def test_logout(self): + def serviceFirewall_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["queryCombinedFirewallPolicyMembers",""], + ["queryCombinedFirewallPolicies",""], + ["performFirewallPoliciesAction","body={}, parameters={}"], + ["setFirewallPoliciesPrecedence","body={}"], + ["getFirewallPolicies","ids='12345678'"], + ["createFirewallPolicies","body={}"], + ["deleteFirewallPolicies","ids='12345678'"], + ["updateFirewallPolicies","body={}"], + ["queryFirewallPolicyMembers",""], + ["queryFirewallPolicies", ""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + + def test_Logout(self): assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceFirewall_GenerateErrors() == True #TODO: My current API key can't hit this API. Pending additional unit testing for now. \ No newline at end of file diff --git a/tests/test_host_group.py b/tests/test_host_group.py index fdd961ae0..b8f617129 100644 --- a/tests/test_host_group.py +++ b/tests/test_host_group.py @@ -25,14 +25,12 @@ def serviceHostGroup_queryHostGroups(self): else: return False - @pytest.mark.skipif(falcon.queryHostGroups(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceHostGroup_queryGroupMembers(self): if falcon.queryGroupMembers(parameters={"limit":1,"id":falcon.queryHostGroups(parameters={"limit":1})["body"]["resources"][0]})["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.queryHostGroups(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceHostGroup_getHostGroups(self): if falcon.getHostGroups(ids=falcon.queryHostGroups(parameters={"limit":1})["body"]["resources"][0])["status_code"] in AllowedResponses: return True @@ -45,27 +43,53 @@ def serviceHostGroup_queryCombinedHostGroups(self): else: return False - @pytest.mark.skipif(falcon.queryCombinedHostGroups(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def serviceHostGroup_queryCombinedGroupMembers(self): if falcon.queryCombinedGroupMembers(parameters={"limit":1,"id":falcon.queryCombinedHostGroups(parameters={"limit":1})["body"]["resources"][0]["id"]})["status_code"] in AllowedResponses: return True else: return False + def serviceHostGroup_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["queryCombinedGroupMembers",""], + ["queryCombinedHostGroups",""], + ["performGroupAction","body={}, parameters={}"], + ["getHostGroups","ids='12345678'"], + ["createHostGroups","body={}"], + ["deleteHostGroups","ids='12345678'"], + ["updateHostGroups","body={}"], + ["queryGroupMembers",""], + ["queryHostGroups",""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_queryHostGroups(self): assert self.serviceHostGroup_queryHostGroups() == True + @pytest.mark.skipif(falcon.queryHostGroups(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_queryGroupMembers(self): assert self.serviceHostGroup_queryGroupMembers() == True + @pytest.mark.skipif(falcon.queryHostGroups(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_getHostGroups(self): assert self.serviceHostGroup_getHostGroups() == True def test_queryCombinedHostGroups(self): assert self.serviceHostGroup_queryCombinedHostGroups() == True - + + @pytest.mark.skipif(falcon.queryCombinedHostGroups(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_queryCombinedGroupMembers(self): assert self.serviceHostGroup_queryCombinedGroupMembers() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceHostGroup_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_hosts.py b/tests/test_hosts.py index dcc45706c..a5a5d766d 100644 --- a/tests/test_hosts.py +++ b/tests/test_hosts.py @@ -63,6 +63,22 @@ def serviceHosts_PerformActionV2(self): else: return False + def serviceHosts_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["PerformActionV2","body={}, parameters={}"], + ["GetDeviceDetails","ids='12345678'"], + ["QueryHiddenDevices",""], + ["QueryDevicesByFilterScroll",""], + ["QueryDevicesByFilter",""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_QueryHiddenDevices(self): assert self.serviceHosts_QueryHiddenDevices() == True @@ -79,5 +95,8 @@ def test_QueryDevicesByFilter(self): # def test_PerformActionV2(self): # assert self.serviceHosts_PerformActionV2() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceHosts_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_incidents.py b/tests/test_incidents.py index 298481ce4..e18d2324d 100644 --- a/tests/test_incidents.py +++ b/tests/test_incidents.py @@ -38,19 +38,35 @@ def serviceIncidents_QueryIncidents(self): else: return False - @pytest.mark.skipif(falcon.QueryBehaviors(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceIncidents_GetBehaviors(self): if falcon.GetBehaviors(body={"ids":falcon.QueryBehaviors(parameters={"limit":1})["body"]["resources"]})["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.QueryIncidents(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def serviceIncidents_GetIncidents(self): if falcon.GetIncidents(body={"ids":falcon.QueryIncidents(parameters={"limit":1})["body"]["resources"]})["status_code"] in AllowedResponses: return True else: return False + def serviceIncidents_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["CrowdScore",""], + ["GetBehaviors","body={}"], + ["PerformIncidentAction","body={}"], + ["GetIncidents","body={}"], + ["QueryBehaviors",""], + ["QueryIncidents",""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_CrowdScore(self): assert self.serviceIncidents_CrowdScore() == True @@ -59,12 +75,17 @@ def test_QueryBehaviors(self): def test_QueryIncidents(self): assert self.serviceIncidents_QueryIncidents() == True - + + @pytest.mark.skipif(falcon.QueryIncidents(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_GetIncidents(self): assert self.serviceIncidents_GetIncidents() == True - + + @pytest.mark.skipif(falcon.QueryBehaviors(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_GetBehaviors(self): assert self.serviceIncidents_GetBehaviors() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceIncidents_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_intel.py b/tests/test_intel.py index 7a206737a..98808ec95 100644 --- a/tests/test_intel.py +++ b/tests/test_intel.py @@ -36,19 +36,18 @@ def serviceIntel_QueryIntelReportEntities(self): else: return False - @pytest.mark.skipif(falcon.QueryIntelActorEntities(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceIntel_GetIntelActorEntities(self): if falcon.GetIntelActorEntities(ids=falcon.QueryIntelActorEntities(parameters={"limit":1})["body"]["resources"][0])["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.QueryIntelIndicatorIds(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def serviceIntel_GetIntelIndicatorEntities(self): if falcon.GetIntelIndicatorEntities(body={"id": falcon.QueryIntelIndicatorIds(parameters={"limit":1})["body"]["resources"][0]})["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.QueryIntelReportEntities(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def serviceIntel_GetIntelReportEntities(self): if falcon.GetIntelReportEntities(ids=falcon.QueryIntelReportEntities(parameters={"limit":1})["body"]["resources"][0])["status_code"] in AllowedResponses: return True @@ -79,6 +78,31 @@ def serviceIntel_QueryIntelRuleIds(self): else: return False + def serviceIntel_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["QueryIntelActorEntities",""], + ["QueryIntelIndicatorEntities",""], + ["QueryIntelReportEntities",""], + ["GetIntelActorEntities","ids='12345678'"], + ["GetIntelIndicatorEntities","body={}"], + ["GetIntelReportPDF","parameters={}"], + ["GetIntelReportEntities","ids='12345678'"], + ["GetIntelRuleFile","parameters={}"], + ["GetLatestIntelRuleFile","parameters={}"], + ["GetIntelRuleEntities", "ids='12345678'"], + ["QueryIntelActorIds", ""], + ["QueryIntelIndicatorIds", ""], + ["QueryIntelReportIds", ""], + ["QueryIntelRuleIds", "parameters={}"] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_QueryIntelActorEntities(self): assert self.serviceIntel_QueryIntelActorEntities() == True @@ -87,14 +111,17 @@ def test_QueryIntelIndicatorEntities(self): def test_QueryIntelReportEntities(self): assert self.serviceIntel_QueryIntelReportEntities() == True - + + @pytest.mark.skipif(falcon.QueryIntelActorEntities(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_GetIntelActorEntities(self): assert self.serviceIntel_GetIntelActorEntities() == True #Not working - data issue with input body payload + #@pytest.mark.skipif(falcon.QueryIntelIndicatorIds(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") # def test_GetIntelIndicatorEntities(self): # assert self.serviceIntel_GetIntelIndicatorEntities() == True - + + @pytest.mark.skipif(falcon.QueryIntelReportEntities(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_GetIntelReportEntities(self): assert self.serviceIntel_GetIntelReportEntities() == True @@ -110,5 +137,8 @@ def test_QueryIntelReportIds(self): def test_QueryIntelRuleIds(self): assert self.serviceIntel_QueryIntelRuleIds() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceIntel_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_iocs.py b/tests/test_iocs.py index 851abc7b1..ba95c484c 100644 --- a/tests/test_iocs.py +++ b/tests/test_iocs.py @@ -24,7 +24,7 @@ def serviceIOCs_QueryIOCs(self): else: return False - @pytest.mark.skipif(falcon.QueryIOCs(parameters={"types":"ipv4"})["status_code"] == 429, reason="API rate limit reached") + def serviceIOCs_GetIOC(self): if falcon.GetIOC(parameters={"type":"ipv4", "value":falcon.QueryIOCs(parameters={"types":"ipv4"})["body"]["resources"][0]})["status_code"] in AllowedResponses: @@ -32,13 +32,37 @@ def serviceIOCs_GetIOC(self): else: return False + def serviceIOCs_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["DevicesCount","parameters={}"], + ["GetIOC","parameters={}"], + ["CreateIOC","body={}"], + ["DeleteIOC","parameters={}"], + ["UpdateIOC","body={}, parameters={}"], + ["DevicesRanOn","parameters={}"], + ["QueryIOCs",""], + ["ProcessesRanOn","parameters={}"], + ["entities_processes","ids='12345678'"] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_QueryIOCs(self): assert self.serviceIOCs_QueryIOCs() == True # Current test environment doesn't have any custom IOCs configured atm + #@pytest.mark.skipif(falcon.QueryIOCs(parameters={"types":"ipv4"})["status_code"] == 429, reason="API rate limit reached") # def test_GetIOC(self): # assert self.serviceIOCs_GetIOC() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceIOCs_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_prevention_policy.py b/tests/test_prevention_policy.py index a08002a43..9ab6261fc 100644 --- a/tests/test_prevention_policy.py +++ b/tests/test_prevention_policy.py @@ -24,7 +24,6 @@ def servicePrevent_queryPreventionPolicies(self): else: return False - @pytest.mark.skipif(falcon.queryPreventionPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def servicePrevent_queryPreventionPolicyMembers(self): if falcon.queryPreventionPolicyMembers(parameters={"id":falcon.queryPreventionPolicies(parameters={"limit":1})["body"]["resources"][0]})["status_code"] in AllowedResponses: return True @@ -32,7 +31,6 @@ def servicePrevent_queryPreventionPolicyMembers(self): return False return True - @pytest.mark.skipif(falcon.queryPreventionPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def servicePrevent_getPreventionPolicies(self): if falcon.getPreventionPolicies(ids=falcon.queryPreventionPolicies(parameters={"limit":1})["body"]["resources"][0])["status_code"] in AllowedResponses: return True @@ -46,7 +44,6 @@ def servicePrevent_queryCombinedPreventionPolicies(self): else: return False - @pytest.mark.skipif(falcon.queryCombinedPreventionPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def servicePrevent_queryCombinedPreventionPolicyMembers(self): if falcon.queryCombinedPreventionPolicyMembers(parameters={"id":falcon.queryCombinedPreventionPolicies(parameters={"limit":1})["body"]["resources"][0]["id"]})["status_code"] in AllowedResponses: return True @@ -54,20 +51,47 @@ def servicePrevent_queryCombinedPreventionPolicyMembers(self): return False return True + def servicePrevent_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["queryCombinedPreventionPolicyMembers",""], + ["queryCombinedPreventionPolicies",""], + ["performPreventionPoliciesAction","body={}, parameters={}"], + ["setPreventionPoliciesPrecedence","body={}"], + ["getPreventionPolicies","ids='12345678'"], + ["createPreventionPolicies","body={}"], + ["deletePreventionPolicies","ids='12345678'"], + ["updatePreventionPolicies","body={}"], + ["queryPreventionPolicyMembers",""], + ["queryPreventionPolicies",""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_queryPreventionPolicies(self): assert self.servicePrevent_queryPreventionPolicies() == True + @pytest.mark.skipif(falcon.queryPreventionPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_queryPreventionPolicyMembers(self): assert self.servicePrevent_queryPreventionPolicyMembers() == True - + + @pytest.mark.skipif(falcon.queryPreventionPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_getPreventionPolicies(self): assert self.servicePrevent_getPreventionPolicies() == True def test_queryCombinedPreventionPolicies(self): assert self.servicePrevent_queryCombinedPreventionPolicies() == True - + + @pytest.mark.skipif(falcon.queryCombinedPreventionPolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_queryCombinedPreventionPolicyMembers(self): assert self.servicePrevent_queryCombinedPreventionPolicyMembers() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.servicePrevent_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_real_time_response.py b/tests/test_real_time_response.py index 35f96a396..5581522d4 100644 --- a/tests/test_real_time_response.py +++ b/tests/test_real_time_response.py @@ -25,8 +25,43 @@ def serviceRTR_ListAllSessions(self): else: return False + def serviceRTR_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["RTR_AggregateSessions","body={}"], + ["BatchActiveResponderCmd","body={}"], + ["BatchCmd","body={}"], + ["BatchGetCmdStatus","parameters={}"], + ["BatchGetCmd","body={}"], + ["BatchInitSessions","body={}"], + ["BatchRefreshSessions","body={}"], + ["RTR_CheckActiveResponderCommandStatus","parameters={}"], + ["RTR_ExecuteActiveResponderCommand","body={}"], + ["RTR_CheckCommandStatus","parameters={}"], + ["RTR_ExecuteCommand","body={}"], + ["RTR_GetExtractedFileContents","parameters={}"], + ["RTR_ListFiles","parameters={}"], + ["RTR_DeleteFile","ids='12345678', parameters={}"], + ["RTR_ListQueuedSessions","body={}"], + ["RTR_DeleteQueuedSession","parameters={}"], + ["RTR_PulseSession","body={}"], + ["RTR_ListSessions","body={}"], + ["RTR_InitSession","body={}"], + ["RTR_DeleteSession","parameters={}"], + ["RTR_ListAllSessions",""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_RTR_ListAllSessions(self): assert self.serviceRTR_ListAllSessions() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceRTR_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_real_time_response_admin.py b/tests/test_real_time_response_admin.py index 3be1de00e..a3ba4508d 100644 --- a/tests/test_real_time_response_admin.py +++ b/tests/test_real_time_response_admin.py @@ -31,11 +31,37 @@ def serviceRTR_ListScripts(self): else: return False + def serviceRTR_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["BatchAdminCmd","body={}"], + ["RTR_CheckAdminCommandStatus","parameters={}"], + ["RTR_ExecuteAdminCommand","body={}"], + ["RTR_GetPut_Files","ids='12345678'"], + ["RTR_CreatePut_Files","data={}, files=[]"], + ["RTR_DeletePut_Files","ids='12345678'"], + ["RTR_GetScripts","ids='12345678'"], + ["RTR_CreateScripts","data={}, files=[]"], + ["RTR_DeleteScripts","ids='12345678'"], + ["RTR_UpdateScripts","data={}, files=[]"], + ["RTR_ListPut_Files",""], + ["RTR_ListScripts",""] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_RTR_ListScripts(self): assert self.serviceRTR_ListScripts() == True def test_RTR_ListPut_Files(self): assert self.serviceRTR_ListPut_Files() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceRTR_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_sensor_update_policy.py b/tests/test_sensor_update_policy.py index bdf701808..6b67b84e8 100644 --- a/tests/test_sensor_update_policy.py +++ b/tests/test_sensor_update_policy.py @@ -29,14 +29,13 @@ def serviceSensorUpdate_querySensorUpdatePolicyMembers(self): return True else: return False - @pytest.mark.skipif(falcon.querySensorUpdatePolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def serviceSensorUpdate_getSensorUpdatePolicies(self): if falcon.getSensorUpdatePolicies(ids=falcon.querySensorUpdatePolicies(parameters={"limit":1})["body"]["resources"][0])["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.querySensorUpdatePolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def serviceSensorUpdate_getSensorUpdatePoliciesV2(self): if falcon.getSensorUpdatePoliciesV2(ids=falcon.querySensorUpdatePolicies(parameters={"limit":1})["body"]["resources"][0])["status_code"] in AllowedResponses: return True @@ -55,6 +54,34 @@ def serviceSensorUpdate_queryCombinedSensorUpdatePolicyMembers(self): else: return False + def serviceSensorUpdate_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["querySensorUpdatePolicies",""], + ["querySensorUpdatePolicyMembers",""], + ["getSensorUpdatePolicies","ids='12345678'"], + ["getSensorUpdatePoliciesV2","ids='12345678'"], + ["queryCombinedSensorUpdatePolicies",""], + ["queryCombinedSensorUpdatePolicyMembers", ""], + ["revealUninstallToken","body={}"], + ["queryCombinedSensorUpdateBuilds", ""], + ["createSensorUpdatePolicies", "body={}"], + ["createSensorUpdatePoliciesV2", "body={}"], + ["deleteSensorUpdatePolicies", "ids='12345678'"], + ["updateSensorUpdatePolicies", "body={}"], + ["updateSensorUpdatePoliciesV2", "body={}"], + ["performSensorUpdatePoliciesAction", "body={},parameters={}"], + ["setSensorUpdatePoliciesPrecedence", "body={}"], + ["queryCombinedSensorUpdatePoliciesV2",""] + ] + + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks + def test_querySensorUpdatePolicies(self): assert self.serviceSensorUpdate_querySensorUpdatePolicies() == True @@ -66,12 +93,17 @@ def test_queryCombinedSensorUpdatePolicies(self): def test_queryCombinedSensorUpdatePolicyMembers(self): assert self.serviceSensorUpdate_queryCombinedSensorUpdatePolicyMembers() == True - + + @pytest.mark.skipif(falcon.querySensorUpdatePolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_getSensorUpdatePolicies(self): assert self.serviceSensorUpdate_getSensorUpdatePolicies() == True - + + @pytest.mark.skipif(falcon.querySensorUpdatePolicies(parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") def test_getSensorUpdatePoliciesV2(self): assert self.serviceSensorUpdate_getSensorUpdatePoliciesV2() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceSensorUpdate_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_spotlight_vulnerabilities.py b/tests/test_spotlight_vulnerabilities.py index f56caf527..f07140b53 100644 --- a/tests/test_spotlight_vulnerabilities.py +++ b/tests/test_spotlight_vulnerabilities.py @@ -24,18 +24,31 @@ def serviceSpotlight_queryVulnerabilities(self): else: return False - @pytest.mark.skipif(falcon.queryVulnerabilities(parameters={"limit":1,"filter":"created_timestamp:>'2020-01-01T00:00:01Z'"})["status_code"] == 429, reason="API rate limit reached") def serviceSpotlight_getVulnerabilities(self): if falcon.getVulnerabilities(ids=falcon.queryVulnerabilities(parameters={"limit":1,"filter":"created_timestamp:>'2020-01-01T00:00:01Z'"})["body"]["resources"][0])["status_code"] in AllowedResponses: return True else: return False + def serviceSpotlight_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + if falcon.queryVulnerabilities(parameters={})["status_code"] != 500: + errorChecks = False + if falcon.getVulnerabilities(ids="12345678")["status_code"] != 500: + errorChecks = False + + return errorChecks + def test_queryVulnerabilities(self): assert self.serviceSpotlight_queryVulnerabilities() == True - + + @pytest.mark.skipif(falcon.queryVulnerabilities(parameters={"limit":1,"filter":"created_timestamp:>'2020-01-01T00:00:01Z'"})["status_code"] == 429, reason="API rate limit reached") def test_getVulnerabilities(self): assert self.serviceSpotlight_getVulnerabilities() == True - def test_logout(self): + def test_Logout(self): assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceSpotlight_GenerateErrors() == True \ No newline at end of file diff --git a/tests/test_uber_api_complete.py b/tests/test_uber_api_complete.py new file mode 100644 index 000000000..2fbb42740 --- /dev/null +++ b/tests/test_uber_api_complete.py @@ -0,0 +1,204 @@ +# test_uber_api_complete.py +# This class tests the uber class + +import json +import os +import sys +import pytest +import datetime +import hashlib +#Import our sibling src folder into the path +sys.path.append(os.path.abspath('src')) +# Classes to test - manually imported from our sibling folder +from falconpy import api_complete as FalconSDK + +AllowedResponses = [200, 400, 415, 429, 500] + +if "DEBUG_API_ID" in os.environ and "DEBUG_API_SECRET" in os.environ: + config = {} + config["falcon_client_id"] = os.getenv("DEBUG_API_ID") + config["falcon_client_secret"] = os.getenv("DEBUG_API_SECRET") +else: + cur_path = os.path.dirname(os.path.abspath(__file__)) + if os.path.exists('%s/test.config' % cur_path): + with open('%s/test.config' % cur_path, 'r') as file_config: + config = json.loads(file_config.read()) + else: + sys.exit(1) + + +falcon = FalconSDK.APIHarness( + creds={ + "client_id": config["falcon_client_id"], + "client_secret": config["falcon_client_secret"] + } +) +falcon.authenticate() +if not falcon.authenticated: + sys.exit(1) + +class TestUber: + def uberCCAWS_GetAWSSettings(self): + if falcon.command("GetAWSSettings")["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_QueryAWSAccounts(self): + if falcon.command("QueryAWSAccounts", parameters={"limit":1})["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_GetAWSAccounts(self): + if falcon.command("GetAWSAccounts", ids=falcon.command("QueryAWSAccounts", parameters={"limit":1})["body"]["resources"][0]["id"])["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_VerifyAWSAccountAccess(self): + if falcon.command("VerifyAWSAccountAccess", ids=falcon.command("QueryAWSAccounts", parameters={"limit":1})["body"]["resources"][0]["id"])["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_QueryAWSAccountsForIDs(self): + if falcon.command("QueryAWSAccountsForIDs", parameters={"limit":1})["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_TestUploadDownload(self): + FILENAME="tests/testfile.png" + fmt='%Y-%m-%d %H:%M:%S' + stddate = datetime.datetime.now().strftime(fmt) + sdtdate = datetime.datetime.strptime(stddate, fmt) + sdtdate = sdtdate.timetuple() + jdate = sdtdate.tm_yday + jdate = "{}{}".format(stddate.replace("-","").replace(":","").replace(" ",""),jdate) + SOURCE="%s_source.png" % jdate + TARGET="tests/%s_target.png" % jdate + PAYLOAD = open(FILENAME, 'rb').read() + response = falcon.command('UploadSampleV3', file_name=SOURCE, data=PAYLOAD, content_type="application/octet-stream") + sha = response["body"]["resources"][0]["sha256"] + response = falcon.command("GetSampleV3", parameters={}, ids=sha) + open(TARGET, 'wb').write(response) + buf=65536 + hash1 = hashlib.sha256() + with open(FILENAME, 'rb') as f: + while True: + data = f.read(buf) + if not data: + break + hash1.update(data) + hash1 = hash1.hexdigest() + hash2 = hashlib.sha256() + with open(TARGET, 'rb') as f: + while True: + data = f.read(buf) + if not data: + break + hash2.update(data) + hash2 = hash2.hexdigest() + if os.path.exists(TARGET): + os.remove(TARGET) + if hash1 == hash2: + return True + else: + return False + + def uberCCAWS_GenerateError(self): + if falcon.command("QueryAWSAccounts", partition=0)["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_GenerateInvalidPayload(self): + if falcon.command("refreshActiveStreamSession", partition=9, parameters={"action_name":"refresh_active_stream_session"})["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_OverrideAndHeader(self): + if falcon.command(action="", override="GET,/cloud-connect-aws/combined/accounts/v1", headers={"Nothing":"Special"})["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_BadCommand(self): + if falcon.command("IWantTheImpossible", parameters={"limit":1})["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_GenerateServerError(self): + if falcon.command("GetAWSAccounts", ids="123", data=['Kerash!'])["status_code"] in AllowedResponses: + return True + else: + return False + + def uberCCAWS_GenerateTokenError(self): + falcon.base_url = "'`\"" + if falcon.deauthenticate() == False: + return True + else: + return False + + def uberCCAWS_BadAuthentication(self): + falcon = FalconSDK.APIHarness( + creds={ + "client_id": "BadClientID", + "client_secret": "BadClientSecret" + } + ) + if falcon.command("QueryAWSAccounts", parameters={"limit":1})["status_code"] in AllowedResponses: + return True + else: + return False + + + def test_GetAWSSettings(self): + assert self.uberCCAWS_GetAWSSettings() == True + + def test_QueryAWSAccounts(self): + assert self.uberCCAWS_QueryAWSAccounts() == True + + @pytest.mark.skipif(falcon.command("QueryAWSAccounts", parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def test_GetAWSAccounts(self): + assert self.uberCCAWS_GetAWSAccounts() == True + + @pytest.mark.skipif(falcon.command("QueryAWSAccounts", parameters={"limit":1})["status_code"] == 429, reason="API rate limit reached") + def test_VerifyAWSAccountAccess(self): + assert self.uberCCAWS_VerifyAWSAccountAccess() == True + + def test_QueryAWSAccountsForIDs(self): + assert self.uberCCAWS_QueryAWSAccountsForIDs() == True + + def test_UploadDownload(self): + assert self.uberCCAWS_TestUploadDownload() == True + + def test_GenerateError(self): + assert self.uberCCAWS_GenerateError() == True + + def test_GenerateInvalidPayload(self): + assert self.uberCCAWS_GenerateInvalidPayload() == True + + def test_BadCommand(self): + assert self.uberCCAWS_BadCommand() == True + + def test_OverrideAndHeader(self): + #Also check token auto-renewal + falcon.token_expiration=0 + assert self.uberCCAWS_OverrideAndHeader() == True + + def test_GenerateServerError(self): + assert self.uberCCAWS_GenerateServerError() == True + + def test_logout(self): + assert falcon.deauthenticate() == True + + def test_GenerateTokenError(self): + assert self.uberCCAWS_GenerateTokenError() == True + + def test_BadAuthentication(self): + assert self.uberCCAWS_BadAuthentication() == True \ No newline at end of file diff --git a/tests/test_user_management.py b/tests/test_user_management.py index 18ee7ff03..696a2a986 100644 --- a/tests/test_user_management.py +++ b/tests/test_user_management.py @@ -29,21 +29,18 @@ def serviceUserManagement_RetrieveUserUUIDsByCID(self): else: return False - @pytest.mark.skipif(falcon.RetrieveEmailsByCID()["status_code"] == 429, reason="API rate limit reached") def serviceUserManagement_RetrieveUserUUID(self): if falcon.RetrieveUserUUID(parameters={"uid": falcon.RetrieveEmailsByCID()["body"]["resources"][0]})["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.RetrieveUserUUIDsByCID()["status_code"] == 429, reason="API rate limit reached") def serviceUserManagement_RetrieveUser(self): if falcon.RetrieveUser(ids=falcon.RetrieveUserUUIDsByCID()["body"]["resources"][0])["status_code"] in AllowedResponses: return True else: return False - @pytest.mark.skipif(falcon.RetrieveUserUUIDsByCID()["status_code"] == 429, reason="API rate limit reached") def serviceUserManagement_GetUserRoleIds(self): if falcon.GetUserRoleIds(parameters={"user_uuid":falcon.RetrieveUserUUIDsByCID()["body"]["resources"][0]})["status_code"] in AllowedResponses: return True @@ -56,33 +53,62 @@ def serviceUserManagement_GetAvailableRoleIds(self): else: return False - @pytest.mark.skipif(falcon.GetAvailableRoleIds()["status_code"] == 429, reason="API rate limit reached") def serviceUserManagement_GetRoles(self): if falcon.GetRoles(ids=falcon.GetAvailableRoleIds()["body"]["resources"][0])["status_code"] in AllowedResponses: return True else: return False + + def serviceUserManagement_GenerateErrors(self): + falcon.base_url = "nowhere" + errorChecks = True + commandList = [ + ["GetRoles","ids='12345678'"], + ["GrantUserRoleIds","body={}, parameters={}"], + ["RevokeUserRoleIds","ids='12345678', parameters={}"], + ["GetAvailableRoleIds",""], + ["GetUserRoleIds","parameters={}"], + ["RetrieveUser","ids='12345678'"], + ["CreateUser","body={}"], + ["DeleteUser","parameters={}"], + ["UpdateUser","body={}, parameters={}"], + ["RetrieveEmailsByCID",""], + ["RetrieveUserUUIDsByCID",""], + ["RetrieveUserUUID","parameters={}"] + ] + for cmd in commandList: + if eval("falcon.{}({})['status_code']".format(cmd[0],cmd[1])) != 500: + errorChecks = False + + return errorChecks def test_RetrieveEmailsByCID(self): assert self.serviceUserManagement_RetrieveEmailsByCID() == True def test_RetrieveUserUUIDsByCID(self): assert self.serviceUserManagement_RetrieveUserUUIDsByCID() == True - + + @pytest.mark.skipif(falcon.RetrieveEmailsByCID()["status_code"] == 429, reason="API rate limit reached") def test_RetrieveUserUUID(self): assert self.serviceUserManagement_RetrieveUserUUID() == True - + + @pytest.mark.skipif(falcon.RetrieveUserUUIDsByCID()["status_code"] == 429, reason="API rate limit reached") def test_RetrieveUser(self): assert self.serviceUserManagement_RetrieveUser() == True - + + @pytest.mark.skipif(falcon.RetrieveUserUUIDsByCID()["status_code"] == 429, reason="API rate limit reached") def test_GetUserRoleIds(self): assert self.serviceUserManagement_GetUserRoleIds() == True def test_GetAvailableRoleIds(self): assert self.serviceUserManagement_GetAvailableRoleIds() == True - + + @pytest.mark.skipif(falcon.GetAvailableRoleIds()["status_code"] == 429, reason="API rate limit reached") def test_GetRoles(self): assert self.serviceUserManagement_GetRoles() == True - def test_logout(self): - assert auth.serviceRevoke() == True \ No newline at end of file + def test_Logout(self): + assert auth.serviceRevoke() == True + + def test_Errors(self): + assert self.serviceUserManagement_GenerateErrors() == True \ No newline at end of file diff --git a/tests/testfile.png b/tests/testfile.png new file mode 100644 index 0000000000000000000000000000000000000000..3495bd51a2c69f6c29d1e96048af4a3fc9f83f7e GIT binary patch literal 29918 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�A8UQUjnU^vbAgfz@=M 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 ZzQRaHLCKUR