feat(servicecatalog): Add new check servicecatalog_portfolio_shared_within_organization_only (#5632)

Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
This commit is contained in:
Mario Rodriguez Lopez
2024-11-08 18:22:13 +01:00
committed by GitHub
parent 23929b3e68
commit 716558ffcb
5 changed files with 268 additions and 9 deletions
@@ -0,0 +1,34 @@
{
"Provider": "aws",
"CheckID": "servicecatalog_portfolio_shared_within_organization_only",
"CheckTitle": "Service Catalog portfolios should be shared within an AWS organization only",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices"
],
"ServiceName": "servicecatalog",
"SubServiceName": "",
"ResourceIdTemplate": "arn:aws:servicecatalog:{region}:{account-id}:portfolio/{portfolio-id}",
"Severity": "high",
"ResourceType": "AwsServiceCatalogPortfolio",
"Description": "This control checks whether AWS Service Catalog shares portfolios within an organization when the integration with AWS Organizations is enabled. The control fails if portfolios aren't shared within an organization.",
"Risk": "Sharing Service Catalog portfolios outside of an organization may result in access granted to unintended AWS accounts, potentially exposing sensitive resources.",
"RelatedUrl": "https://docs.aws.amazon.com/servicecatalog/latest/adminguide/catalogs_portfolios_sharing.html",
"Remediation": {
"Code": {
"CLI": "aws servicecatalog create-portfolio-share --portfolio-id <portfolio-id> --organization-ids <org-id>",
"NativeIaC": "",
"Other": "https://docs.aws.amazon.com/servicecatalog/latest/adminguide/catalogs_portfolios_sharing.html",
"Terraform": ""
},
"Recommendation": {
"Text": "Configure AWS Service Catalog to share portfolios only within your AWS Organization for more secure access management.",
"Url": "https://docs.aws.amazon.com/servicecatalog/latest/adminguide/catalogs_portfolios_sharing.html"
}
},
"Categories": [
"trustboundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,32 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.organizations.organizations_client import (
organizations_client,
)
from prowler.providers.aws.services.servicecatalog.servicecatalog_client import (
servicecatalog_client,
)
class servicecatalog_portfolio_shared_within_organization_only(Check):
def execute(self):
findings = []
for org in organizations_client.organizations:
if org.status == "ACTIVE":
for portfolio in servicecatalog_client.portfolios.values():
if portfolio.shares is not None:
report = Check_Report_AWS(self.metadata())
report.region = portfolio.region
report.resource_id = portfolio.id
report.resource_arn = portfolio.arn
report.resource_tags = portfolio.tags
report.status = "PASS"
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared within your AWS Organization."
for portfolio_share in portfolio.shares:
if portfolio_share.type == "ACCOUNT":
report.status = "FAIL"
report.status_extended = f"ServiceCatalog Portfolio {portfolio.name} is shared with an account."
break
findings.append(report)
return findings
@@ -9,7 +9,7 @@ from prowler.providers.aws.lib.service.service import AWSService
PORTFOLIO_SHARE_TYPES = [
"ACCOUNT",
"ORGANIZATION",
"ORGANIZATION_UNIT",
"ORGANIZATIONAL_UNIT",
"ORGANIZATION_MEMBER_ACCOUNT",
]
@@ -48,9 +48,9 @@ class ServiceCatalog(AWSService):
def _describe_portfolio_shares(self, portfolio):
try:
logger.info("ServiceCatalog - describing portfolios shares...")
try:
regional_client = self.regional_clients[portfolio.region]
for portfolio_type in PORTFOLIO_SHARE_TYPES:
regional_client = self.regional_clients[portfolio.region]
for portfolio_type in PORTFOLIO_SHARE_TYPES:
try:
for share in regional_client.describe_portfolio_shares(
PortfolioId=portfolio.id,
Type=portfolio_type,
@@ -60,10 +60,16 @@ class ServiceCatalog(AWSService):
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:
if error.response["Error"]["Code"] == "AccessDeniedException":
logger.error(
f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
portfolio.shares = None
else:
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}"
@@ -75,7 +81,7 @@ class ServiceCatalog(AWSService):
try:
regional_client = self.regional_clients[portfolio.region]
portfolio.tags = regional_client.describe_portfolio(
PortfolioId=portfolio.id,
Id=portfolio.id,
)["Tags"]
except Exception as error:
logger.error(
@@ -0,0 +1,187 @@
from unittest import mock
import botocore
from boto3 import client
from moto import mock_aws
from prowler.providers.aws.services.organizations.organizations_service import (
Organizations,
)
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-account-test",
"ARN": "arn:aws:servicecatalog:eu-west-1:123456789012:portfolio/portfolio-account-test",
"DisplayName": "portfolio-account",
}
],
}
elif operation_name == "DescribePortfolioShares":
return {
"PortfolioShareDetails": [
{
"Type": "ACCOUNT",
"Accepted": True,
}
],
}
return make_api_call(self, operation_name, kwarg)
def mock_make_api_call_v2(self, operation_name, kwarg):
if operation_name == "ListPortfolios":
return {
"PortfolioDetails": [
{
"Id": "portfolio-org-test",
"ARN": "arn:aws:servicecatalog:eu-west-1:123456789012:portfolio/portfolio-org-test",
"DisplayName": "portfolio-org",
}
],
}
elif operation_name == "DescribePortfolioShares":
if kwarg["type"] == "ACCOUNT":
return {
"PortfolioShareDetails": [
{
"Type": "ORGANIZATION",
"Accepted": True,
}
],
}
return make_api_call(self, operation_name, kwarg)
class Test_servicecatalog_portfolio_shared_within_organization_only:
def test_no_portfolios(self):
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_client",
new=ServiceCatalog(aws_provider),
):
from prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only import (
servicecatalog_portfolio_shared_within_organization_only,
)
check = servicecatalog_portfolio_shared_within_organization_only()
result = check.execute()
assert len(result) == 0
@mock_aws
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
def test_organizations_not_active(self):
client("servicecatalog", region_name=AWS_REGION_EU_WEST_1)
aws_provider = set_mocked_aws_provider(
[AWS_REGION_EU_WEST_1], create_default_organization=False
)
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_client",
new=ServiceCatalog(aws_provider),
), mock.patch(
"prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only.organizations_client",
new=Organizations(aws_provider),
):
from prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only import (
servicecatalog_portfolio_shared_within_organization_only,
)
check = servicecatalog_portfolio_shared_within_organization_only()
result = check.execute()
assert len(result) == 0
@mock_aws
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
def test_portfolio_share_account(self):
client("servicecatalog", region_name=AWS_REGION_EU_WEST_1)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
conn = client("organizations")
conn.create_organization()
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_client",
new=ServiceCatalog(aws_provider),
), mock.patch(
"prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only.organizations_client",
new=Organizations(aws_provider),
):
from prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only import (
servicecatalog_portfolio_shared_within_organization_only,
)
check = servicecatalog_portfolio_shared_within_organization_only()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "ServiceCatalog Portfolio portfolio-account is shared with an account."
)
assert result[0].resource_id == "portfolio-account-test"
assert (
result[0].resource_arn
== f"arn:aws:servicecatalog:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:portfolio/portfolio-account-test"
)
assert result[0].region == AWS_REGION_EU_WEST_1
@mock_aws
@mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_v2)
def test_portfolio_share_organization(self):
client("servicecatalog", region_name=AWS_REGION_EU_WEST_1)
aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
conn = client("organizations")
conn.create_organization()
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=aws_provider,
), mock.patch(
"prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_client",
new=ServiceCatalog(aws_provider),
), mock.patch(
"prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only.organizations_client",
new=Organizations(aws_provider),
):
from prowler.providers.aws.services.servicecatalog.servicecatalog_portfolio_shared_within_organization_only.servicecatalog_portfolio_shared_within_organization_only import (
servicecatalog_portfolio_shared_within_organization_only,
)
check = servicecatalog_portfolio_shared_within_organization_only()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "ServiceCatalog Portfolio portfolio-org is shared within your AWS Organization."
)
assert result[0].resource_id == "portfolio-org-test"
assert (
result[0].resource_arn
== f"arn:aws:servicecatalog:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:portfolio/portfolio-org-test"
)
assert result[0].region == AWS_REGION_EU_WEST_1