mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(apigateway): add check for secrets in REST API stage variables (#11188)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
@@ -26,6 +26,7 @@ The following list includes all the AWS checks with configurable variables that
|
||||
|---------------------------------------------------------------|--------------------------------------------------|-----------------|
|
||||
| `acm_certificates_expiration_check` | `days_to_expire_threshold` | Integer |
|
||||
| `acmpca_certificate_authority_pqc_key_algorithm` | `acmpca_pqc_key_algorithms` | List of Strings |
|
||||
| `apigateway_restapi_no_secrets_in_stage_variables` | `secrets_ignore_patterns` | List of Strings |
|
||||
| `appstream_fleet_maximum_session_duration` | `max_session_duration_seconds` | Integer |
|
||||
| `appstream_fleet_session_disconnect_timeout` | `max_disconnect_timeout_in_seconds` | Integer |
|
||||
| `appstream_fleet_session_idle_disconnect_timeout` | `max_idle_disconnect_timeout_in_seconds` | Integer |
|
||||
|
||||
@@ -12,6 +12,7 @@ The checks with this functionality are the following.
|
||||
|
||||
AWS:
|
||||
|
||||
- apigateway\_restapi\_no\_secrets\_in\_stage\_variables
|
||||
- autoscaling\_find\_secrets\_ec2\_launch\_configuration
|
||||
- awslambda\_function\_no\_secrets\_in\_code
|
||||
- awslambda\_function\_no\_secrets\_in\_variables
|
||||
|
||||
@@ -24,6 +24,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- CIS GitHub Benchmark v1.2.0 compliance framework for the GitHub provider [(#11719)](https://github.com/prowler-cloud/prowler/pull/11719)
|
||||
- AWS Bedrock AgentCore privilege escalation paths in the IAM privilege escalation checks, covering Runtime, Harness, Code Interpreter and Custom Browser [(#11726)](https://github.com/prowler-cloud/prowler/pull/11726)
|
||||
- `--scan-secrets-validate` flag and `aws.secrets_validate` configuration option to optionally validate the secrets discovered by the secret-scanning checks against the provider APIs; secrets confirmed to be live are reported as critical [(#11694)](https://github.com/prowler-cloud/prowler/pull/11694)
|
||||
- `apigateway_restapi_no_secrets_in_stage_variables` check for AWS provider, scanning API Gateway REST API stage variables for hardcoded secrets such as passwords, API keys, and tokens [(#11188)](https://github.com/prowler-cloud/prowler/pull/11188)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Provider": "aws",
|
||||
"CheckID": "apigateway_restapi_no_secrets_in_stage_variables",
|
||||
"CheckTitle": "API Gateway REST API stage variables should not contain secrets",
|
||||
"CheckType": [
|
||||
"Software and Configuration Checks/AWS Security Best Practices",
|
||||
"Software and Configuration Checks/Industry and Regulatory Standards/AWS Foundational Security Best Practices"
|
||||
],
|
||||
"ServiceName": "apigateway",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "arn:aws:apigateway:region::/restapis/api-id/stages/stage-name",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AwsApiGatewayStage",
|
||||
"ResourceGroup": "security",
|
||||
"Description": "Checks API Gateway REST API stage variables for hardcoded secrets such as passwords, API keys, and tokens. Stage variables should reference AWS Secrets Manager or Parameter Store rather than containing plaintext credentials.",
|
||||
"Risk": "Hardcoded secrets in stage variables are stored in plaintext in the AWS control plane and are visible to anyone with read access to the API Gateway configuration. This can lead to unauthorized access, credential theft, and lateral movement across systems.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_how-services-use-secrets_api-gateway.html"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "aws apigateway update-stage --rest-api-id <api-id> --stage-name <stage-name> --patch-operations op=remove,path=/variables/<variable-name>",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Open AWS Console > API Gateway\n2. Select the REST API and stage\n3. Go to Stage Variables tab\n4. Remove any variables containing plaintext secrets\n5. Reference secrets using AWS Secrets Manager integration instead",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remove hardcoded secrets from API Gateway stage variables. Use AWS Secrets Manager or Parameter Store to manage credentials and retrieve them at runtime using Lambda authorizers or integration request mapping templates.",
|
||||
"Url": "https://hub.prowler.com/check/apigateway_restapi_no_secrets_in_stage_variables"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"secrets"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "Infrastructure Protection"
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import json
|
||||
|
||||
from prowler.lib.check.models import Check, Check_Report_AWS
|
||||
from prowler.lib.utils.utils import (
|
||||
SecretsScanError,
|
||||
annotate_verified_secrets,
|
||||
detect_secrets_scan_batch,
|
||||
)
|
||||
from prowler.providers.aws.services.apigateway.apigateway_client import (
|
||||
apigateway_client,
|
||||
)
|
||||
|
||||
|
||||
class apigateway_restapi_no_secrets_in_stage_variables(Check):
|
||||
"""Check that API Gateway REST API stage variables contain no hardcoded secrets."""
|
||||
|
||||
def execute(self) -> list[Check_Report_AWS]:
|
||||
findings = []
|
||||
secrets_ignore_patterns = apigateway_client.audit_config.get(
|
||||
"secrets_ignore_patterns", []
|
||||
)
|
||||
validate = apigateway_client.audit_config.get("secrets_validate", False)
|
||||
|
||||
# Collect one payload per stage (its variables) and scan them all in
|
||||
# batched Kingfisher invocations instead of one subprocess per stage.
|
||||
# Findings are keyed by (rest_api index, stage index).
|
||||
def payloads():
|
||||
for api_index, rest_api in enumerate(apigateway_client.rest_apis):
|
||||
for stage_index, stage in enumerate(rest_api.stages):
|
||||
if stage.variables:
|
||||
yield (api_index, stage_index), json.dumps(
|
||||
stage.variables, indent=2
|
||||
)
|
||||
|
||||
scan_error = None
|
||||
try:
|
||||
batch_results = detect_secrets_scan_batch(
|
||||
payloads(),
|
||||
excluded_secrets=secrets_ignore_patterns,
|
||||
validate=validate,
|
||||
)
|
||||
except SecretsScanError as error:
|
||||
batch_results = {}
|
||||
scan_error = error
|
||||
|
||||
for api_index, rest_api in enumerate(apigateway_client.rest_apis):
|
||||
for stage_index, stage in enumerate(rest_api.stages):
|
||||
report = Check_Report_AWS(metadata=self.metadata(), resource=rest_api)
|
||||
report.resource_arn = stage.arn
|
||||
report.resource_id = f"{rest_api.name}/{stage.name}"
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"No secrets found in stage variables of API Gateway "
|
||||
f"REST API {rest_api.name} stage {stage.name}."
|
||||
)
|
||||
|
||||
if stage.variables:
|
||||
if scan_error:
|
||||
report.status = "MANUAL"
|
||||
report.status_extended = (
|
||||
f"Could not scan stage variables of API Gateway REST API "
|
||||
f"{rest_api.name} stage {stage.name} for secrets: "
|
||||
f"{scan_error}; manual review is required."
|
||||
)
|
||||
findings.append(report)
|
||||
continue
|
||||
|
||||
detect_secrets_output = batch_results.get((api_index, stage_index))
|
||||
if detect_secrets_output:
|
||||
variable_names = list(stage.variables.keys())
|
||||
secrets_string = ", ".join(
|
||||
[
|
||||
f"{secret['type']} in variable "
|
||||
f"{variable_names[secret['line_number'] - 2]}"
|
||||
for secret in detect_secrets_output
|
||||
]
|
||||
)
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Potential "
|
||||
f"{'secrets' if len(detect_secrets_output) > 1 else 'secret'} "
|
||||
f"found in stage variables of API Gateway REST API "
|
||||
f"{rest_api.name} stage {stage.name} -> {secrets_string}."
|
||||
)
|
||||
annotate_verified_secrets(report, detect_secrets_output)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -179,6 +179,7 @@ class APIGateway(AWSService):
|
||||
tracing_enabled=tracing_enabled,
|
||||
cache_enabled=cache_enabled,
|
||||
cache_data_encrypted=cache_data_encrypted,
|
||||
variables=stage.get("variables", {}),
|
||||
)
|
||||
)
|
||||
except ClientError as error:
|
||||
@@ -265,6 +266,7 @@ class Stage(BaseModel):
|
||||
tracing_enabled: Optional[bool] = None
|
||||
cache_enabled: Optional[bool] = None
|
||||
cache_data_encrypted: Optional[bool] = None
|
||||
variables: Optional[dict] = {}
|
||||
|
||||
|
||||
class PathResourceMethods(BaseModel):
|
||||
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
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_no_secrets_in_stage_variables:
|
||||
@mock_aws
|
||||
def test_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_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables.apigateway_client",
|
||||
new=APIGateway(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.apigateway.apigateway_restapi_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables import (
|
||||
apigateway_restapi_no_secrets_in_stage_variables,
|
||||
)
|
||||
|
||||
check = apigateway_restapi_no_secrets_in_stage_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_stage_with_no_variables(self):
|
||||
apigw = client("apigateway", region_name=AWS_REGION_US_EAST_1)
|
||||
rest_api = apigw.create_rest_api(name="test-api")
|
||||
api_id = rest_api["id"]
|
||||
|
||||
root_id = apigw.get_resources(restApiId=api_id)["items"][0]["id"]
|
||||
resource = apigw.create_resource(
|
||||
restApiId=api_id, parentId=root_id, pathPart="test"
|
||||
)
|
||||
apigw.put_method(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
authorizationType="NONE",
|
||||
)
|
||||
apigw.put_integration(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
type="HTTP",
|
||||
integrationHttpMethod="POST",
|
||||
uri="http://test.com",
|
||||
)
|
||||
apigw.create_deployment(restApiId=api_id, stageName="prod")
|
||||
|
||||
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_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables.apigateway_client",
|
||||
new=APIGateway(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.apigateway.apigateway_restapi_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables import (
|
||||
apigateway_restapi_no_secrets_in_stage_variables,
|
||||
)
|
||||
|
||||
check = apigateway_restapi_no_secrets_in_stage_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
"No secrets found in stage variables of API Gateway "
|
||||
"REST API test-api stage prod."
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "test-api/prod"
|
||||
|
||||
@mock_aws
|
||||
def test_stage_with_safe_variables(self):
|
||||
apigw = client("apigateway", region_name=AWS_REGION_US_EAST_1)
|
||||
rest_api = apigw.create_rest_api(name="test-api")
|
||||
api_id = rest_api["id"]
|
||||
|
||||
root_id = apigw.get_resources(restApiId=api_id)["items"][0]["id"]
|
||||
resource = apigw.create_resource(
|
||||
restApiId=api_id, parentId=root_id, pathPart="test"
|
||||
)
|
||||
apigw.put_method(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
authorizationType="NONE",
|
||||
)
|
||||
apigw.put_integration(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
type="HTTP",
|
||||
integrationHttpMethod="POST",
|
||||
uri="http://test.com",
|
||||
)
|
||||
apigw.create_deployment(restApiId=api_id, stageName="prod")
|
||||
apigw.update_stage(
|
||||
restApiId=api_id,
|
||||
stageName="prod",
|
||||
patchOperations=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/variables/environment",
|
||||
"value": "production",
|
||||
},
|
||||
{"op": "replace", "path": "/variables/region", "value": "us-east-1"},
|
||||
],
|
||||
)
|
||||
|
||||
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_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables.apigateway_client",
|
||||
new=APIGateway(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.apigateway.apigateway_restapi_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables import (
|
||||
apigateway_restapi_no_secrets_in_stage_variables,
|
||||
)
|
||||
|
||||
check = apigateway_restapi_no_secrets_in_stage_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].status_extended == (
|
||||
"No secrets found in stage variables of API Gateway "
|
||||
"REST API test-api stage prod."
|
||||
)
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "test-api/prod"
|
||||
|
||||
@mock_aws
|
||||
def test_stage_with_secrets_in_variables(self):
|
||||
apigw = client("apigateway", region_name=AWS_REGION_US_EAST_1)
|
||||
rest_api = apigw.create_rest_api(name="test-api")
|
||||
api_id = rest_api["id"]
|
||||
|
||||
root_id = apigw.get_resources(restApiId=api_id)["items"][0]["id"]
|
||||
resource = apigw.create_resource(
|
||||
restApiId=api_id, parentId=root_id, pathPart="test"
|
||||
)
|
||||
apigw.put_method(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
authorizationType="NONE",
|
||||
)
|
||||
apigw.put_integration(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
type="HTTP",
|
||||
integrationHttpMethod="POST",
|
||||
uri="http://test.com",
|
||||
)
|
||||
apigw.create_deployment(restApiId=api_id, stageName="prod")
|
||||
# A safe variable is added alongside the secret so the secret is not the
|
||||
# only variable present. This guards the line-number -> variable-name
|
||||
# mapping against an off-by-one that would otherwise still point at the
|
||||
# single variable and pass unnoticed.
|
||||
# A syntactically valid JSON Web Token that Kingfisher flags as a secret.
|
||||
apigw.update_stage(
|
||||
restApiId=api_id,
|
||||
stageName="prod",
|
||||
patchOperations=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/variables/environment",
|
||||
"value": "production",
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/variables/api_token",
|
||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
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_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables.apigateway_client",
|
||||
new=APIGateway(aws_provider),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.apigateway.apigateway_restapi_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables import (
|
||||
apigateway_restapi_no_secrets_in_stage_variables,
|
||||
)
|
||||
|
||||
check = apigateway_restapi_no_secrets_in_stage_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "test-api" in result[0].status_extended
|
||||
assert "prod" in result[0].status_extended
|
||||
assert "in variable api_token" in result[0].status_extended
|
||||
# The secret must be attributed to the correct variable, not the
|
||||
# safe one that precedes it.
|
||||
assert "in variable environment" not in result[0].status_extended
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "test-api/prod"
|
||||
|
||||
@mock_aws
|
||||
def test_stage_with_variables_scan_error(self):
|
||||
apigw = client("apigateway", region_name=AWS_REGION_US_EAST_1)
|
||||
rest_api = apigw.create_rest_api(name="test-api")
|
||||
api_id = rest_api["id"]
|
||||
|
||||
root_id = apigw.get_resources(restApiId=api_id)["items"][0]["id"]
|
||||
resource = apigw.create_resource(
|
||||
restApiId=api_id, parentId=root_id, pathPart="test"
|
||||
)
|
||||
apigw.put_method(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
authorizationType="NONE",
|
||||
)
|
||||
apigw.put_integration(
|
||||
restApiId=api_id,
|
||||
resourceId=resource["id"],
|
||||
httpMethod="GET",
|
||||
type="HTTP",
|
||||
integrationHttpMethod="POST",
|
||||
uri="http://test.com",
|
||||
)
|
||||
apigw.create_deployment(restApiId=api_id, stageName="prod")
|
||||
apigw.update_stage(
|
||||
restApiId=api_id,
|
||||
stageName="prod",
|
||||
patchOperations=[
|
||||
{"op": "replace", "path": "/variables/api_token", "value": "value"},
|
||||
],
|
||||
)
|
||||
|
||||
from prowler.lib.utils.utils import SecretsScanError
|
||||
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_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables.apigateway_client",
|
||||
new=APIGateway(aws_provider),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.apigateway.apigateway_restapi_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables.detect_secrets_scan_batch",
|
||||
side_effect=SecretsScanError("Kingfisher failed"),
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.apigateway.apigateway_restapi_no_secrets_in_stage_variables.apigateway_restapi_no_secrets_in_stage_variables import (
|
||||
apigateway_restapi_no_secrets_in_stage_variables,
|
||||
)
|
||||
|
||||
check = apigateway_restapi_no_secrets_in_stage_variables()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "manual review is required" in result[0].status_extended
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert result[0].resource_id == "test-api/prod"
|
||||
Reference in New Issue
Block a user