diff --git a/configurations/system/tests.py b/configurations/system/tests.py index 283c66f3..ea4f75df 100644 --- a/configurations/system/tests.py +++ b/configurations/system/tests.py @@ -13,6 +13,7 @@ from .tests_cases.security_risks_tests import SecurityRisksTests from .tests_cases.vuln_scan_tests import VulnerabilityScanningTests from .tests_cases.workflows_tests import WorkflowsTests +from .tests_cases.accounts_tests import AccountsTests def all_tests_names(): @@ -34,6 +35,7 @@ def all_tests_names(): tests.extend(TestUtil.get_class_methods(SeccompProfileTests)) tests.extend(TestUtil.get_class_methods(WorkflowsTests)) tests.extend(TestUtil.get_class_methods(RegistryTests)) + tests.extend(TestUtil.get_class_methods(AccountsTests)) return tests @@ -70,6 +72,8 @@ def get_test(test_name): return WorkflowsTests().__getattribute__(test_name)() if test_name in TestUtil.get_class_methods(RegistryTests): return RegistryTests().__getattribute__(test_name)() + if test_name in TestUtil.get_class_methods(AccountsTests): + return AccountsTests().__getattribute__(test_name)() ALL_TESTS = all_tests_names() diff --git a/configurations/system/tests_cases/accounts_tests.py b/configurations/system/tests_cases/accounts_tests.py new file mode 100644 index 00000000..d955a535 --- /dev/null +++ b/configurations/system/tests_cases/accounts_tests.py @@ -0,0 +1,14 @@ +import inspect + +from .structures import KubescapeConfiguration + + +class AccountsTests(object): + + @staticmethod + def accounts(): + from tests_scripts.accounts.accounts import Accounts + return KubescapeConfiguration( + name=inspect.currentframe().f_code.co_name, + test_obj=Accounts, + ) \ No newline at end of file diff --git a/infrastructure/backend_api.py b/infrastructure/backend_api.py index 13593ee1..29da79d8 100644 --- a/infrastructure/backend_api.py +++ b/infrastructure/backend_api.py @@ -142,6 +142,13 @@ class NotExistingCustomer(Exception): API_WEBHOOKS = "/api/v1/notifications/teams" API_TEAMS_TEST_MESSAGE = "/api/v1/notifications/teams/testMessage" +API_ACCOUNTS = "/api/v1/accounts" +API_ACCOUNTS_CLOUD_LIST = "/api/v1/accounts/cloud/list" +API_ACCOUNTS_KUBERNETES_LIST = "/api/v1/accounts/kubernetes/list" +API_ACCOUNTS_AWS_REGIONS = "/api/v1/accounts/aws/regions" +API_UNIQUEVALUES_ACCOUNTS_CLOUD= "/api/v1/uniqueValues/accounts/cloud" +API_UNIQUEVALUES_ACCOUNTS_KUBERNETES = "/api/v1/uniqueValues/accounts/kubernetes" + def deco_cookie(func): @@ -3085,7 +3092,113 @@ def test_webhook_message(self, body): return r.json() + def get_cloud_accounts(self, body=None, **kwargs): + url = API_ACCOUNTS_CLOUD_LIST + if body is None: + body = {"pageSize": 150, "pageNum": 1} + + params = {"customerGUID": self.selected_tenant_id} + if kwargs: + params.update(**kwargs) + r = self.post(url, params=params, json=body) + if not 200 <= r.status_code < 300: + raise Exception( + 'Error accessing cloud accounts. Customer: "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() + + def get_kubernetes_accounts(self, body=None, **kwargs): + url = API_ACCOUNTS_KUBERNETES_LIST + if body is None: + body = {"pageSize": 150, "pageNum": 1} + + params = {"customerGUID": self.selected_tenant_id} + if kwargs: + params.update(**kwargs) + r = self.post(url, params=params, json=body) + if not 200 <= r.status_code < 300: + raise Exception( + 'Error accessing cloud accounts. Customer: "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() + + + def get_cloud_accounts_uniquevalues(self, body): + params = {"customerGUID": self.selected_tenant_id} + + Logger.logger.info("get_cloud_accounts_uniquevalues body: %s" % body) + r = self.post(API_UNIQUEVALUES_ACCOUNTS_CLOUD, params=params, json=body) + + if not 200 <= r.status_code < 300: + raise Exception( + 'Error accessing dashboard. Request: get_cloud_accounts_uniquevalues "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() + + def get_kubernetes_accounts_uniquevalues(self, body): + params = {"customerGUID": self.selected_tenant_id} + + Logger.logger.info("get_kubernetes_accounts_uniquevalues body: %s" % body) + + r = self.post(API_UNIQUEVALUES_ACCOUNTS_KUBERNETES, params=params, json=body) + + if not 200 <= r.status_code < 300: + raise Exception( + 'Error accessing dashboard. Request: get_kubernetes_accounts_uniquevalues "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() + + def create_cloud_account(self, body, provider): + url = API_ACCOUNTS + params = {"customerGUID": self.selected_tenant_id, + "provider": provider} + r = self.post(url, params=params, json=body) + if not 200 <= r.status_code < 300: + raise Exception( + 'Error creating cloud account. Customer: "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() + + def delete_cloud_account(self, guid): + url = API_ACCOUNTS + params = {"customerGUID": self.selected_tenant_id} + body = { + "innerFilters": [ + { + "guid": guid + } + ] + } + r = self.delete(url, params=params, json=body) + if not 200 <= r.status_code < 300: + raise Exception( + 'Error deleting cloud account. Customer: "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() + + + + def update_cloud_account(self, body, provider): + url = API_ACCOUNTS + params = {"customerGUID": self.selected_tenant_id, + "provider": provider} + r = self.put(url, params=params, json=body) + if not 200 <= r.status_code < 300: + raise Exception( + 'Error updating cloud account. Customer: "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() + + + def get_aws_regions(self): + url = API_ACCOUNTS_AWS_REGIONS + r = self.get(url, params={"customerGUID": self.selected_tenant_id}) + if not 200 <= r.status_code < 300: + raise Exception( + 'Error accessing AWS regions. Customer: "%s" (code: %d, message: %s)' % ( + self.customer, r.status_code, r.text)) + return r.json() diff --git a/system_test_mapping.json b/system_test_mapping.json index d48f2f8d..1021f890 100644 --- a/system_test_mapping.json +++ b/system_test_mapping.json @@ -1689,5 +1689,18 @@ "description": "Checks workflows configurations", "skip_on_environment": "", "owner": "jonathang@armosec.io" + }, + "accounts": { + "target": [ + "Backend" + ], + "target_repositories": [ + "config-service-dummy", + "cadashboardbe-dummy", + "event-ingester-service-dummy" + ], + "description": "Checks accounts", + "skip_on_environment": "", + "owner": "eranm@armosec.io" } } \ No newline at end of file diff --git a/tests_scripts/accounts/accounts.py b/tests_scripts/accounts/accounts.py new file mode 100644 index 00000000..6fd47f9b --- /dev/null +++ b/tests_scripts/accounts/accounts.py @@ -0,0 +1,309 @@ + +from systest_utils import Logger, statics +from tests_scripts.helm.base_helm import BaseHelm +import random + + + +PROVIDER_AWS = "aws" +PROVIDER_AZURE = "azure" +PROVIDER_GCP = "gcp" + +# a generated good arn from Eran aws dev account - consider moving to an env var? +GOOD_ARN = "arn:aws:iam::015253967648:role/armo-scan-role-cross-with_customer-015253967648" + + +class Accounts(BaseHelm): + def __init__(self, test_obj=None, backend=None, kubernetes_obj=None, test_driver=None): + super().__init__(test_driver=test_driver, test_obj=test_obj, backend=backend, kubernetes_obj=kubernetes_obj) + + + self.helm_kwargs = { + "capabilities.vulnerabilityScan": "disable", + "grypeOfflineDB.enabled": "false", + "capabilities.relevancy": "disabled", + # "capabilities.runtimeObservability": "disable", + "capabilities.malwareDetection": "disable", + "capabilities.runtimeDetection": "disable", + "alertCRD.installDefault": False, + "alertCRD.scopeClustered": False, + } + test_helm_kwargs = self.test_obj.get_arg("helm_kwargs") + if test_helm_kwargs: + self.helm_kwargs.update(test_helm_kwargs) + + self.test_cloud_accounts_guids = [] + self.cluster = None + self.wait_for_agg_to_end = False + + + def start(self): + """ + Agenda: + 1. Create bad arn cloud account with cspm. + 2. Create good arn cloud account with cspm. + 3. Validate accounts cloud with cspm list. + 4. Validate accounts cloud with cspm uniquevalues. + 5. Edit and validate cloud account with cspm. + 6. Delete and validate cloud account with cspm. + 7. Validate cspm results apis - TODO + 8. Validate aws regions + 9. Install kubescape with helm-chart + 10. Validate accounts kubernetes list. + 11. Validate accounts kubernetes uniquevalues. + + """ + + assert self.backend is not None, f'the test {self.test_driver.test_name} must run with backend' + + # generate random number for cloud account name for uniqueness + rand = str(random.randint(10000000, 99999999)) + + bad_arn = "arn:aws:iam::12345678:role/armo-scan-role-cross-with_customer-12345678" + cloud_account_name = "AWS System Test Account " + rand + + Logger.logger.info('Stage 1: Create bad arn cloud account with cspm') + self.create_and_validate_cloud_account(cloud_account_name, bad_arn, PROVIDER_AWS, expect_failure=True) + + Logger.logger.info('Stage 2: Create good arn cloud account with cspm') + self.create_and_validate_cloud_account(cloud_account_name, GOOD_ARN, PROVIDER_AWS, expect_failure=False) + + Logger.logger.info('Stage 3: Validate accounts cloud with cspm list') + guid = self.validate_accounts_cloud_list(cloud_account_name, GOOD_ARN) + self.test_cloud_accounts_guids.append(guid) + + Logger.logger.info('Stage 4: Validate accounts cloud with cspm uniquevalues') + self.validate_accounts_cloud_uniquevalues(cloud_account_name) + + + Logger.logger.info('Stage 5: Edit and validate cloud account with cspm') + self.update_and_validate_cloud_account(guid, cloud_account_name + " updated", GOOD_ARN) + + Logger.logger.info('Stage 6: Delete and validate cloud account with cspm') + self.delete_and_validate_cloud_account(guid) + self.test_cloud_accounts_guids.remove(guid) + + Logger.logger.info('Stage 7: Validate cspm results apis - TODO') + ### TODO ### + # + # + #################### + + + Logger.logger.info('Stage 8: Validate aws regions') + res = self.backend.get_aws_regions() + assert len(res) > 0, f"failed to get aws regions, res is {res}" + + + self.cluster, self.namespace = self.setup(apply_services=False) + + + ## TODO: consider moving to a separate test that checks posture results + Logger.logger.info('Stage 9: Install kubescape with helm-chart') + self.install_kubescape(helm_kwargs=self.helm_kwargs) + + Logger.logger.info('Stage 10: Validate accounts kubernetes list') + + r, t = self.wait_for_report( + self.validate_accounts_kubernetes_list, + timeout=180, + cluster=self.cluster + ) + + Logger.logger.info('Stage 11: Validate accounts kubernetes uniquevalues') + self.validate_accounts_kubernetes_uniquevalues(cluster=self.cluster) + + + return self.cleanup() + + + + def cleanup(self, **kwargs): + for guid in self.test_cloud_accounts_guids: + self.backend.delete_cloud_account(guid=guid) + Logger.logger.info(f"Deleted cloud account with guid {guid}") + return super().cleanup(**kwargs) + + + def install_kubescape(self, helm_kwargs: dict = None): + self.add_and_upgrade_armo_to_repo() + self.install_armo_helm_chart(helm_kwargs=helm_kwargs) + self.verify_running_pods(namespace=statics.CA_NAMESPACE_FROM_HELM_NAME) + + + def create_and_validate_cloud_account(self, cloud_account_name:str, arn:str, provider:str, expect_failure:bool=False): + """ + Create and validate cloud account. + """ + + body = { + "name": cloud_account_name, + "cspmConfig": { + "crossAccountsRoleARN": arn, + "stackRegion": "us-east-1" + }, + } + + failed = False + try: + res = self.backend.create_cloud_account(body=body, provider=provider) + except Exception as e: + failed = True + + assert failed == expect_failure, f"expected to fail bad ARN for cspm, body used: {body}" + + if not expect_failure: + assert "Cloud account created" in res, f"Cloud account was not created, body used: {body}" + + + def validate_accounts_cloud_list(self, cloud_account_name:str, arn:str): + """ + Validate accounts cloud list. + """ + + body = { + "pageSize": 100, + "pageNum": 0, + "innerFilters": [ + { + "name": cloud_account_name + } + ], + } + + res = self.backend.get_cloud_accounts(body=body) + assert "response" in res, f"response not in {res}" + assert len(res["response"]) > 0, f"response is empty" + assert res["response"][0]["name"] == cloud_account_name, f"name is not {cloud_account_name}" + assert "features" in res["response"][0], f"features not in {res['response'][0]}" + assert "cspm" in res["response"][0]["features"], f"cspm not in {res['response'][0]['features']}" + assert "config" in res["response"][0]["features"]["cspm"], f"config not in {res['response'][0]['features']['cspm']}" + assert "crossAccountsRoleARN" in res["response"][0]["features"]["cspm"]["config"], f"crossAccountsRoleARN not in {res['response'][0]['features']['cspm']['config']}" + assert res["response"][0]["features"]["cspm"]["config"]["crossAccountsRoleARN"] == arn, f"crossAccountsRoleARN is not {arn}" + + guid = res["response"][0]["guid"] + return guid + + def validate_accounts_cloud_uniquevalues(self, cloud_account_name:str): + """ + Validate accounts cloud uniquevalues. + """ + + unique_values_body = { + "fields": { + "name": "", + }, + "innerFilters": [ + { + "name": cloud_account_name + } + ], + "pageSize": 100, + "pageNum": 1 + } + + res = self.backend.get_cloud_accounts_uniquevalues(body=unique_values_body) + assert "fields" in res, f"failed to get fields for cloud accounts unique values, body used: {unique_values_body}, res is {res}" + assert len(res["fields"]) > 0, f"response is empty" + assert len(res["fields"]["name"]) == 1, f"response is empty" + assert res["fields"]["name"][0] == cloud_account_name, f"name is not {cloud_account_name}" + + def update_and_validate_cloud_account(self, guid:str, cloud_account_name:str, arn:str): + """ + Update and validate cloud account. + """ + + body = { + "guid": guid, + "name": cloud_account_name, + "cspmConfig": { + "crossAccountsRoleARN": arn, + "stackRegion": "us-east-1" + } + } + + res = self.backend.update_cloud_account(body=body, provider=PROVIDER_AWS) + assert "Cloud account updated" in res, f"Cloud account with guid {guid} was not updated" + + body = { + "pageSize": 100, + "pageNum": 0, + "innerFilters": [ + { + "name": cloud_account_name + } + ], + } + + res = self.backend.get_cloud_accounts(body=body) + assert "response" in res, f"failed to get cloud accounts, body used: {body}, res is {res}" + assert len(res["response"]) > 0, f"response is empty" + assert res["response"][0]["name"] == cloud_account_name, f"failed to update cloud account, name is not {cloud_account_name}" + + def delete_and_validate_cloud_account(self, guid:str): + """ + Delete and validate cloud account. + """ + + res = self.backend.delete_cloud_account(guid=guid) + assert "Cloud account deleted" in res, f"Cloud account with guid {guid} was not deleted" + + body = { + "pageSize": 100, + "pageNum": 0, + "innerFilters": [ + { + "guid": guid + } + ], + } + + res = self.backend.get_cloud_accounts(body=body) + assert "response" in res, f"response not in {res}" + assert len(res["response"]) == 0, f"response is not empty" + + def validate_accounts_kubernetes_list(self, cluster:str): + """ + Validate accounts kubernetes list. + """ + + body = { + "pageSize": 100, + "pageNum": 1, + "innerFilters": [{ + "cluster": cluster + }] + } + + res = self.backend.get_kubernetes_accounts(body=body) + + + + assert "response" in res, f"response not in {res}" + assert len(res["response"]) > 0, f"response is empty" + assert res["response"][0]["cluster"] == cluster, f"cluster is not {cluster}" + + def validate_accounts_kubernetes_uniquevalues(self, cluster:str): + """ + Validate accounts kubernetes uniquevalues. + """ + + unique_values_body = { + "fields": { + "cluster": cluster, + }, + "innerFilters": [ + { + "cluster": cluster + } + ], + "pageSize": 100, + "pageNum": 1 + } + + res = self.backend.get_kubernetes_accounts_uniquevalues(body=unique_values_body) + assert "fields" in res, f"failed to get fields for kubernetes accounts unique values, body used: {unique_values_body}, res is {res}" + assert len(res["fields"]) > 0, f"response is empty" + assert len(res["fields"]["cluster"]) == 1, f"response is empty" + assert res["fields"]["cluster"][0] == cluster, f"cluster is not {cluster}" +