From 1aca7a754c93f0aba21a52b1bfc1cd81bf74ee72 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:38:55 +0200 Subject: [PATCH] feat(apigateway): add new check `apigateway_restapi_cache_encrypted` (#5448) --- .../__init__.py | 0 ...eway_restapi_cache_encrypted.metadata.json | 34 ++ .../apigateway_restapi_cache_encrypted.py | 25 ++ .../services/apigateway/apigateway_service.py | 15 +- ...apigateway_restapi_cache_encrypted_test.py | 292 ++++++++++++++++++ .../apigateway/apigateway_service_test.py | 12 + 6 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/__init__.py create mode 100644 prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.metadata.json create mode 100644 prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.py create mode 100644 tests/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted_test.py diff --git a/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/__init__.py b/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.metadata.json b/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.metadata.json new file mode 100644 index 0000000000..4a0c67df92 --- /dev/null +++ b/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "apigateway_restapi_cache_encrypted", + "CheckTitle": "Check if API Gateway REST API cache data is encrypted at rest.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "apigateway", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:apigateway:region:account-id:/restapis/restapi-id/stages/stage-name", + "Severity": "medium", + "ResourceType": "AwsApiGatewayStage", + "Description": "This control checks whether all methods in API Gateway REST API stages that have cache enabled are encrypted. The control fails if any method in an API Gateway REST API stage is configured to cache and the cache is not encrypted.", + "Risk": "Without encryption, cached data in API Gateway REST APIs may be vulnerable to unauthorized access, potentially exposing sensitive information to users not authenticated to AWS.", + "RelatedUrl": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-caching.html#enable-api-gateway-caching", + "Remediation": { + "Code": { + "CLI": "aws apigateway update-stage --rest-api-id --stage-name --patch-operations op=replace,path=///caching/enabled,value=true op=replace,path=///caching/dataEncrypted,value=true", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/apigateway-controls.html#apigateway-5", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that API Gateway REST API cache data is encrypted at rest by enabling the 'Encrypt cache data' setting in the API Gateway stage configuration.", + "Url": "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-caching.html#enable-api-gateway-caching" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.py b/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.py new file mode 100644 index 0000000000..86c239f725 --- /dev/null +++ b/prowler/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.apigateway.apigateway_client import ( + apigateway_client, +) + + +class apigateway_restapi_cache_encrypted(Check): + def execute(self): + findings = [] + for rest_api in apigateway_client.rest_apis: + for stage in rest_api.stages: + if stage.cache_enabled: + report = Check_Report_AWS(self.metadata()) + report.region = rest_api.region + report.resource_id = rest_api.name + report.resource_arn = stage.arn + report.resource_tags = stage.tags + report.status = "PASS" + report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} in stage {stage.name} has cache encryption enabled." + if not stage.cache_data_encrypted: + report.status = "FAIL" + report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} in stage {stage.name} has cache enabled but cache data is not encrypted at rest." + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/apigateway/apigateway_service.py b/prowler/providers/aws/services/apigateway/apigateway_service.py index b738762bb1..ce0cb20fb4 100644 --- a/prowler/providers/aws/services/apigateway/apigateway_service.py +++ b/prowler/providers/aws/services/apigateway/apigateway_service.py @@ -114,11 +114,18 @@ class APIGateway(AWSService): waf = None logging = False client_certificate = False + cache_enabled = False + cache_data_encrypted = False if "webAclArn" in stage: waf = stage["webAclArn"] if "methodSettings" in stage: - if stage["methodSettings"]: - logging = True + for settings in stage["methodSettings"].values(): + if settings.get("loggingLevel"): + logging = True + if settings.get("cachingEnabled"): + cache_enabled = True + if settings.get("cacheDataEncrypted"): + cache_data_encrypted = True if "clientCertificateId" in stage: client_certificate = True arn = f"arn:{self.audited_partition}:apigateway:{regional_client.region}::/restapis/{rest_api.id}/stages/{stage['stageName']}" @@ -130,6 +137,8 @@ class APIGateway(AWSService): client_certificate=client_certificate, waf=waf, tags=[stage.get("tags")], + cache_enabled=cache_enabled, + cache_data_encrypted=cache_data_encrypted, ) ) except ClientError as error: @@ -213,6 +222,8 @@ class Stage(BaseModel): client_certificate: bool waf: Optional[str] tags: Optional[list] = [] + cache_enabled: Optional[bool] + cache_data_encrypted: Optional[bool] class PathResourceMethods(BaseModel): diff --git a/tests/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted_test.py b/tests/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted_test.py new file mode 100644 index 0000000000..4da331fa9d --- /dev/null +++ b/tests/providers/aws/services/apigateway/apigateway_restapi_cache_encrypted/apigateway_restapi_cache_encrypted_test.py @@ -0,0 +1,292 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_apigateway_restapi_cache_encrypted: + @mock_aws + def test_apigateway_no_rest_apis(self): + from prowler.providers.aws.services.apigateway.apigateway_service import ( + APIGateway, + ) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted.apigateway_client", + new=APIGateway(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted import ( + apigateway_restapi_cache_encrypted, + ) + + check = apigateway_restapi_cache_encrypted() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_apigateway_one_rest_api_with_cache_encrypted(self): + # Create APIGateway Mocked Resources + apigateway_client = client("apigateway", region_name=AWS_REGION_US_EAST_1) + rest_api = apigateway_client.create_rest_api( + name="test-rest-api", + ) + # Get the rest api's root id + root_resource_id = apigateway_client.get_resources(restApiId=rest_api["id"])[ + "items" + ][0]["id"] + resource = apigateway_client.create_resource( + restApiId=rest_api["id"], + parentId=root_resource_id, + pathPart="test-path", + ) + apigateway_client.put_method( + restApiId=rest_api["id"], + resourceId=resource["id"], + httpMethod="GET", + authorizationType="NONE", + ) + apigateway_client.put_integration( + restApiId=rest_api["id"], + resourceId=resource["id"], + httpMethod="GET", + type="HTTP", + integrationHttpMethod="POST", + uri="http://test.com", + ) + apigateway_client.create_deployment( + restApiId=rest_api["id"], + stageName="test", + ) + apigateway_client.update_stage( + restApiId=rest_api["id"], + stageName="test", + patchOperations=[ + { + "op": "replace", + "path": "/*/*/caching/enabled", + "value": "true", + }, + { + "op": "replace", + "path": "/*/*/caching/dataEncrypted", + "value": "true", + }, + ], + ) + from prowler.providers.aws.services.apigateway.apigateway_service import ( + APIGateway, + ) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted.apigateway_client", + new=APIGateway(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted import ( + apigateway_restapi_cache_encrypted, + ) + + check = apigateway_restapi_cache_encrypted() + result = check.execute() + + assert result[0].status == "PASS" + assert len(result) == 1 + assert ( + result[0].status_extended + == f"API Gateway test-rest-api ID {rest_api['id']} in stage test has cache encryption enabled." + ) + assert result[0].resource_id == "test-rest-api" + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:apigateway:{AWS_REGION_US_EAST_1}::/restapis/{rest_api['id']}/stages/test" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [None] + + @mock_aws + def test_apigateway_one_rest_api_without_cache_encrypted(self): + # Create APIGateway Mocked Resources + apigateway_client = client("apigateway", region_name=AWS_REGION_US_EAST_1) + # Create APIGateway Rest API + rest_api = apigateway_client.create_rest_api( + name="test-rest-api", + ) + # Get the rest api's root id + root_resource_id = apigateway_client.get_resources(restApiId=rest_api["id"])[ + "items" + ][0]["id"] + resource = apigateway_client.create_resource( + restApiId=rest_api["id"], + parentId=root_resource_id, + pathPart="test-path", + ) + apigateway_client.put_method( + restApiId=rest_api["id"], + resourceId=resource["id"], + httpMethod="GET", + authorizationType="NONE", + ) + apigateway_client.put_integration( + restApiId=rest_api["id"], + resourceId=resource["id"], + httpMethod="GET", + type="HTTP", + integrationHttpMethod="POST", + uri="http://test.com", + ) + apigateway_client.create_deployment( + restApiId=rest_api["id"], + stageName="test", + ) + apigateway_client.update_stage( + restApiId=rest_api["id"], + stageName="test", + patchOperations=[ + { + "op": "replace", + "path": "/*/*/caching/enabled", + "value": "true", + }, + { + "op": "replace", + "path": "/*/*/caching/dataEncrypted", + "value": "false", + }, + ], + ) + + from prowler.providers.aws.services.apigateway.apigateway_service import ( + APIGateway, + ) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted.apigateway_client", + new=APIGateway(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted import ( + apigateway_restapi_cache_encrypted, + ) + + check = apigateway_restapi_cache_encrypted() + result = check.execute() + + assert result[0].status == "FAIL" + assert len(result) == 1 + assert ( + result[0].status_extended + == f"API Gateway test-rest-api ID {rest_api['id']} in stage test has cache enabled but cache data is not encrypted at rest." + ) + assert result[0].resource_id == "test-rest-api" + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:apigateway:{AWS_REGION_US_EAST_1}::/restapis/{rest_api['id']}/stages/test" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [None] + + @mock_aws + def test_apigateway_one_rest_api_without_cache_enabled(self): + # Create APIGateway Mocked Resources + apigateway_client = client("apigateway", region_name=AWS_REGION_US_EAST_1) + # Create APIGateway Rest API + rest_api = apigateway_client.create_rest_api( + name="test-rest-api", + ) + # Get the rest api's root id + root_resource_id = apigateway_client.get_resources(restApiId=rest_api["id"])[ + "items" + ][0]["id"] + resource = apigateway_client.create_resource( + restApiId=rest_api["id"], + parentId=root_resource_id, + pathPart="test-path", + ) + apigateway_client.put_method( + restApiId=rest_api["id"], + resourceId=resource["id"], + httpMethod="GET", + authorizationType="NONE", + ) + apigateway_client.put_integration( + restApiId=rest_api["id"], + resourceId=resource["id"], + httpMethod="GET", + type="HTTP", + integrationHttpMethod="POST", + uri="http://test.com", + ) + apigateway_client.create_deployment( + restApiId=rest_api["id"], + stageName="test", + ) + apigateway_client.update_stage( + restApiId=rest_api["id"], + stageName="test", + patchOperations=[ + { + "op": "replace", + "path": "/*/*/caching/enabled", + "value": "false", + }, + { + "op": "replace", + "path": "/*/*/caching/cacheDataEncrypted", + "value": "false", + }, + ], + ) + + from prowler.providers.aws.services.apigateway.apigateway_service import ( + APIGateway, + ) + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted.apigateway_client", + new=APIGateway(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.apigateway.apigateway_restapi_cache_encrypted.apigateway_restapi_cache_encrypted import ( + apigateway_restapi_cache_encrypted, + ) + + check = apigateway_restapi_cache_encrypted() + result = check.execute() + + assert len(result) == 0 diff --git a/tests/providers/aws/services/apigateway/apigateway_service_test.py b/tests/providers/aws/services/apigateway/apigateway_service_test.py index 353df71253..c3a86e9e48 100644 --- a/tests/providers/aws/services/apigateway/apigateway_service_test.py +++ b/tests/providers/aws/services/apigateway/apigateway_service_test.py @@ -141,11 +141,23 @@ class Test_APIGateway_Service: "path": "/*/*/logging/loglevel", "value": "INFO", }, + { + "op": "replace", + "path": "/*/*/caching/enabled", + "value": "true", + }, + { + "op": "replace", + "path": "/*/*/caching/dataEncrypted", + "value": "false", + }, ], ) aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) apigateway = APIGateway(aws_provider) assert apigateway.rest_apis[0].stages[0].logging is True + assert apigateway.rest_apis[0].stages[0].cache_enabled is True + assert apigateway.rest_apis[0].stages[0].cache_data_encrypted is False # Test APIGateway _get_resources @mock_aws