From 96f265731eb43b7600c94fa1b3d48e5538c52a96 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Mon, 4 Nov 2024 11:16:57 +0100 Subject: [PATCH 1/3] feat(servicecatalog): Add new service servicecatalog with respective unit tests --- .../aws/services/servicecatalog/__init__.py | 0 .../servicecatalog/servicecatalog_client.py | 6 + .../servicecatalog/servicecatalog_service.py | 101 ++++++++++++++++ .../servicecatalog_service_test.py | 108 ++++++++++++++++++ 4 files changed, 215 insertions(+) create mode 100644 prowler/providers/aws/services/servicecatalog/__init__.py create mode 100644 prowler/providers/aws/services/servicecatalog/servicecatalog_client.py create mode 100644 prowler/providers/aws/services/servicecatalog/servicecatalog_service.py create mode 100644 tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py diff --git a/prowler/providers/aws/services/servicecatalog/__init__.py b/prowler/providers/aws/services/servicecatalog/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/aws/services/servicecatalog/servicecatalog_client.py b/prowler/providers/aws/services/servicecatalog/servicecatalog_client.py new file mode 100644 index 00000000000..bbede226596 --- /dev/null +++ b/prowler/providers/aws/services/servicecatalog/servicecatalog_client.py @@ -0,0 +1,6 @@ +from prowler.providers.aws.services.servicecatalog.servicecatalog_service import ( + ServiceCatalog, +) +from prowler.providers.common.provider import Provider + +servicecatalog_client = ServiceCatalog(Provider.get_global_provider()) diff --git a/prowler/providers/aws/services/servicecatalog/servicecatalog_service.py b/prowler/providers/aws/services/servicecatalog/servicecatalog_service.py new file mode 100644 index 00000000000..7a39abb98a7 --- /dev/null +++ b/prowler/providers/aws/services/servicecatalog/servicecatalog_service.py @@ -0,0 +1,101 @@ +from typing import Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.lib.scan_filters.scan_filters import is_resource_filtered +from prowler.providers.aws.lib.service.service import AWSService + + +class ServiceCatalog(AWSService): + def __init__(self, provider): + # Call AWSService's __init__ + super().__init__(__class__.__name__, provider) + self.portfolios = {} + self.__threading_call__(self._list_portfolios) + self.__threading_call__( + self._describe_portfolio_shares, self.portfolios.values() + ) + self.__threading_call__(self._describe_portfolio, self.portfolios.values()) + + def _list_portfolios(self, regional_client): + logger.info("ServiceCatalog - listing portfolios...") + try: + response = regional_client.list_portfolios() + for portfolio in response["PortfolioDetails"]: + portfolio_arn = portfolio["ARN"] + if not self.audit_resources or ( + is_resource_filtered(portfolio_arn, self.audit_resources) + ): + self.portfolios[portfolio_arn] = Portfolio( + arn=portfolio_arn, + id=portfolio["Id"], + name=portfolio["DisplayName"], + region=regional_client.region, + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _describe_portfolio_shares(self, portfolio): + try: + logger.info("ServiceCatalog - describing portfolios shares...") + try: + regional_client = self.regional_clients[portfolio.region] + types = [ + "ACCOUNT", + "ORGANIZATION", + "ORGANIZATION_UNIT", + "ORGANIZATION_MEMBER_ACCOUNT", + ] + for portfolio_type in types: + portfolio_shares = regional_client.describe_portfolio_shares( + PortfolioId=portfolio.id, + Type=portfolio_type, + )["PortfolioShareDetails"] + for share in portfolio_shares: + portfolio_share = PortfolioShare( + type=portfolio_type, + accepted=share["Accepted"], + ) + portfolio.shares.append(portfolio_share) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _describe_portfolio(self, portfolio): + try: + logger.info("ServiceCatalog - describing portfolios...") + try: + regional_client = self.regional_clients[portfolio.region] + portfolio.tags = regional_client.describe_portfolio( + PortfolioId=portfolio.id, + )["Tags"] + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +class PortfolioShare(BaseModel): + type: str + accepted: bool + + +class Portfolio(BaseModel): + id: str + name: str + arn: str + region: str + shares: Optional[list[PortfolioShare]] = [] + tags: Optional[list] = [] diff --git a/tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py b/tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py new file mode 100644 index 00000000000..25f5f808c64 --- /dev/null +++ b/tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py @@ -0,0 +1,108 @@ +from unittest.mock import patch + +import botocore +from moto import mock_aws + +from prowler.providers.aws.services.servicecatalog.servicecatalog_service import ( + ServiceCatalog, +) +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "ListPortfolios": + return { + "PortfolioDetails": [ + { + "Id": "portfolio-id-test", + "ARN": "arn:aws:servicecatalog:eu-west-1:123456789012:portfolio/portfolio-id-test", + "DisplayName": "portfolio-name", + } + ], + } + elif operation_name == "DescribePortfolioShares": + return { + "PortfolioShareDetails": [ + { + "Type": "ACCOUNT", + "Accepted": True, + } + ], + } + elif operation_name == "DescribePortfolio": + return { + "Tags": {"tag1": "value1", "tag2": "value2"}, + } + return make_api_call(self, operation_name, kwarg) + + +def mock_generate_regional_clients(provider, service): + regional_client = provider._session.current_session.client( + service, region_name=AWS_REGION_EU_WEST_1 + ) + regional_client.region = AWS_REGION_EU_WEST_1 + return {AWS_REGION_EU_WEST_1: regional_client} + + +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +@patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, +) +class Test_ServiceCatalog_Service: + # Test ServiceCatalog Service + def test_service(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + service_catalog = ServiceCatalog(aws_provider) + assert service_catalog.service == "servicecatalog" + + # Test ServiceCatalog client + def test_client(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + service_catalog = ServiceCatalog(aws_provider) + for reg_client in service_catalog.regional_clients.values(): + assert reg_client.__class__.__name__ == "ServiceCatalog" + + # Test ServiceCatalog session + def test__get_session__(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + ses = ServiceCatalog(aws_provider) + assert ses.session.__class__.__name__ == "Session" + + @mock_aws + # Test ServiceCatalog list portfolios + def test_list_portfolios(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + service_catalog = ServiceCatalog(aws_provider) + arn = f"arn:aws:servicecatalog:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:portfolio/portfolio-id-test" + assert service_catalog.portfolios[arn].name == "portfolio-name" + assert service_catalog.portfolios[arn].id == "portfolio-id-test" + assert service_catalog.portfolios[arn].arn == arn + assert service_catalog.portfolios[arn].region == AWS_REGION_EU_WEST_1 + + @mock_aws + # Test ServiceCatalog describe + def test_describe_portfolio_shares(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + service_catalog = ServiceCatalog(aws_provider) + arn = f"arn:aws:servicecatalog:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:portfolio/portfolio-id-test" + assert len(service_catalog.portfolios[arn].shares) == 4 + assert service_catalog.portfolios[arn].shares[0].accepted + assert service_catalog.portfolios[arn].shares[0].type == "ACCOUNT" + + @mock_aws + # Test ServiceCatalog list queues + def test_describe_portfolio(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + service_catalog = ServiceCatalog(aws_provider) + arn = f"arn:aws:servicecatalog:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:portfolio/portfolio-id-test" + assert service_catalog.portfolios[arn].tags == { + "tag1": "value1", + "tag2": "value2", + } From c4360c9c311b2ddeb4b330c0fea5b542aae303e7 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Mon, 4 Nov 2024 11:21:17 +0100 Subject: [PATCH 2/3] fix(servicecatalog): Fix typo --- .../services/servicecatalog/servicecatalog_service_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py b/tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py index 25f5f808c64..3279d204db7 100644 --- a/tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py +++ b/tests/providers/aws/services/servicecatalog/servicecatalog_service_test.py @@ -87,7 +87,7 @@ def test_list_portfolios(self): assert service_catalog.portfolios[arn].region == AWS_REGION_EU_WEST_1 @mock_aws - # Test ServiceCatalog describe + # Test ServiceCatalog describe portfolio shares def test_describe_portfolio_shares(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) service_catalog = ServiceCatalog(aws_provider) @@ -97,7 +97,7 @@ def test_describe_portfolio_shares(self): assert service_catalog.portfolios[arn].shares[0].type == "ACCOUNT" @mock_aws - # Test ServiceCatalog list queues + # Test ServiceCatalog describe portfolio def test_describe_portfolio(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) service_catalog = ServiceCatalog(aws_provider) From 68592bece82399a2c3abc52543b0226540df349d Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 6 Nov 2024 11:10:45 -0500 Subject: [PATCH 3/3] chore: revision --- .../servicecatalog/servicecatalog_service.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/prowler/providers/aws/services/servicecatalog/servicecatalog_service.py b/prowler/providers/aws/services/servicecatalog/servicecatalog_service.py index 7a39abb98a7..31a1f798fed 100644 --- a/prowler/providers/aws/services/servicecatalog/servicecatalog_service.py +++ b/prowler/providers/aws/services/servicecatalog/servicecatalog_service.py @@ -6,6 +6,13 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService +PORTFOLIO_SHARE_TYPES = [ + "ACCOUNT", + "ORGANIZATION", + "ORGANIZATION_UNIT", + "ORGANIZATION_MEMBER_ACCOUNT", +] + class ServiceCatalog(AWSService): def __init__(self, provider): @@ -43,18 +50,11 @@ def _describe_portfolio_shares(self, portfolio): logger.info("ServiceCatalog - describing portfolios shares...") try: regional_client = self.regional_clients[portfolio.region] - types = [ - "ACCOUNT", - "ORGANIZATION", - "ORGANIZATION_UNIT", - "ORGANIZATION_MEMBER_ACCOUNT", - ] - for portfolio_type in types: - portfolio_shares = regional_client.describe_portfolio_shares( + for portfolio_type in PORTFOLIO_SHARE_TYPES: + for share in regional_client.describe_portfolio_shares( PortfolioId=portfolio.id, Type=portfolio_type, - )["PortfolioShareDetails"] - for share in portfolio_shares: + ).get("PortfolioShareDetails", []): portfolio_share = PortfolioShare( type=portfolio_type, accepted=share["Accepted"],