-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(servicecatalog): Add new check `servicecatalog_portfolio_shared_…
…within_organization_only` (#5632) Co-authored-by: Sergio Garcia <38561120+sergargar@users.noreply.github.com>
- Loading branch information
1 parent
23929b3
commit 716558f
Showing
5 changed files
with
268 additions
and
9 deletions.
There are no files selected for viewing
Empty file.
34 changes: 34 additions & 0 deletions
34
..._organization_only/servicecatalog_portfolio_shared_within_organization_only.metadata.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": "" | ||
} |
32 changes: 32 additions & 0 deletions
32
...ared_within_organization_only/servicecatalog_portfolio_shared_within_organization_only.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
...within_organization_only/servicecatalog_portfolio_shared_within_organization_only_test.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |