feat(checks): PHP runtime coverage after the October security release (#13127)

Co-authored-by: Rubén De la Torre Vico <98956809+puchy22@users.noreply.github.com>
This commit is contained in:
Pedro MartínandRubén De la Torre Vico authored and GitHub committed 2026-10-23 12:49:36 +02:00
1 parent 35d25a7218
commit 669df91ac9
63 files changed
+1928 -732

No files matched your search

+23
View File
@@ -164,6 +164,28 @@ mainConfig:
# aws.appstream_fleet_maximum_session_duration
max_session_duration_seconds: 36000 # 10 Hours
# AWS Elastic Beanstalk Configuration
# aws.elasticbeanstalk_environment_php_platform_updated
# Oldest PHP branch still receiving security fixes
elasticbeanstalk_php_min_branch: "8.2"
# Oldest Amazon Linux 2023 PHP platform version bundling the fixed PHP release
elasticbeanstalk_php_min_platform_version: "4.13.13"
# AWS Lightsail Configuration
# aws.lightsail_instance_php_blueprint_updated
# Blueprint id prefixes that ship PHP and require manual runtime verification
lightsail_php_blueprints:
[
"wordpress",
"lamp",
"nginx",
"drupal",
"joomla",
"magento",
"prestashop",
"plesk",
"cpanel",
]
# AWS Lambda Configuration
# aws.awslambda_function_using_supported_runtimes
obsolete_lambda_runtimes:
@@ -501,6 +523,7 @@ mainConfig:
shodan_api_key: null
# Azure App Service
# Minimum runtime versions; any newer version passes
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.5"
# azure.app_ensure_python_version_is_latest
@@ -39,6 +39,9 @@ The following list includes all the AWS checks with configurable variables that
| `awslambda_function_no_secrets_in_variables` | `secrets_ignore_patterns` | List of Strings | `[]` |
| `awslambda_function_using_supported_runtimes` | `obsolete_lambda_runtimes` | List of Strings | See `config.yaml` |
| `awslambda_function_vpc_multi_az` | `lambda_min_azs` | Integer | `2` |
| `elasticbeanstalk_environment_php_platform_updated` | `elasticbeanstalk_php_min_branch` | String | `"8.2"` |
| `elasticbeanstalk_environment_php_platform_updated` | `elasticbeanstalk_php_min_platform_version` | String | `"4.13.13"` |
| `lightsail_instance_php_blueprint_updated` | `lightsail_php_blueprints` | List of Strings | See `config.yaml` |
| `cloudformation_stack_cdktoolkit_bootstrap_version` | `recommended_cdk_bootstrap_version` | Integer | `21` |
| `cloudformation_stack_outputs_find_secrets` | `secrets_ignore_patterns` | List of Strings | `[]` |
| `cloudfront_distributions_pqc_tls_enabled` | `cloudfront_pqc_min_protocol_versions` | List of Strings | `["TLSv1.3_2025"]` |
@@ -231,6 +234,7 @@ The following list includes all the GCP checks with configurable variables that
| Check Name | Value | Type | Default |
|---------------------------------------------------|-----------------------------------|---------|---------|
| `appengine_version_php_runtime_supported` | `appengine_php_min_version` | String | `"8.2"` |
| `cloudstorage_bucket_sufficient_retention_period` | `storage_min_retention_days` | Integer | `90` |
| `compute_instance_group_multiple_zones` | `mig_min_zones` | Integer | `2` |
| `compute_public_address_shodan` | `shodan_api_key` | String | `null` |
@@ -448,6 +452,28 @@ aws:
# aws.appstream_fleet_maximum_session_duration
max_session_duration_seconds: 36000 # 10 Hours
# AWS Elastic Beanstalk Configuration
# aws.elasticbeanstalk_environment_php_platform_updated
# Oldest PHP branch still receiving security fixes
elasticbeanstalk_php_min_branch: "8.2"
# Oldest Amazon Linux 2023 PHP platform version bundling the fixed PHP release
elasticbeanstalk_php_min_platform_version: "4.13.13"
# AWS Lightsail Configuration
# aws.lightsail_instance_php_blueprint_updated
# Blueprint id prefixes that ship PHP and require manual runtime verification
lightsail_php_blueprints:
[
"wordpress",
"lamp",
"nginx",
"drupal",
"joomla",
"magento",
"prestashop",
"plesk",
"cpanel",
]
# AWS Lambda Configuration
# aws.awslambda_function_using_supported_runtimes
obsolete_lambda_runtimes:
@@ -718,6 +744,7 @@ azure:
shodan_api_key: null
# Azure App Service
# Minimum runtime versions; any newer version passes
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.5"
# azure.app_ensure_python_version_is_latest
@@ -0,0 +1 @@
`elasticbeanstalk_environment_php_platform_updated` check to flag Elastic Beanstalk PHP environments on branches without security fixes or on platform versions older than the one bundling the fixed PHP release
@@ -0,0 +1 @@
`lightsail_instance_php_blueprint_updated` check to flag Lightsail instances created from PHP blueprints for manual verification of the PHP release
@@ -0,0 +1 @@
`wafv2_webacl_managed_rule_groups_enabled` check to verify WAFv2 Web ACLs reference at least one managed rule group
@@ -0,0 +1 @@
Azure App Service runtime checks (`app_ensure_php_version_is_latest`, `app_ensure_python_version_is_latest`, `app_ensure_java_version_is_latest`) now pass any runtime at or above the configured minimum instead of failing every version newer than the configured one
@@ -0,0 +1 @@
`appengine` GCP service and `appengine_version_php_runtime_supported` check to flag App Engine serving versions on PHP runtimes without security fixes
@@ -123,7 +123,8 @@
"inspector2_active_findings_exist",
"awslambda_function_using_supported_runtimes",
"rds_instance_deprecated_engine_version",
"rds_instance_extended_support"
"rds_instance_extended_support",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -160,7 +161,8 @@
"redshift_cluster_automatic_upgrades",
"kafka_cluster_uses_latest_version",
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"ecs_service_fargate_latest_platform_version"
"ecs_service_fargate_latest_platform_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -215,7 +217,8 @@
"eks_cluster_uses_a_supported_version",
"ecs_service_fargate_latest_platform_version",
"kafka_cluster_uses_latest_version",
"opensearch_service_domains_updated_to_the_latest_service_software_version"
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -60,7 +60,8 @@
"wafv2_webacl_logging_enabled",
"wafv2_webacl_rule_logging_enabled",
"apigateway_restapi_waf_acl_attached",
"cognito_user_pool_waf_acl_attached"
"cognito_user_pool_waf_acl_attached",
"wafv2_webacl_managed_rule_groups_enabled"
]
},
{
+2 -1
View File
@@ -5142,7 +5142,8 @@
"ssm_managed_compliant_patching",
"elasticache_redis_cluster_auto_minor_version_upgrades",
"codeartifact_packages_external_public_publishing_disabled",
"elasticbeanstalk_environment_managed_updates_enabled"
"elasticbeanstalk_environment_managed_updates_enabled",
"elasticbeanstalk_environment_php_platform_updated"
]
},
{
@@ -2856,7 +2856,8 @@
"waf_regional_webacl_with_rules",
"wafv2_webacl_logging_enabled",
"wafv2_webacl_rule_logging_enabled",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"ConfigRequirements": [
{
@@ -3619,7 +3620,8 @@
"rds_instance_deprecated_engine_version",
"rds_instance_minor_version_upgrade_enabled",
"redshift_cluster_automatic_upgrades",
"ssm_managed_compliant_patching"
"ssm_managed_compliant_patching",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -2859,7 +2859,8 @@
"waf_regional_webacl_with_rules",
"wafv2_webacl_logging_enabled",
"wafv2_webacl_rule_logging_enabled",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"ConfigRequirements": [
{
@@ -3622,7 +3623,8 @@
"rds_instance_deprecated_engine_version",
"rds_instance_minor_version_upgrade_enabled",
"redshift_cluster_automatic_upgrades",
"ssm_managed_compliant_patching"
"ssm_managed_compliant_patching",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
+4 -2
View File
@@ -1128,7 +1128,8 @@
"Description": "requirements regarding security updates throughout the entire lifetime of the ICT services or ICT products, or replacement after the end of the support period;",
"Checks": [
"elasticbeanstalk_environment_managed_updates_enabled",
"opensearch_service_domains_updated_to_the_latest_service_software_version"
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
@@ -1253,7 +1254,8 @@
"memorydb_cluster_auto_minor_version_upgrades",
"elasticbeanstalk_environment_managed_updates_enabled",
"elasticache_redis_cluster_auto_minor_version_upgrades",
"glue_development_endpoints_job_bookmark_encryption_enabled"
"glue_development_endpoints_job_bookmark_encryption_enabled",
"elasticbeanstalk_environment_php_platform_updated"
],
"Attributes": [
{
+7 -2
View File
@@ -280,7 +280,11 @@
"ec2_instance_with_outdated_ami",
"eks_cluster_uses_a_supported_version",
"kafka_cluster_uses_latest_version",
"rds_instance_deprecated_engine_version"
"rds_instance_deprecated_engine_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"gcp": [
"appengine_version_php_runtime_supported"
]
}
},
@@ -3717,7 +3721,8 @@
"waf_regional_rule_with_conditions",
"waf_regional_rulegroup_not_empty",
"waf_regional_webacl_with_rules",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"cloudflare": [
"dns_record_proxied",
+7 -2
View File
@@ -6363,7 +6363,8 @@
"cloudfront_distributions_using_waf",
"guardduty_is_enabled",
"shield_advanced_protection_in_cloudfront_distributions",
"shield_advanced_protection_in_internet_facing_load_balancers"
"shield_advanced_protection_in_internet_facing_load_balancers",
"wafv2_webacl_managed_rule_groups_enabled"
],
"azure": [
"defender_ensure_defender_for_arm_is_on",
@@ -8481,7 +8482,8 @@
"memorydb_cluster_auto_minor_version_upgrades",
"mq_broker_auto_minor_version_upgrades",
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"kafka_cluster_uses_latest_version"
"kafka_cluster_uses_latest_version",
"elasticbeanstalk_environment_php_platform_updated"
],
"azure": [
"app_ensure_java_version_is_latest",
@@ -8492,6 +8494,9 @@
],
"alibabacloud": [
"ecs_instance_latest_os_patches_applied"
],
"gcp": [
"appengine_version_php_runtime_supported"
]
}
},
+9 -4
View File
@@ -354,7 +354,8 @@
"kafka_cluster_uses_latest_version",
"opensearch_service_domains_updated_to_the_latest_service_software_version",
"rds_instance_deprecated_engine_version",
"wellarchitected_workload_no_high_or_medium_risks"
"wellarchitected_workload_no_high_or_medium_risks",
"elasticbeanstalk_environment_php_platform_updated"
],
"azure": [],
"gcp": [],
@@ -718,7 +719,8 @@
"waf_regional_rule_with_conditions",
"waf_regional_rulegroup_not_empty",
"waf_regional_webacl_with_rules",
"wafv2_webacl_with_rules"
"wafv2_webacl_with_rules",
"wafv2_webacl_managed_rule_groups_enabled"
],
"azure": [
"network_vnet_ddos_protection_enabled",
@@ -1756,7 +1758,8 @@
"ecr_registry_enhanced_scanning_enabled",
"inspector2_active_findings_exist",
"inspector2_is_enabled",
"ssm_managed_compliant_patching"
"ssm_managed_compliant_patching",
"elasticbeanstalk_environment_php_platform_updated"
],
"azure": [
"app_ensure_java_version_is_latest",
@@ -1765,7 +1768,9 @@
"app_function_latest_runtime_version",
"defender_ensure_system_updates_are_applied"
],
"gcp": [],
"gcp": [
"appengine_version_php_runtime_supported"
],
"kubernetes": [],
"m365": []
},
+3 -1
View File
@@ -4427,7 +4427,9 @@
"ComplementaryCriteria": ""
}
],
"Checks": []
"Checks": [
"appengine_version_php_runtime_supported"
]
},
{
"Id": "OPS-33.01AS",
+4 -2
View File
@@ -656,7 +656,8 @@
"apikeys_api_restrictions_configured",
"apikeys_key_rotated_in_90_days",
"artifacts_container_analysis_enabled",
"bigquery_dataset_cmk_encryption"
"bigquery_dataset_cmk_encryption",
"appengine_version_php_runtime_supported"
],
"Attributes": [
{
@@ -733,7 +734,8 @@
"Checks": [
"compute_instance_shielded_vm_enabled",
"gcr_container_scanning_enabled",
"artifacts_container_analysis_enabled"
"artifacts_container_analysis_enabled",
"appengine_version_php_runtime_supported"
],
"Attributes": [
{
@@ -81,7 +81,8 @@
"compute_instance_shielded_vm_enabled",
"compute_project_os_login_enabled",
"artifacts_container_analysis_enabled",
"gcr_container_scanning_enabled"
"gcr_container_scanning_enabled",
"appengine_version_php_runtime_supported"
]
},
{
+27
View File
@@ -139,6 +139,28 @@ aws:
# aws.appstream_fleet_maximum_session_duration
max_session_duration_seconds: 36000 # 10 Hours
# AWS Elastic Beanstalk Configuration
# aws.elasticbeanstalk_environment_php_platform_updated
# Oldest PHP branch still receiving security fixes
elasticbeanstalk_php_min_branch: "8.2"
# Oldest Amazon Linux 2023 PHP platform version bundling the fixed PHP release
elasticbeanstalk_php_min_platform_version: "4.13.13"
# AWS Lightsail Configuration
# aws.lightsail_instance_php_blueprint_updated
# Blueprint id prefixes that ship PHP and require manual runtime verification
lightsail_php_blueprints:
[
"wordpress",
"lamp",
"nginx",
"drupal",
"joomla",
"magento",
"prestashop",
"plesk",
"cpanel",
]
# AWS Lambda Configuration
# aws.awslambda_function_using_supported_runtimes
obsolete_lambda_runtimes:
@@ -550,6 +572,7 @@ azure:
defender_attack_path_minimal_risk_level: "High"
# Azure App Service
# Minimum runtime versions; any newer version passes
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.5"
# azure.app_ensure_python_version_is_latest
@@ -650,6 +673,10 @@ gcp:
# GCP Secret Manager Rotation Period
# gcp.secretmanager_secret_rotation_enabled
secretmanager_max_rotation_days: 90
# GCP App Engine PHP runtime
# gcp.appengine_version_php_runtime_supported
# Oldest PHP branch still receiving security fixes
appengine_php_min_version: "8.2"
# Kubernetes Configuration
kubernetes:
+22
View File
@@ -88,6 +88,8 @@ _validate_trusted_ips = validate_ip_networks
_validate_semver = make_dotted_version_validator(3, 3)
# "1.28" style strings (EKS minor versions).
_validate_eks_minor = make_dotted_version_validator(2, 2)
_validate_php_branch = make_dotted_version_validator(2, 2)
_validate_platform_version = make_dotted_version_validator(3, 3)
def _validate_account_ids(v: Optional[list[str]]) -> Optional[list[str]]:
@@ -309,6 +311,20 @@ class AWSProviderConfig(ProviderConfigBase):
),
)
# --- Elastic Beanstalk ------------------------------------------------
elasticbeanstalk_php_min_branch: Annotated[
Optional[str], AfterValidator(_validate_php_branch)
] = Field(
default=None,
description='Oldest PHP branch still receiving security fixes, e.g. "8.2".',
)
elasticbeanstalk_php_min_platform_version: Annotated[
Optional[str], AfterValidator(_validate_platform_version)
] = Field(
default=None,
description='Oldest Amazon Linux 2023 PHP platform version bundling the fixed PHP release, e.g. "4.13.13".',
)
# --- Lambda -----------------------------------------------------------
obsolete_lambda_runtimes: Optional[list[str]] = None
lambda_min_azs: Optional[int] = Field(
@@ -318,6 +334,12 @@ class AWSProviderConfig(ProviderConfigBase):
description="Min number of AZs a VPC-bound Lambda must span. Range: 1..6.",
)
# --- Lightsail --------------------------------------------------------
lightsail_php_blueprints: Optional[list[str]] = Field(
default=None,
description="Blueprint id prefixes that ship PHP and require manual runtime verification.",
)
# --- Organizations ----------------------------------------------------
organizations_enabled_regions: Optional[list[str]] = None
organizations_trusted_delegated_administrators: Annotated[
+13 -2
View File
@@ -1,10 +1,13 @@
"""GCP provider config schema with safety bounds."""
from typing import Optional
from typing import Annotated, Optional
from pydantic import Field
from pydantic import AfterValidator, Field
from prowler.config.schema.base import ProviderConfigBase
from prowler.config.schema.validators import make_dotted_version_validator
_validate_php_branch = make_dotted_version_validator(2, 2)
class GCPProviderConfig(ProviderConfigBase):
@@ -43,3 +46,11 @@ class GCPProviderConfig(ProviderConfigBase):
le=3650,
description="Min retention period on Cloud Storage buckets. Range: 1..3650.",
)
# --- App Engine -----------------------------------------------------
appengine_php_min_version: Annotated[
Optional[str], AfterValidator(_validate_php_branch)
] = Field(
default=None,
description='Oldest PHP branch still receiving security fixes, e.g. "8.2".',
)
@@ -0,0 +1,44 @@
{
"Provider": "aws",
"CheckID": "elasticbeanstalk_environment_php_platform_updated",
"CheckTitle": "Elastic Beanstalk PHP environment runs a platform version with the fixed PHP release",
"CheckType": [
"Software and Configuration Checks/Patch Management",
"Software and Configuration Checks/Vulnerabilities/CVE"
],
"ServiceName": "elasticbeanstalk",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "AwsElasticBeanstalkEnvironment",
"ResourceGroup": "compute",
"Description": "**Elastic Beanstalk PHP environments** are evaluated to confirm they run a supported PHP branch (`elasticbeanstalk_php_min_branch`, `8.2` by default) on an **Amazon Linux 2023 platform version** that bundles the fixed PHP release (`elasticbeanstalk_php_min_platform_version`, `4.13.13` by default). The platform family, version and PHP branch are read from the environment's solution stack name.",
"Risk": "PHP platform versions published before the fixed release bundle a PHP runtime vulnerable to **unauthenticated remote code execution** through crafted web requests, giving attackers code execution with the web worker's privileges, access to application data and the ability to crash the environment. Branches that reached end of life never receive the fix.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/elasticbeanstalk/latest/relnotes/release-2026-10-21-al2023-php84-php85.html",
"https://docs.aws.amazon.com/elasticbeanstalk/latest/relnotes/release-2026-10-22-al2023-php82-php83.html",
"https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features.platform.upgrade.html",
"https://www.php.net/supported-versions.php"
],
"Remediation": {
"Code": {
"CLI": "aws elasticbeanstalk update-environment --environment-name <environment-name> --solution-stack-name \"64bit Amazon Linux 2023 v4.13.13 running PHP 8.4\"",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::ElasticBeanstalk::Environment\n Properties:\n ApplicationName: <example_resource_name>\n SolutionStackName: \"64bit Amazon Linux 2023 v4.13.13 running PHP 8.4\" # Critical: platform version with the fixed PHP release\n```",
"Other": "1. Open the AWS Management Console and go to Elastic Beanstalk\n2. Select the environment and open its dashboard\n3. Under Platform, choose Change version\n4. Select platform version 4.13.13 or newer for a supported PHP branch (8.2 or newer)\n5. Click Save and wait for the environment update to finish",
"Terraform": "```hcl\nresource \"aws_elastic_beanstalk_environment\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n application = \"<example_resource_name>\"\n solution_stack_name = \"64bit Amazon Linux 2023 v4.13.13 running PHP 8.4\" # Critical: platform version with the fixed PHP release\n}\n```"
},
"Recommendation": {
"Text": "Update every PHP environment to the latest Amazon Linux 2023 platform version and keep **managed platform updates** enabled so future security releases are applied automatically. Move environments still on PHP 8.1 or older to a supported branch, since those branches receive no fixes. Validate in staging and roll out with a maintenance window.",
"Url": "https://hub.prowler.com/check/elasticbeanstalk_environment_php_platform_updated"
}
},
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [
"elasticbeanstalk_environment_managed_updates_enabled"
],
"Notes": "Only environments whose solution stack runs PHP are evaluated. Platform versions are compared only within the Amazon Linux 2023 family; older families are reported as failing because they did not receive fixed PHP releases."
}
@@ -0,0 +1,84 @@
import re
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_client import (
elasticbeanstalk_client,
)
# "64bit Amazon Linux 2023 v4.13.13 running PHP 8.4"
SOLUTION_STACK_RE = re.compile(
r"^(?P<family>.+?) v(?P<version>\d+(?:\.\d+)+) running PHP (?P<branch>\d+\.\d+)",
re.IGNORECASE,
)
# "arn:aws:elasticbeanstalk:us-east-1::platform/PHP 8.4 running on 64bit Amazon Linux 2023/4.13.13"
PLATFORM_ARN_RE = re.compile(
r"platform/PHP (?P<branch>\d+\.\d+) running on (?P<family>[^/]+)/(?P<version>\d+(?:\.\d+)+)",
re.IGNORECASE,
)
AMAZON_LINUX_YEAR_RE = re.compile(r"Amazon Linux (\d{4})")
# First Amazon Linux generation that received platform versions with the fixed PHP release
FIXED_PLATFORM_YEAR = 2023
def _version_tuple(value: str) -> tuple[int, ...]:
return tuple(int(part) for part in value.split(".")) if value else ()
def _family_has_fixed_releases(family: str) -> bool:
match = AMAZON_LINUX_YEAR_RE.search(family)
return bool(match) and int(match.group(1)) >= FIXED_PLATFORM_YEAR
def _php_platform(environment) -> tuple[str, str, str] | None:
"""Return (branch, family, version) for PHP environments, None otherwise."""
for pattern, value in (
(SOLUTION_STACK_RE, environment.solution_stack_name),
(PLATFORM_ARN_RE, environment.platform_arn),
):
match = pattern.search(value or "")
if match:
return match["branch"], match["family"], match["version"]
return None
class elasticbeanstalk_environment_php_platform_updated(Check):
def execute(self):
findings = []
# `or` covers an explicit null in the config file, which .get() would return as None
min_php_branch = (
elasticbeanstalk_client.audit_config.get("elasticbeanstalk_php_min_branch")
or "8.2"
)
min_platform_version = (
elasticbeanstalk_client.audit_config.get(
"elasticbeanstalk_php_min_platform_version"
)
or "4.13.13"
)
for environment in elasticbeanstalk_client.environments.values():
php_platform = _php_platform(environment)
if not php_platform:
continue
php_branch, family, platform_version = php_platform
stack = environment.solution_stack_name or environment.platform_arn
report = Check_Report_AWS(metadata=self.metadata(), resource=environment)
report.status = "FAIL"
if _version_tuple(php_branch) < _version_tuple(min_php_branch):
report.status_extended = f"Elastic Beanstalk environment {environment.name} runs PHP {php_branch}, a branch that no longer receives security fixes (minimum supported branch is {min_php_branch})."
elif not _family_has_fixed_releases(family):
report.status_extended = f"Elastic Beanstalk environment {environment.name} runs PHP {php_branch} on platform '{stack}', a platform family without fixed PHP releases; migrate it to a PHP {php_branch} Amazon Linux 2023 platform version {min_platform_version} or newer."
elif _version_tuple(platform_version) < _version_tuple(
min_platform_version
):
report.status_extended = f"Elastic Beanstalk environment {environment.name} runs PHP {php_branch} on platform version {platform_version}, below the minimum fixed platform version {min_platform_version}."
else:
report.status = "PASS"
report.status_extended = f"Elastic Beanstalk environment {environment.name} runs PHP {php_branch} on platform version {platform_version}, which includes the fixed PHP release."
findings.append(report)
return findings
@@ -39,6 +39,10 @@ class ElasticBeanstalk(AWSService):
application_name=environment["ApplicationName"],
name=environment["EnvironmentName"],
region=regional_client.region,
platform_arn=environment.get("PlatformArn", ""),
solution_stack_name=environment.get(
"SolutionStackName", ""
),
)
except Exception as error:
logger.error(
@@ -121,6 +125,8 @@ class Environment(BaseModel):
arn: str
region: str
application_name: str
platform_arn: str = ""
solution_stack_name: str = ""
health_reporting: Optional[str]
managed_platform_updates: Optional[str]
cloudwatch_stream_logs: Optional[str]
@@ -0,0 +1,43 @@
{
"Provider": "aws",
"CheckID": "lightsail_instance_php_blueprint_updated",
"CheckTitle": "Lightsail instance created from a PHP blueprint runs a fixed PHP release",
"CheckType": [
"Software and Configuration Checks/Patch Management",
"Software and Configuration Checks/Vulnerabilities/CVE"
],
"ServiceName": "lightsail",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:lightsail:region:account-id:Instance/resource-id",
"Severity": "high",
"ResourceType": "Other",
"ResourceGroup": "compute",
"Description": "**Lightsail instances** created from blueprints that ship PHP (WordPress, WordPress Multisite, LAMP, Nginx and other PHP applications, configurable through `lightsail_php_blueprints`) are flagged for manual verification, because the PHP runtime inside the instance is not visible through the Lightsail API and running instances are not patched in place.",
"Risk": "A PHP release older than the fixed versions is vulnerable to **unauthenticated remote code execution** through crafted web requests. Lightsail does not update running instances, and Bitnami-packaged blueprints bundle their own PHP copy that the system package manager never updates, so the instance stays exploitable until it is patched by hand or recreated from an updated blueprint.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-update-php-security-vulnerabilities.html",
"https://docs.aws.amazon.com/lightsail/latest/userguide/migrate-from-bitnami-to-lightsail-blueprints.html",
"https://www.php.net/supported-versions.php"
],
"Remediation": {
"Code": {
"CLI": "sudo apt update && sudo apt upgrade -y && sudo systemctl restart apache2 && php -v",
"NativeIaC": "",
"Other": "1. Create a snapshot of the instance\n2. Connect over SSH and check the PHP release: php -v on Lightsail-packaged blueprints, /opt/bitnami/php/bin/php -v on Bitnami-packaged blueprints\n3. Lightsail-packaged blueprints (WordPress, WordPress Multisite, LAMP, Nginx): run sudo apt update && sudo apt upgrade, then restart Apache (sudo systemctl restart apache2) or PHP-FPM on Nginx (sudo systemctl restart php8.2-fpm)\n4. Bitnami-packaged WordPress, WordPress Multisite and LAMP: create a new instance from the matching Lightsail-packaged blueprint, move the content, database and static IP, then delete the old instance\n5. Bitnami-packaged applications without a Lightsail-packaged equivalent (Drupal, Joomla, Magento, PrestaShop and others): install a package-managed PHP-FPM alongside the Bitnami copy and point the Bitnami web server at it, following the AWS guide linked above\n6. Confirm the web server uses the fixed release: 8.2.35, 8.3.36, 8.4.27, 8.5.12 or newer",
"Terraform": ""
},
"Recommendation": {
"Text": "Verify the PHP release on every instance created from a PHP blueprint and update it immediately. Prefer **Lightsail-packaged blueprints**, whose PHP is maintained through the system package manager, over Bitnami-packaged ones; where no Lightsail-packaged equivalent exists, replace the bundled Bitnami PHP with a package-managed PHP-FPM so future updates arrive through apt. Upgrade instances still on PHP 8.1 or older, since those branches receive no security fixes.",
"Url": "https://hub.prowler.com/check/lightsail_instance_php_blueprint_updated"
}
},
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [
"lightsail_instance_automated_snapshots"
],
"Notes": "This check always reports MANUAL for instances matching a PHP blueprint prefix, because the PHP version is only observable from inside the instance. Instances created from other blueprints are not evaluated."
}
@@ -0,0 +1,36 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.lightsail.lightsail_client import lightsail_client
default_php_blueprints = [
"wordpress",
"lamp",
"nginx",
"drupal",
"joomla",
"magento",
"prestashop",
"plesk",
"cpanel",
]
class lightsail_instance_php_blueprint_updated(Check):
def execute(self):
findings = []
php_blueprints = (
lightsail_client.audit_config.get("lightsail_php_blueprints")
or default_php_blueprints
)
for instance in lightsail_client.instances.values():
blueprint = instance.blueprint_id.lower()
if not any(blueprint.startswith(prefix) for prefix in php_blueprints):
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=instance)
report.status = "MANUAL"
report.status_extended = f"Lightsail instance {instance.name} was created from the PHP blueprint '{instance.blueprint_id}'; the PHP runtime cannot be inspected through the API, verify over SSH that it runs a fixed release (8.2.35, 8.3.36, 8.4.27, 8.5.12 or newer); Bitnami-packaged blueprints bundle a PHP copy that apt never updates, so migrate them to the Lightsail-packaged blueprint or replace the bundled PHP with a package-managed one."
findings.append(report)
return findings
@@ -81,6 +81,8 @@ class Lightsail(AWSService):
ip_address_type=instance.get("ipAddressType", "ipv4"),
ports=ports,
auto_snapshot=auto_snapshot_enabled,
blueprint_id=instance.get("blueprintId", ""),
blueprint_name=instance.get("blueprintName", ""),
)
except Exception as error:
@@ -183,6 +185,8 @@ class Instance(BaseModel):
ip_address_type: str
ports: List[PortRange]
auto_snapshot: bool
blueprint_id: str = ""
blueprint_name: str = ""
class Database(BaseModel):
@@ -133,11 +133,23 @@ class WAFv2(AWSService):
try:
rules = get_web_acl.get("WebACL", {}).get("Rules", [])
for rule in rules:
managed_statement = rule.get("Statement", {}).get(
"ManagedRuleGroupStatement"
)
new_rule = Rule(
name=rule.get("Name", ""),
cloudwatch_metrics_enabled=rule.get(
"VisibilityConfig", {}
).get("CloudWatchMetricsEnabled", False),
managed_rule_group=(
ManagedRuleGroup(
vendor_name=managed_statement.get("VendorName", ""),
name=managed_statement.get("Name", ""),
version=managed_statement.get("Version", ""),
)
if managed_statement
else None
),
)
if (
rule.get("Statement", {})
@@ -200,11 +212,20 @@ class Scope(Enum):
CLOUDFRONT = "CLOUDFRONT"
class ManagedRuleGroup(BaseModel):
"""Managed rule group referenced by a Web ACL rule."""
vendor_name: str
name: str
version: str = ""
class Rule(BaseModel):
"""Model representing a rule for the Web ACL."""
name: str
cloudwatch_metrics_enabled: bool = False
managed_rule_group: Optional[ManagedRuleGroup] = None
class WebAclv2(BaseModel):
@@ -0,0 +1,44 @@
{
"Provider": "aws",
"CheckID": "wafv2_webacl_managed_rule_groups_enabled",
"CheckTitle": "WAFv2 Web ACL uses managed rule groups",
"CheckType": [
"Software and Configuration Checks/AWS Security Best Practices",
"Effects/Data Exposure"
],
"ServiceName": "wafv2",
"SubServiceName": "",
"ResourceIdTemplate": "arn:partition:wafv2:region:account-id:scope/webacl/webacl-name/webacl-id",
"Severity": "medium",
"ResourceType": "AwsWafv2WebAcl",
"ResourceGroup": "network",
"Description": "**WAFv2 Web ACLs** are evaluated to confirm at least one rule references a **managed rule group** (`ManagedRuleGroupStatement`), such as the AWS Core rule set or the PHP application rule set, whose signatures the vendor updates when new attack patterns appear.",
"Risk": "A Web ACL built only from custom rules receives no vendor-maintained signatures, so newly disclosed exploitation patterns against the protected application reach the backend uninspected until someone writes a rule by hand. Managed rule groups are the compensating control cloud providers ship while runtime patches roll out, including request body size limits and runtime-specific rules.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups.html",
"https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-use-case.html#aws-managed-rule-groups-use-case-php-app",
"https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-baseline.html"
],
"Remediation": {
"Code": {
"CLI": "aws wafv2 get-web-acl --name <webacl-name> --scope <REGIONAL|CLOUDFRONT> --id <webacl-id> > webacl.json && <append {\"Name\":\"AWS-AWSManagedRulesCommonRuleSet\",\"Priority\":<unused-priority>,\"Statement\":{\"ManagedRuleGroupStatement\":{\"VendorName\":\"AWS\",\"Name\":\"AWSManagedRulesCommonRuleSet\"}},\"OverrideAction\":{\"None\":{}},\"VisibilityConfig\":{\"SampledRequestsEnabled\":true,\"CloudWatchMetricsEnabled\":true,\"MetricName\":\"AWSManagedRulesCommonRuleSet\"}} to .WebACL.Rules in webacl.json> && aws wafv2 update-web-acl --name <webacl-name> --scope <REGIONAL|CLOUDFRONT> --id <webacl-id> --lock-token <LockToken from webacl.json> --default-action <DefaultAction from webacl.json> --visibility-config <VisibilityConfig from webacl.json> --rules <Rules from webacl.json including the new rule>",
"NativeIaC": "```yaml\nResources:\n <example_resource_name>:\n Type: AWS::WAFv2::WebACL\n Properties:\n Name: <example_resource_name>\n Scope: REGIONAL\n DefaultAction:\n Allow: {}\n VisibilityConfig:\n SampledRequestsEnabled: true\n CloudWatchMetricsEnabled: true\n MetricName: <example_resource_name>\n Rules:\n - Name: AWS-AWSManagedRulesCommonRuleSet\n Priority: 0\n OverrideAction:\n None: {}\n Statement:\n ManagedRuleGroupStatement: # Critical: managed rule group to pass the check\n VendorName: AWS\n Name: AWSManagedRulesCommonRuleSet\n VisibilityConfig:\n SampledRequestsEnabled: true\n CloudWatchMetricsEnabled: true\n MetricName: AWSManagedRulesCommonRuleSet\n```",
"Other": "1. Open the AWS WAF console and select the Web ACL\n2. Open the Rules tab and choose Add rules > Add managed rule groups\n3. Enable the AWS Core rule set and the rule groups matching the protected application (for example PHP application); the console keeps the existing rules and default action\n4. Save the Web ACL and review sampled requests before switching rules from Count to Block",
"Terraform": "```hcl\nresource \"aws_wafv2_web_acl\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n scope = \"REGIONAL\"\n\n default_action {\n allow {}\n }\n\n rule {\n name = \"AWS-AWSManagedRulesCommonRuleSet\"\n priority = 0\n\n override_action {\n none {}\n }\n\n statement {\n managed_rule_group_statement { # Critical: managed rule group to pass the check\n name = \"AWSManagedRulesCommonRuleSet\"\n vendor_name = \"AWS\"\n }\n }\n\n visibility_config {\n cloudwatch_metrics_enabled = true\n metric_name = \"AWSManagedRulesCommonRuleSet\"\n sampled_requests_enabled = true\n }\n }\n\n visibility_config {\n cloudwatch_metrics_enabled = true\n metric_name = \"<example_resource_name>\"\n sampled_requests_enabled = true\n }\n}\n```"
},
"Recommendation": {
"Text": "Attach the **AWS Core rule set** to every Web ACL and add the managed rule groups that match the protected stack (PHP, SQL database, known bad inputs). Keep the default rule group version so vendor updates apply automatically, and review sampled requests before moving rules from Count to Block.",
"Url": "https://hub.prowler.com/check/wafv2_webacl_managed_rule_groups_enabled"
}
},
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [
"wafv2_webacl_with_rules",
"wafv2_webacl_logging_enabled"
],
"Notes": "Rule groups attached through Firewall Manager are not inspected for managed statements, so a Web ACL protected only by Firewall Manager policies may be reported as failing. UpdateWebACL replaces the whole rule list and default action, so any CLI or IaC remediation must resend the existing configuration together with the new managed rule group."
}
@@ -0,0 +1,26 @@
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.wafv2.wafv2_client import wafv2_client
class wafv2_webacl_managed_rule_groups_enabled(Check):
def execute(self):
findings = []
for web_acl in wafv2_client.web_acls.values():
report = Check_Report_AWS(metadata=self.metadata(), resource=web_acl)
managed_rule_groups = [
f"{rule.managed_rule_group.vendor_name}/{rule.managed_rule_group.name}"
for rule in web_acl.rules
if rule.managed_rule_group
]
report.status = "FAIL"
report.status_extended = (
f"AWS WAFv2 Web ACL {web_acl.name} does not use any managed rule group."
)
if managed_rule_groups:
report.status = "PASS"
report.status_extended = f"AWS WAFv2 Web ACL {web_acl.name} uses managed rule groups: {', '.join(managed_rule_groups)}."
findings.append(report)
return findings
Whitespace-only changes.
@@ -0,0 +1,32 @@
import re
_VERSION_RE = re.compile(r"\d+(?:\.\d+)*")
_JAVA_SUFFIX_RE = re.compile(r"-(?:java|jre)(\d+)", re.IGNORECASE)
def parse_runtime_version(value) -> tuple[int, ...]:
"""Return the first dotted number in a stack string as an int tuple, empty if none."""
match = _VERSION_RE.search("" if value is None else str(value))
return tuple(int(part) for part in match.group().split(".")) if match else ()
def parse_java_version(value) -> tuple[int, ...]:
"""Java major from 'JAVA|17-java17', 'TOMCAT|10.0-java11', '1.8' or '17'."""
match = _JAVA_SUFFIX_RE.search("" if value is None else str(value))
if match:
return (int(match.group(1)),)
version = parse_runtime_version(value)
# Legacy '1.8' naming for Java 8
if len(version) > 1 and version[0] == 1:
return (version[1],)
return version[:1]
def runtime_version_at_least(
current: tuple[int, ...], minimum: tuple[int, ...]
) -> bool:
"""Compare only the components the platform exposes (no patch level in linuxFxVersion)."""
if not current or not minimum:
return False
length = min(len(current), len(minimum))
return current[:length] >= minimum[:length]
@@ -1,7 +1,7 @@
{
"Provider": "azure",
"CheckID": "app_ensure_java_version_is_latest",
"CheckTitle": "App Service web app uses the latest supported Java version or 17 by default",
"CheckTitle": "App Service web app runs a supported Java version (17 or newer by default)",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
@@ -9,7 +9,7 @@
"Severity": "low",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "**Azure App Service web apps** that run **Java** are assessed to ensure their configured runtime uses the **latest supported major version** (LTS) for the environment, across Linux and Windows.\n\n*Only apps with Java enabled are considered.*",
"Description": "**Azure App Service web apps** that run **Java** are assessed to ensure their configured runtime major version is at or above the configured **minimum supported LTS** (`java_latest_version`, `17` by default), across Linux and Windows.\n\n*Only apps with Java enabled are considered.*",
"Risk": "Using an **outdated Java runtime** enables known exploits like **remote code execution**, unsafe **deserialization**, and **cryptographic flaws**, risking data theft and tampering (**confidentiality, integrity**) and outages or takeover (**availability**). Unsupported versions also delay critical security patches.",
"RelatedUrl": "",
"AdditionalURLs": [
@@ -1,4 +1,8 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.lib.runtime.runtime_version import (
parse_java_version,
runtime_version_at_least,
)
from prowler.providers.azure.services.app.app_client import app_client
@@ -14,27 +18,33 @@ class app_ensure_java_version_is_latest(Check):
subscription_id, subscription_id
)
for app in apps.values():
linux_framework = getattr(app.configurations, "linux_fx_version", "")
windows_framework_version = getattr(
app.configurations, "java_version", None
# The SDK can return None for linuxFxVersion, which getattr preserves
linux_framework = (
getattr(app.configurations, "linux_fx_version", "") or ""
)
windows_version = getattr(app.configurations, "java_version", None)
if "java" in linux_framework.lower() or windows_framework_version:
report = Check_Report_Azure(metadata=self.metadata(), resource=app)
report.subscription = subscription_id
report.status = "FAIL"
java_latest_version = app_client.audit_config.get(
"java_latest_version", "17"
)
report.status_extended = f"Java version is set to '{f'java{windows_framework_version}' if windows_framework_version else linux_framework}', but should be set to 'java {java_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
if "java" not in linux_framework.lower() and not windows_version:
continue
if (
f"java{java_latest_version}" in linux_framework
or java_latest_version == windows_framework_version
):
report.status = "PASS"
report.status_extended = f"Java version is set to 'java {java_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
framework = (
linux_framework
if "java" in linux_framework.lower()
else f"java{windows_version}"
)
minimum = app_client.audit_config.get("java_latest_version") or "17"
findings.append(report)
report = Check_Report_Azure(metadata=self.metadata(), resource=app)
report.subscription = subscription_id
report.status = "FAIL"
report.status_extended = f"Java version is set to '{framework}', which is below the minimum supported version 'java {minimum}', for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
if runtime_version_at_least(
parse_java_version(framework), parse_java_version(minimum)
):
report.status = "PASS"
report.status_extended = f"Java version is set to '{framework}', which meets the minimum supported version 'java {minimum}', for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
findings.append(report)
return findings
@@ -1,7 +1,7 @@
{
"Provider": "azure",
"CheckID": "app_ensure_php_version_is_latest",
"CheckTitle": "App Service web app uses the latest supported PHP version or 8.5 by default",
"CheckTitle": "App Service web app runs a supported PHP version (8.5 or newer by default)",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
@@ -9,7 +9,7 @@
"Severity": "low",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "**Azure App Service web apps** running **PHP** are evaluated to ensure the runtime is configured to the **latest supported** release. The finding compares the app's PHP stack (from `linuxFxVersion` or `php_version`) with the newest available version.",
"Description": "**Azure App Service web apps** running **PHP** are evaluated to ensure the runtime is at or above the configured **minimum supported** release. The finding compares the app's PHP stack (from `linuxFxVersion` or `php_version`) with `php_latest_version` (`8.5` by default); any version at or above it passes.",
"Risk": "Using **outdated PHP** enables exploitation of known flaws, including **remote code execution**, causing secret disclosure (confidentiality), unauthorized changes (integrity), and crashes or downtime (availability). Deprecated versions lack patches, widening exposure and instability.",
"RelatedUrl": "",
"AdditionalURLs": [
@@ -34,5 +34,5 @@
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "If your app is written using version-dependent features or libraries, they may not be available on the latest version. If you wish to upgrade, research the impact thoroughly. Upgrading may have unforeseen consequences that could result in downtime"
"Notes": "App Service exposes only the PHP major.minor version, not the patch level, so this check cannot confirm a specific patch release is deployed. If your app depends on version-specific features or libraries, research the impact before upgrading."
}
@@ -1,4 +1,8 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.lib.runtime.runtime_version import (
parse_runtime_version,
runtime_version_at_least,
)
from prowler.providers.azure.services.app.app_client import app_client
@@ -14,29 +18,33 @@ class app_ensure_php_version_is_latest(Check):
subscription_id, subscription_id
)
for app in apps.values():
framework = getattr(app.configurations, "linux_fx_version", "")
# The SDK can return None for linuxFxVersion, which getattr preserves
linux_framework = (
getattr(app.configurations, "linux_fx_version", "") or ""
)
windows_version = getattr(app.configurations, "php_version", "")
if "php" in framework.lower() or getattr(
app.configurations, "php_version", ""
if "php" not in linux_framework.lower() and not windows_version:
continue
framework = (
linux_framework
if "php" in linux_framework.lower()
else windows_version
)
minimum = app_client.audit_config.get("php_latest_version") or "8.5"
report = Check_Report_Azure(metadata=self.metadata(), resource=app)
report.subscription = subscription_id
report.status = "FAIL"
report.status_extended = f"PHP version is set to '{framework}', which is below the minimum supported version '{minimum}', for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
if runtime_version_at_least(
parse_runtime_version(framework), parse_runtime_version(minimum)
):
report = Check_Report_Azure(metadata=self.metadata(), resource=app)
report.subscription = subscription_id
report.status = "FAIL"
report.status = "PASS"
report.status_extended = f"PHP version is set to '{framework}', which meets the minimum supported version '{minimum}', for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
php_latest_version = app_client.audit_config.get(
"php_latest_version", "8.5"
)
report.status_extended = f"PHP version is set to '{framework}', the latest version that you could use is the '{php_latest_version}' version, for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
if (
php_latest_version in framework
or getattr(app.configurations, "php_version", None)
== php_latest_version
):
report.status = "PASS"
report.status_extended = f"PHP version is set to '{php_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
findings.append(report)
findings.append(report)
return findings
@@ -1,7 +1,7 @@
{
"Provider": "azure",
"CheckID": "app_ensure_python_version_is_latest",
"CheckTitle": "App Service web app uses the latest supported Python version or 3.12 by default",
"CheckTitle": "App Service web app runs a supported Python version (3.12 or newer by default)",
"CheckType": [],
"ServiceName": "app",
"SubServiceName": "",
@@ -9,7 +9,7 @@
"Severity": "low",
"ResourceType": "microsoft.web/sites",
"ResourceGroup": "serverless",
"Description": "**Azure App Service web apps** using **Python** are assessed to confirm the runtime is the **latest supported version** (e.g., `3.12`). The evaluation reads the app's stack configuration to detect Python usage and compares the configured runtime against the defined latest baseline.",
"Description": "**Azure App Service web apps** using **Python** are assessed to confirm the runtime is at or above the configured **minimum supported version** (`python_latest_version`, `3.12` by default). The evaluation reads the app's stack configuration to detect Python usage and compares the configured runtime against that baseline.",
"Risk": "Outdated **Python runtimes** weaken security and reliability:\n- Compromise confidentiality via known interpreter/SSL flaws\n- Undermine integrity through RCE and package exploitation\n- Reduce availability when deprecated versions lose patches and break under load",
"RelatedUrl": "",
"AdditionalURLs": [
@@ -1,4 +1,8 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.lib.runtime.runtime_version import (
parse_runtime_version,
runtime_version_at_least,
)
from prowler.providers.azure.services.app.app_client import app_client
@@ -14,27 +18,33 @@ class app_ensure_python_version_is_latest(Check):
subscription_id, subscription_id
)
for app in apps.values():
framework = getattr(app.configurations, "linux_fx_version", "")
# The SDK can return None for linuxFxVersion, which getattr preserves
linux_framework = (
getattr(app.configurations, "linux_fx_version", "") or ""
)
windows_version = getattr(app.configurations, "python_version", "")
if "python" in framework.lower() or getattr(
app.configurations, "python_version", ""
if "python" not in linux_framework.lower() and not windows_version:
continue
framework = (
linux_framework
if "python" in linux_framework.lower()
else windows_version
)
minimum = app_client.audit_config.get("python_latest_version") or "3.12"
report = Check_Report_Azure(metadata=self.metadata(), resource=app)
report.subscription = subscription_id
report.status = "FAIL"
report.status_extended = f"Python version is set to '{framework}', which is below the minimum supported version '{minimum}', for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
if runtime_version_at_least(
parse_runtime_version(framework), parse_runtime_version(minimum)
):
report = Check_Report_Azure(metadata=self.metadata(), resource=app)
report.subscription = subscription_id
report.status = "FAIL"
python_latest_version = app_client.audit_config.get(
"python_latest_version", "3.12"
)
report.status_extended = f"Python version is '{framework}', the latest version that you could use is the '{python_latest_version}' version, for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
report.status = "PASS"
report.status_extended = f"Python version is set to '{framework}', which meets the minimum supported version '{minimum}', for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
if (
python_latest_version in framework
or getattr(app.configurations, "python_version", None)
== python_latest_version
):
report.status = "PASS"
report.status_extended = f"Python version is set to '{python_latest_version}' for app '{app.name}' in subscription '{subscription_name} ({subscription_id})'."
findings.append(report)
findings.append(report)
return findings
Whitespace-only changes.
@@ -0,0 +1,4 @@
from prowler.providers.common.provider import Provider
from prowler.providers.gcp.services.appengine.appengine_service import AppEngine
appengine_client = AppEngine(Provider.get_global_provider())
@@ -0,0 +1,109 @@
from googleapiclient.errors import HttpError
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.providers.gcp.config import DEFAULT_RETRY_ATTEMPTS
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.gcp.lib.service.service import GCPService
class AppEngine(GCPService):
"""App Engine Admin API client exposing every service version per project."""
def __init__(self, provider: GcpProvider) -> None:
super().__init__("appengine", provider)
self.versions = []
self._get_versions()
def _get_versions(self) -> None:
for project_id in self.project_ids:
try:
app = (
self.client.apps()
.get(appsId=project_id)
.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
)
except HttpError as http_error:
# 404 means the API is enabled but no App Engine application exists
if http_error.resp.status == 404:
logger.warning(
f"{project_id} -- App Engine API is enabled but no application exists"
)
else:
logger.error(
f"{project_id} -- {http_error.__class__.__name__}[{http_error.__traceback__.tb_lineno}]: {http_error}"
)
continue
except Exception as error:
logger.error(
f"{project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
continue
location = app.get("locationId", "")
try:
services = self.client.apps().services()
services_request = services.list(appsId=project_id)
while services_request is not None:
services_response = services_request.execute(
num_retries=DEFAULT_RETRY_ATTEMPTS
)
for service in services_response.get("services", []):
service_id = service.get("id", "")
self._get_service_versions(project_id, location, service_id)
services_request = services.list_next(
previous_request=services_request,
previous_response=services_response,
)
except Exception as error:
logger.error(
f"{project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _get_service_versions(
self, project_id: str, location: str, service_id: str
) -> None:
try:
versions = self.client.apps().services().versions()
request = versions.list(appsId=project_id, servicesId=service_id)
while request is not None:
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
for version in response.get("versions", []):
version_id = version.get("id", "")
self.versions.append(
Version(
id=version.get(
"name",
f"apps/{project_id}/services/{service_id}/versions/{version_id}",
),
name=f"{service_id}/{version_id}",
version_id=version_id,
service=service_id,
project_id=project_id,
location=location,
runtime=version.get("runtime", ""),
env=version.get("env", "standard"),
serving_status=version.get("servingStatus", "SERVING"),
)
)
request = versions.list_next(
previous_request=request, previous_response=response
)
except Exception as error:
logger.error(
f"{project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class Version(BaseModel):
"""App Engine service version consumed by GCP checks."""
id: str
name: str
version_id: str
service: str
project_id: str
location: str
runtime: str
env: str = "standard"
serving_status: str = "SERVING"
@@ -0,0 +1,38 @@
{
"Provider": "gcp",
"CheckID": "appengine_version_php_runtime_supported",
"CheckTitle": "App Engine serving version runs a supported PHP runtime",
"CheckType": [],
"ServiceName": "appengine",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "appengine.googleapis.com/Version",
"Description": "**App Engine** serving versions that run **PHP** are evaluated to confirm the runtime is a supported branch (`appengine_php_min_version`, `8.2` by default). Standard environment runtimes such as `php82` are compared directly; flexible environment versions are reported for manual verification because their PHP binary lives in a customer-built image that is not patched in place.",
"Risk": "PHP runtimes older than the supported branches are vulnerable to **unauthenticated remote code execution** through crafted web requests and receive no fixes, so a serving version pinned to one of them stays exploitable. Flexible environment versions keep the vulnerable binary until they are redeployed on a fixed release.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://cloud.google.com/appengine/docs/standard/lifecycle/support-schedule",
"https://cloud.google.com/appengine/docs/standard/php-gen2/runtime",
"https://cloud.google.com/appengine/docs/flexible/php/runtime",
"https://www.php.net/supported-versions.php"
],
"Remediation": {
"Code": {
"CLI": "gcloud app deploy app.yaml --project <PROJECT_ID> # with runtime: php84 in app.yaml",
"NativeIaC": "",
"Other": "1. Edit the service's app.yaml and set runtime to a supported PHP branch (for example php84)\n2. For flexible environment services, rebuild the image on a fixed PHP release\n3. Run gcloud app deploy to create a new version\n4. Route traffic to the new version and stop or delete versions still on unsupported runtimes",
"Terraform": "```hcl\nresource \"google_app_engine_standard_app_version\" \"<example_resource_name>\" {\n version_id = \"v2\"\n service = \"default\"\n runtime = \"php84\" # Critical: supported PHP runtime to pass the check\n\n entrypoint {\n shell = \"serve index.php\"\n }\n\n deployment {\n zip {\n source_url = \"https://storage.googleapis.com/<example_resource_id>/app.zip\"\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Deploy every App Engine service on a **supported PHP runtime** and redeploy whenever a branch approaches end of support. Standard environment runtimes on supported branches receive security fixes automatically; flexible environment images must be rebuilt and redeployed, so prefer the standard environment for PHP workloads where possible.",
"Url": "https://hub.prowler.com/check/appengine_version_php_runtime_supported"
}
},
"Categories": [
"vulnerabilities"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Only versions with serving status SERVING and a PHP runtime are evaluated. Flexible environment versions are reported as MANUAL since the deployed PHP release is not visible through the Admin API; versions on the custom runtime are not evaluated at all because the Admin API does not expose which language they run."
}
@@ -0,0 +1,52 @@
import re
from prowler.lib.check.models import Check, Check_Report_GCP
from prowler.providers.gcp.services.appengine.appengine_client import appengine_client
# Standard environment runtimes look like "php82"; flexible ones are just "php"
PHP_RUNTIME_RE = re.compile(r"^php(?P<major>\d)(?P<minor>\d+)?$", re.IGNORECASE)
def _version_tuple(value: str) -> tuple[int, ...]:
return tuple(int(part) for part in value.split(".")) if value else ()
class appengine_version_php_runtime_supported(Check):
"""Check that serving App Engine versions run a supported PHP runtime."""
def execute(self) -> list[Check_Report_GCP]:
findings = []
# `or` covers an explicit null in the config file, which .get() would return as None
minimum = (
appengine_client.audit_config.get("appengine_php_min_version") or "8.2"
)
for version in appengine_client.versions:
runtime = version.runtime.lower()
if not runtime.startswith("php") or version.serving_status != "SERVING":
continue
report = Check_Report_GCP(
metadata=self.metadata(),
resource=version,
resource_id=version.id,
)
match = PHP_RUNTIME_RE.match(runtime)
if version.env.lower() in ("flex", "flexible") or not (
match and match["minor"]
):
report.status = "MANUAL"
report.status_extended = f"App Engine version {version.name} runs on the flexible environment with runtime '{version.runtime}', which is not patched in place; redeploy it on a fixed PHP release ({minimum} or newer) and verify the image."
else:
php_version = f"{match['major']}.{match['minor']}"
if _version_tuple(php_version) < _version_tuple(minimum):
report.status = "FAIL"
report.status_extended = f"App Engine version {version.name} runs PHP {php_version} ('{version.runtime}'), a runtime that no longer receives security fixes (minimum supported version is {minimum})."
else:
report.status = "PASS"
report.status_extended = f"App Engine version {version.name} runs PHP {php_version} ('{version.runtime}'), a supported runtime that receives security fixes automatically."
findings.append(report)
return findings
+13
View File
@@ -111,6 +111,19 @@ config_aws = {
"max_idle_disconnect_timeout_in_seconds": 600,
"max_disconnect_timeout_in_seconds": 300,
"max_session_duration_seconds": 36000,
"elasticbeanstalk_php_min_branch": "8.2",
"elasticbeanstalk_php_min_platform_version": "4.13.13",
"lightsail_php_blueprints": [
"wordpress",
"lamp",
"nginx",
"drupal",
"joomla",
"magento",
"prestashop",
"plesk",
"cpanel",
],
"obsolete_lambda_runtimes": [
"java8",
"go1.x",
+23
View File
@@ -93,6 +93,28 @@ aws:
# aws.appstream_fleet_maximum_session_duration
max_session_duration_seconds: 36000 # 10 Hours
# AWS Elastic Beanstalk Configuration
# aws.elasticbeanstalk_environment_php_platform_updated
# Oldest PHP branch still receiving security fixes
elasticbeanstalk_php_min_branch: "8.2"
# Oldest Amazon Linux 2023 PHP platform version bundling the fixed PHP release
elasticbeanstalk_php_min_platform_version: "4.13.13"
# AWS Lightsail Configuration
# aws.lightsail_instance_php_blueprint_updated
# Blueprint id prefixes that ship PHP and require manual runtime verification
lightsail_php_blueprints:
[
"wordpress",
"lamp",
"nginx",
"drupal",
"joomla",
"magento",
"prestashop",
"plesk",
"cpanel",
]
# AWS Lambda Configuration
# aws.awslambda_function_using_supported_runtimes
obsolete_lambda_runtimes:
@@ -415,6 +437,7 @@ azure:
defender_attack_path_minimal_risk_level: "High"
# Azure App Service
# Minimum runtime versions; any newer version passes
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.2"
# azure.app_ensure_python_version_is_latest
@@ -0,0 +1,136 @@
from unittest import mock
import pytest
from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_service import (
Environment,
)
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_EU_WEST_1,
set_mocked_aws_provider,
)
CHECK_PATH = "prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_php_platform_updated.elasticbeanstalk_environment_php_platform_updated"
ENV_ARN = f"arn:aws:elasticbeanstalk:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:environment/test-app/test-env"
def _environment(solution_stack_name="", platform_arn=""):
return Environment(
id="e-abc123",
name="test-env",
arn=ENV_ARN,
region=AWS_REGION_EU_WEST_1,
application_name="test-app",
solution_stack_name=solution_stack_name,
platform_arn=platform_arn,
)
def _run_check(environments, audit_config=None):
elasticbeanstalk_client = mock.MagicMock()
elasticbeanstalk_client.environments = environments
elasticbeanstalk_client.audit_config = audit_config or {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]),
),
mock.patch(
f"{CHECK_PATH}.elasticbeanstalk_client", new=elasticbeanstalk_client
),
):
from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_environment_php_platform_updated.elasticbeanstalk_environment_php_platform_updated import (
elasticbeanstalk_environment_php_platform_updated,
)
return elasticbeanstalk_environment_php_platform_updated().execute()
class Test_elasticbeanstalk_environment_php_platform_updated:
def test_no_environments(self):
assert len(_run_check({})) == 0
def test_non_php_environment_is_skipped(self):
environments = {
ENV_ARN: _environment("64bit Amazon Linux 2023 v4.7.3 running Python 3.12")
}
assert len(_run_check(environments)) == 0
def test_environment_without_stack_is_skipped(self):
assert len(_run_check({ENV_ARN: _environment()})) == 0
@pytest.mark.parametrize(
"solution_stack_name,expected_status,expected_message",
[
(
"64bit Amazon Linux 2023 v4.13.13 running PHP 8.4",
"PASS",
"Elastic Beanstalk environment test-env runs PHP 8.4 on platform version 4.13.13, which includes the fixed PHP release.",
),
(
"64bit Amazon Linux 2023 v4.14.0 running PHP 8.2",
"PASS",
"Elastic Beanstalk environment test-env runs PHP 8.2 on platform version 4.14.0, which includes the fixed PHP release.",
),
(
"64bit Amazon Linux 2023 v4.13.12 running PHP 8.5",
"FAIL",
"Elastic Beanstalk environment test-env runs PHP 8.5 on platform version 4.13.12, below the minimum fixed platform version 4.13.13.",
),
(
"64bit Amazon Linux 2023 v4.9.2 running PHP 8.3",
"FAIL",
"Elastic Beanstalk environment test-env runs PHP 8.3 on platform version 4.9.2, below the minimum fixed platform version 4.13.13.",
),
(
"64bit Amazon Linux 2023 v4.13.13 running PHP 8.1",
"FAIL",
"Elastic Beanstalk environment test-env runs PHP 8.1, a branch that no longer receives security fixes (minimum supported branch is 8.2).",
),
(
"64bit Amazon Linux 2 v3.9.5 running PHP 8.2",
"FAIL",
"Elastic Beanstalk environment test-env runs PHP 8.2 on platform '64bit Amazon Linux 2 v3.9.5 running PHP 8.2', a platform family without fixed PHP releases; migrate it to a PHP 8.2 Amazon Linux 2023 platform version 4.13.13 or newer.",
),
],
)
def test_php_environment(
self, solution_stack_name, expected_status, expected_message
):
result = _run_check({ENV_ARN: _environment(solution_stack_name)})
assert len(result) == 1
assert result[0].status == expected_status
assert result[0].status_extended == expected_message
assert result[0].resource_id == "e-abc123"
assert result[0].resource_arn == ENV_ARN
assert result[0].region == AWS_REGION_EU_WEST_1
def test_platform_arn_fallback(self):
environments = {
ENV_ARN: _environment(
platform_arn=f"arn:aws:elasticbeanstalk:{AWS_REGION_EU_WEST_1}::platform/PHP 8.3 running on 64bit Amazon Linux 2023/4.13.13"
)
}
result = _run_check(environments)
assert len(result) == 1
assert result[0].status == "PASS"
def test_custom_minimums(self):
environments = {
ENV_ARN: _environment("64bit Amazon Linux 2023 v4.13.13 running PHP 8.3")
}
result = _run_check(
environments,
{
"elasticbeanstalk_php_min_branch": "8.4",
"elasticbeanstalk_php_min_platform_version": "4.13.13",
},
)
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "Elastic Beanstalk environment test-env runs PHP 8.3, a branch that no longer receives security fixes (minimum supported branch is 8.4)."
)
@@ -7,7 +7,11 @@ from moto import mock_aws
from prowler.providers.aws.services.elasticbeanstalk.elasticbeanstalk_service import (
ElasticBeanstalk,
)
from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider
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
@@ -120,6 +124,44 @@ class Test_ElasticBeanstalk_Service:
== "test-app"
)
@mock_aws
def test_describe_environments_platform(self):
environment_arn = f"arn:aws:elasticbeanstalk:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:environment/test-app/test-env"
def mock_describe_environments(self, operation_name, kwarg):
if operation_name == "DescribeEnvironments":
return {
"Environments": [
{
"EnvironmentId": "e-abc123",
"EnvironmentArn": environment_arn,
"ApplicationName": "test-app",
"EnvironmentName": "test-env",
"SolutionStackName": "64bit Amazon Linux 2023 v4.13.13 running PHP 8.4",
"PlatformArn": f"arn:aws:elasticbeanstalk:{AWS_REGION_EU_WEST_1}::platform/PHP 8.4 running on 64bit Amazon Linux 2023/4.13.13",
}
]
}
return make_api_call(self, operation_name, kwarg)
with patch(
"botocore.client.BaseClient._make_api_call",
new=mock_describe_environments,
):
elasticbeanstalk = ElasticBeanstalk(
set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
)
environment = elasticbeanstalk.environments[environment_arn]
assert (
environment.solution_stack_name
== "64bit Amazon Linux 2023 v4.13.13 running PHP 8.4"
)
assert (
environment.platform_arn
== f"arn:aws:elasticbeanstalk:{AWS_REGION_EU_WEST_1}::platform/PHP 8.4 running on 64bit Amazon Linux 2023/4.13.13"
)
# Test _describe_configuration_settings
@mock_aws
def test_describe_configuration_settings(self):
@@ -0,0 +1,89 @@
from unittest.mock import MagicMock, patch
import pytest
from prowler.providers.aws.services.lightsail.lightsail_service import Instance
from tests.providers.aws.utils import (
AWS_REGION_US_EAST_1,
AWS_REGION_US_EAST_1_AZA,
BASE_LIGHTSAIL_ARN,
set_mocked_aws_provider,
)
CHECK_PATH = "prowler.providers.aws.services.lightsail.lightsail_instance_php_blueprint_updated.lightsail_instance_php_blueprint_updated"
INSTANCE_ARN = f"{BASE_LIGHTSAIL_ARN}:Instance/test-instance"
def _instance(blueprint_id, blueprint_name=""):
return Instance(
name="test-instance",
id="1234/5678",
arn=INSTANCE_ARN,
tags=[],
region=AWS_REGION_US_EAST_1,
availability_zone=AWS_REGION_US_EAST_1_AZA,
static_ip=False,
public_ip="",
private_ip="172.16.2.3",
ipv6_addresses=[],
ip_address_type="ipv4",
ports=[],
auto_snapshot=False,
blueprint_id=blueprint_id,
blueprint_name=blueprint_name,
)
def _run_check(instances, audit_config=None):
lightsail_client = MagicMock
lightsail_client.instances = instances
lightsail_client.audit_config = audit_config or {}
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
),
patch(
"prowler.providers.aws.services.lightsail.lightsail_service.Lightsail",
new=lightsail_client,
),
patch(f"{CHECK_PATH}.lightsail_client", new=lightsail_client),
):
from prowler.providers.aws.services.lightsail.lightsail_instance_php_blueprint_updated.lightsail_instance_php_blueprint_updated import (
lightsail_instance_php_blueprint_updated,
)
return lightsail_instance_php_blueprint_updated().execute()
class Test_lightsail_instance_php_blueprint_updated:
def test_no_instances(self):
assert len(_run_check({})) == 0
@pytest.mark.parametrize("blueprint_id", ["ubuntu_22_04", "nodejs", "mean", ""])
def test_non_php_blueprint_is_skipped(self, blueprint_id):
assert len(_run_check({INSTANCE_ARN: _instance(blueprint_id)})) == 0
@pytest.mark.parametrize(
"blueprint_id",
["wordpress", "wordpress_multisite", "lamp_8_bitnami", "nginx", "drupal"],
)
def test_php_blueprint_is_manual(self, blueprint_id):
result = _run_check({INSTANCE_ARN: _instance(blueprint_id, "WordPress")})
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"Lightsail instance test-instance was created from the PHP blueprint '{blueprint_id}'; the PHP runtime cannot be inspected through the API, verify over SSH that it runs a fixed release (8.2.35, 8.3.36, 8.4.27, 8.5.12 or newer); Bitnami-packaged blueprints bundle a PHP copy that apt never updates, so migrate them to the Lightsail-packaged blueprint or replace the bundled PHP with a package-managed one."
)
assert result[0].resource_id == "1234/5678"
assert result[0].resource_arn == INSTANCE_ARN
assert result[0].region == AWS_REGION_US_EAST_1
def test_custom_blueprint_list(self):
instances = {INSTANCE_ARN: _instance("custom_php_app")}
assert len(_run_check(instances)) == 0
result = _run_check(instances, {"lightsail_php_blueprints": ["custom_php"]})
assert len(result) == 1
assert result[0].status == "MANUAL"
@@ -202,6 +202,14 @@ class TestLightsailService:
== f"{BASE_LIGHTSAIL_ARN}:Instance/test-id"
)
assert lightsail.instances[f"{BASE_LIGHTSAIL_ARN}:Instance/test-id"].tags == []
assert (
lightsail.instances[f"{BASE_LIGHTSAIL_ARN}:Instance/test-id"].blueprint_id
== "wordpress"
)
assert (
lightsail.instances[f"{BASE_LIGHTSAIL_ARN}:Instance/test-id"].blueprint_name
== "WordPress"
)
assert (
lightsail.instances[f"{BASE_LIGHTSAIL_ARN}:Instance/test-id"].region
== AWS_REGION_US_EAST_1
@@ -207,7 +207,24 @@ class Test_WAFv2_Service:
"CloudWatchMetricsEnabled": True,
"MetricName": "web-acl-test-metric",
},
}
},
{
"Name": "AWS-AWSManagedRulesPHPRuleSet",
"Priority": 2,
"Statement": {
"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesPHPRuleSet",
"Version": "Version_2.3",
}
},
"OverrideAction": {"None": {}},
"VisibilityConfig": {
"SampledRequestsEnabled": True,
"CloudWatchMetricsEnabled": True,
"MetricName": "php-rule-set",
},
},
],
VisibilityConfig={
"SampledRequestsEnabled": False,
@@ -221,6 +238,11 @@ class Test_WAFv2_Service:
aws = set_mocked_aws_provider([AWS_REGION_EU_WEST_1])
wafv2 = WAFv2(aws)
assert len(wafv2.web_acls) == 1
assert len(wafv2.web_acls[waf_arn].rules) == 1
assert len(wafv2.web_acls[waf_arn].rules) == 2
assert wafv2.web_acls[waf_arn].rules[0].name == "rule-on"
assert wafv2.web_acls[waf_arn].rules[0].cloudwatch_metrics_enabled
assert wafv2.web_acls[waf_arn].rules[0].managed_rule_group is None
managed = wafv2.web_acls[waf_arn].rules[1].managed_rule_group
assert managed.vendor_name == "AWS"
assert managed.name == "AWSManagedRulesPHPRuleSet"
assert managed.version == "Version_2.3"
@@ -0,0 +1,89 @@
from unittest import mock
from prowler.providers.aws.services.wafv2.wafv2_service import (
ManagedRuleGroup,
Rule,
WebAclv2,
)
from tests.providers.aws.utils import (
AWS_ACCOUNT_NUMBER,
AWS_REGION_US_EAST_1,
set_mocked_aws_provider,
)
CHECK_PATH = "prowler.providers.aws.services.wafv2.wafv2_webacl_managed_rule_groups_enabled.wafv2_webacl_managed_rule_groups_enabled"
WEB_ACL_ARN = f"arn:aws:wafv2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:regional/webacl/test-acl/abc"
def _web_acl(rules):
return WebAclv2(
arn=WEB_ACL_ARN,
name="test-acl",
id="abc",
albs=[],
user_pools=[],
region=AWS_REGION_US_EAST_1,
tags=[],
rules=rules,
)
def _run_check(web_acls):
wafv2_client = mock.MagicMock()
wafv2_client.web_acls = web_acls
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]),
),
mock.patch(f"{CHECK_PATH}.wafv2_client", new=wafv2_client),
):
from prowler.providers.aws.services.wafv2.wafv2_webacl_managed_rule_groups_enabled.wafv2_webacl_managed_rule_groups_enabled import (
wafv2_webacl_managed_rule_groups_enabled,
)
return wafv2_webacl_managed_rule_groups_enabled().execute()
class Test_wafv2_webacl_managed_rule_groups_enabled:
def test_no_web_acls(self):
assert len(_run_check({})) == 0
def test_no_managed_rule_groups(self):
result = _run_check({WEB_ACL_ARN: _web_acl([Rule(name="custom-rule")])})
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== "AWS WAFv2 Web ACL test-acl does not use any managed rule group."
)
assert result[0].resource_id == "abc"
assert result[0].resource_arn == WEB_ACL_ARN
assert result[0].region == AWS_REGION_US_EAST_1
def test_managed_rule_groups(self):
rules = [
Rule(name="custom-rule"),
Rule(
name="AWS-AWSManagedRulesCommonRuleSet",
managed_rule_group=ManagedRuleGroup(
vendor_name="AWS", name="AWSManagedRulesCommonRuleSet"
),
),
Rule(
name="AWS-AWSManagedRulesPHPRuleSet",
managed_rule_group=ManagedRuleGroup(
vendor_name="AWS",
name="AWSManagedRulesPHPRuleSet",
version="Version_2.3",
),
),
]
result = _run_check({WEB_ACL_ARN: _web_acl(rules)})
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "AWS WAFv2 Web ACL test-acl uses managed rule groups: AWS/AWSManagedRulesCommonRuleSet, AWS/AWSManagedRulesPHPRuleSet."
)
@@ -0,0 +1,54 @@
import pytest
from prowler.providers.azure.lib.runtime.runtime_version import (
parse_java_version,
parse_runtime_version,
runtime_version_at_least,
)
class Test_runtime_version_helpers:
@pytest.mark.parametrize(
"value,expected",
[
("PHP|8.3", (8, 3)),
("php|8.0", (8, 0)),
("PYTHON|3.12", (3, 12)),
("8.2.35", (8, 2, 35)),
("", ()),
(None, ()),
("PHP|latest", ()),
],
)
def test_parse_runtime_version(self, value, expected):
assert parse_runtime_version(value) == expected
@pytest.mark.parametrize(
"value,expected",
[
("JAVA|17-java17", (17,)),
("TOMCAT|10.0-java11", (11,)),
("JBOSSEAP|7-java8", (8,)),
("JAVA|8-jre8", (8,)),
("java1.8", (8,)),
("17", (17,)),
("", ()),
],
)
def test_parse_java_version(self, value, expected):
assert parse_java_version(value) == expected
@pytest.mark.parametrize(
"current,minimum,expected",
[
((8, 3), (8, 2), True),
((8, 2), (8, 2), True),
((8, 1), (8, 2), False),
((8, 2), (8, 2, 35), True),
((9,), (8, 2), True),
((), (8, 2), False),
((8, 2), (), False),
],
)
def test_runtime_version_at_least(self, current, minimum, expected):
assert runtime_version_at_least(current, minimum) is expected
@@ -1,6 +1,8 @@
from unittest import mock
from uuid import uuid4
import pytest
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
@@ -8,333 +10,165 @@ from tests.providers.azure.azure_fixtures import (
set_mocked_azure_provider,
)
CHECK_PATH = "prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest"
def _run_check(app_client):
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(f"{CHECK_PATH}.app_client", new=app_client),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
return app_ensure_java_version_is_latest().execute()
def _app_client(configurations, audit_config=None):
# app_service imports monitor_client, which needs the provider patched at import time
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
):
from prowler.providers.azure.services.app.app_service import WebApp
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = audit_config or {}
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=configurations,
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
return app_client, resource_id
class Test_app_ensure_java_version_is_latest:
def test_app_no_subscriptions(self):
app_client = mock.MagicMock
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.apps = {}
assert len(_run_check(app_client)) == 0
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 0
def test_app_subscriptions_empty(self):
app_client = mock.MagicMock
def test_app_subscription_empty(self):
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.apps = {AZURE_SUBSCRIPTION_ID: {}}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 0
assert len(_run_check(app_client)) == 0
def test_app_configurations_none(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client, _ = _app_client(None)
assert len(_run_check(app_client)) == 0
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
def test_app_linux_fx_version_none_is_skipped(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version=None, java_version=None)
)
assert len(_run_check(app_client)) == 0
def test_app_linux_fx_version_none_uses_windows_version(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version=None, java_version="17")
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == "PASS"
def test_app_other_runtime_is_skipped(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version="php|8.0", java_version=None)
)
assert len(_run_check(app_client)) == 0
@pytest.mark.parametrize(
"configurations,minimum,expected_status,expected_message",
[
(
{"linux_fx_version": "Tomcat|9.0-java11", "java_version": None},
"17",
"FAIL",
"Java version is set to 'Tomcat|9.0-java11', which is below the minimum supported version 'java 17',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "Tomcat|9.0-java17", "java_version": None},
"17",
"PASS",
"Java version is set to 'Tomcat|9.0-java17', which meets the minimum supported version 'java 17',",
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=None,
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 0
def test_app_linux_java_version_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"java_latest_version": "17"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
(
{"linux_fx_version": "JAVA|21-java21", "java_version": None},
"17",
"PASS",
"Java version is set to 'JAVA|21-java21', which meets the minimum supported version 'java 17',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "JAVA|8-jre8", "java_version": None},
"17",
"FAIL",
"Java version is set to 'JAVA|8-jre8', which is below the minimum supported version 'java 17',",
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(
linux_fx_version="Tomcat|9.0-java17", java_version=None
),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Java version is set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_linux_java_version_not_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"java_latest_version": "17"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
(
{"linux_fx_version": "JBOSSEAP|7-java8", "java_version": None},
"11",
"FAIL",
"Java version is set to 'JBOSSEAP|7-java8', which is below the minimum supported version 'java 11',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "", "java_version": "11"},
"17",
"FAIL",
"Java version is set to 'java11', which is below the minimum supported version 'java 17',",
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(
linux_fx_version="Tomcat|9.0-java11", java_version=None
),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Java version is set to 'Tomcat|9.0-java11', but should be set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_windows_java_version_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"java_latest_version": "17"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
(
{"linux_fx_version": "", "java_version": "17"},
"17",
"PASS",
"Java version is set to 'java17', which meets the minimum supported version 'java 17',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "", "java_version": "1.8"},
"17",
"FAIL",
"Java version is set to 'java1.8', which is below the minimum supported version 'java 17',",
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
],
)
def test_app_java_version(
self, configurations, minimum, expected_status, expected_message
):
app_client, resource_id = _app_client(
mock.MagicMock(**configurations), {"java_latest_version": minimum}
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == expected_status
assert (
result[0].status_extended
== f"{expected_message} for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(
linux_fx_version="", java_version="17"
),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Java version is set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_windows_java_version_not_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"java_latest_version": "17"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(
linux_fx_version="", java_version="11"
),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Java version is set to 'java11', but should be set to 'java 17' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_linux_php_version_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"java_latest_version": "17"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_java_version_is_latest.app_ensure_java_version_is_latest import (
app_ensure_java_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(
linux_fx_version="php|8.0", java_version=None
),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_java_version_is_latest()
result = check.execute()
assert len(result) == 0
def test_app_default_minimum(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version="JAVA|21-java21", java_version=None)
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == "PASS"
@@ -1,6 +1,8 @@
from unittest import mock
from uuid import uuid4
import pytest
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
@@ -8,186 +10,177 @@ from tests.providers.azure.azure_fixtures import (
set_mocked_azure_provider,
)
CHECK_PATH = "prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest"
def _run_check(app_client):
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(f"{CHECK_PATH}.app_client", new=app_client),
):
from prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest import (
app_ensure_php_version_is_latest,
)
return app_ensure_php_version_is_latest().execute()
def _app_client(configurations, audit_config=None):
# app_service imports monitor_client, which needs the provider patched at import time
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
):
from prowler.providers.azure.services.app.app_service import WebApp
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = audit_config or {}
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=configurations,
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
return app_client, resource_id
class Test_app_ensure_php_version_is_latest:
def test_app_no_subscriptions(self):
app_client = mock.MagicMock
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.apps = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest import (
app_ensure_php_version_is_latest,
)
check = app_ensure_php_version_is_latest()
result = check.execute()
assert len(result) == 0
assert len(_run_check(app_client)) == 0
def test_app_subscription_empty(self):
app_client = mock.MagicMock
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.apps = {AZURE_SUBSCRIPTION_ID: {}}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest import (
app_ensure_php_version_is_latest,
)
check = app_ensure_php_version_is_latest()
result = check.execute()
assert len(result) == 0
assert len(_run_check(app_client)) == 0
def test_app_configurations_none(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client, _ = _app_client(None)
assert len(_run_check(app_client)) == 0
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
def test_app_linux_fx_version_none_is_skipped(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version=None, php_version="")
)
assert len(_run_check(app_client)) == 0
def test_app_linux_fx_version_none_uses_windows_version(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version=None, php_version="8.5")
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == "PASS"
def test_app_other_runtime_is_skipped(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version="PYTHON|3.12", php_version="")
)
assert len(_run_check(app_client)) == 0
@pytest.mark.parametrize(
"configurations,minimum,expected_status,expected_message",
[
(
{"linux_fx_version": "php|8.0", "php_version": ""},
"8.2",
"FAIL",
"PHP version is set to 'php|8.0', which is below the minimum supported version '8.2',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "PHP|8.1", "php_version": ""},
"8.2",
"FAIL",
"PHP version is set to 'PHP|8.1', which is below the minimum supported version '8.2',",
),
):
from prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest import (
app_ensure_php_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=None,
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_php_version_is_latest()
result = check.execute()
assert len(result) == 0
def test_app_php_version_not_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"php_latest_version": "8.2"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
(
{"linux_fx_version": "PHP|8.2", "php_version": ""},
"8.2",
"PASS",
"PHP version is set to 'PHP|8.2', which meets the minimum supported version '8.2',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "PHP|8.3", "php_version": ""},
"8.2",
"PASS",
"PHP version is set to 'PHP|8.3', which meets the minimum supported version '8.2',",
),
):
from prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest import (
app_ensure_php_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(linux_fx_version="php|8.0"),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_php_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"PHP version is set to 'php|8.0', the latest version that you could use is the '8.2' version, for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_php_version_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"php_latest_version": "8.2"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
(
{"linux_fx_version": "PHP|8.5", "php_version": ""},
"8.2",
"PASS",
"PHP version is set to 'PHP|8.5', which meets the minimum supported version '8.2',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "PHP|8.3", "php_version": ""},
"8.4",
"FAIL",
"PHP version is set to 'PHP|8.3', which is below the minimum supported version '8.4',",
),
):
from prowler.providers.azure.services.app.app_ensure_php_version_is_latest.app_ensure_php_version_is_latest import (
app_ensure_php_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
(
{"linux_fx_version": "PHP|8.2", "php_version": ""},
"8.2.35",
"PASS",
"PHP version is set to 'PHP|8.2', which meets the minimum supported version '8.2.35',",
),
(
{"linux_fx_version": "", "php_version": "7.4"},
"8.2",
"FAIL",
"PHP version is set to '7.4', which is below the minimum supported version '8.2',",
),
(
{"linux_fx_version": "", "php_version": "8.2"},
"8.2",
"PASS",
"PHP version is set to '8.2', which meets the minimum supported version '8.2',",
),
(
{"linux_fx_version": "PHP|latest", "php_version": ""},
"8.2",
"FAIL",
"PHP version is set to 'PHP|latest', which is below the minimum supported version '8.2',",
),
],
)
def test_app_php_version(
self, configurations, minimum, expected_status, expected_message
):
app_client, resource_id = _app_client(
mock.MagicMock(**configurations), {"php_latest_version": minimum}
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == expected_status
assert (
result[0].status_extended
== f"{expected_message} for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(linux_fx_version="php|8.2"),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_php_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"PHP version is set to '8.2' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_default_minimum(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version="PHP|8.5", php_version="")
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == "PASS"
@@ -1,6 +1,8 @@
from unittest import mock
from uuid import uuid4
import pytest
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
@@ -8,185 +10,153 @@ from tests.providers.azure.azure_fixtures import (
set_mocked_azure_provider,
)
CHECK_PATH = "prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest"
def _run_check(app_client):
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(f"{CHECK_PATH}.app_client", new=app_client),
):
from prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest import (
app_ensure_python_version_is_latest,
)
return app_ensure_python_version_is_latest().execute()
def _app_client(configurations, audit_config=None):
# app_service imports monitor_client, which needs the provider patched at import time
with mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
):
from prowler.providers.azure.services.app.app_service import WebApp
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = audit_config or {}
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=configurations,
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
return app_client, resource_id
class Test_app_ensure_python_version_is_latest:
def test_app_no_subscriptions(self):
app_client = mock.MagicMock
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.apps = {}
assert len(_run_check(app_client)) == 0
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest import (
app_ensure_python_version_is_latest,
)
check = app_ensure_python_version_is_latest()
result = check.execute()
assert len(result) == 0
def test_app_subscriptions_empty(self):
app_client = mock.MagicMock
def test_app_subscription_empty(self):
app_client = mock.MagicMock()
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.apps = {AZURE_SUBSCRIPTION_ID: {}}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest.app_client",
new=app_client,
),
):
from prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest import (
app_ensure_python_version_is_latest,
)
check = app_ensure_python_version_is_latest()
result = check.execute()
assert len(result) == 0
assert len(_run_check(app_client)) == 0
def test_app_configurations_none(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
app_client, _ = _app_client(None)
assert len(_run_check(app_client)) == 0
def test_app_linux_fx_version_none_is_skipped(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version=None, python_version="")
)
assert len(_run_check(app_client)) == 0
def test_app_linux_fx_version_none_uses_windows_version(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version=None, python_version="3.12")
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == "PASS"
def test_app_other_runtime_is_skipped(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version="PHP|8.2", python_version="")
)
assert len(_run_check(app_client)) == 0
@pytest.mark.parametrize(
"configurations,minimum,expected_status,expected_message",
[
(
{"linux_fx_version": "python|3.10", "python_version": ""},
"3.12",
"FAIL",
"Python version is set to 'python|3.10', which is below the minimum supported version '3.12',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "PYTHON|3.12", "python_version": ""},
"3.12",
"PASS",
"Python version is set to 'PYTHON|3.12', which meets the minimum supported version '3.12',",
),
):
from prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest import (
app_ensure_python_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=None,
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_python_version_is_latest()
result = check.execute()
assert len(result) == 0
def test_app_python_version_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"python_latest_version": "3.12"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
(
{"linux_fx_version": "PYTHON|3.13", "python_version": ""},
"3.12",
"PASS",
"Python version is set to 'PYTHON|3.13', which meets the minimum supported version '3.12',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "PYTHON|3.9", "python_version": ""},
"3.10",
"FAIL",
"Python version is set to 'PYTHON|3.9', which is below the minimum supported version '3.10',",
),
):
from prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest import (
app_ensure_python_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(linux_fx_version="python|3.12"),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_python_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Python version is set to '3.12' for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_python_version_not_latest(self):
resource_id = f"/subscriptions/{uuid4()}"
app_client = mock.MagicMock
app_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
app_client.audit_config = {"python_latest_version": "3.12"}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
(
{"linux_fx_version": "", "python_version": "3.4"},
"3.12",
"FAIL",
"Python version is set to '3.4', which is below the minimum supported version '3.12',",
),
mock.patch(
"prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest.app_client",
new=app_client,
(
{"linux_fx_version": "", "python_version": "3.12"},
"3.12",
"PASS",
"Python version is set to '3.12', which meets the minimum supported version '3.12',",
),
):
from prowler.providers.azure.services.app.app_ensure_python_version_is_latest.app_ensure_python_version_is_latest import (
app_ensure_python_version_is_latest,
)
from prowler.providers.azure.services.app.app_service import WebApp
],
)
def test_app_python_version(
self, configurations, minimum, expected_status, expected_message
):
app_client, resource_id = _app_client(
mock.MagicMock(**configurations), {"python_latest_version": minimum}
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == expected_status
assert (
result[0].status_extended
== f"{expected_message} for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
app_client.apps = {
AZURE_SUBSCRIPTION_ID: {
resource_id: WebApp(
resource_id=resource_id,
name="app_id-1",
auth_enabled=True,
configurations=mock.MagicMock(linux_fx_version="python|3.10"),
client_cert_mode="Ignore",
https_only=False,
identity=None,
location="West Europe",
)
}
}
check = app_ensure_python_version_is_latest()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Python version is 'python|3.10', the latest version that you could use is the '3.12' version, for app 'app_id-1' in subscription '{AZURE_SUBSCRIPTION_DISPLAY}'."
)
assert result[0].resource_id == resource_id
assert result[0].resource_name == "app_id-1"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].location == "West Europe"
def test_app_default_minimum(self):
app_client, _ = _app_client(
mock.MagicMock(linux_fx_version="PYTHON|3.13", python_version="")
)
result = _run_check(app_client)
assert len(result) == 1
assert result[0].status == "PASS"
+1
View File
@@ -93,6 +93,7 @@ class TestGCPProvider:
"max_unused_account_days": 180,
"storage_min_retention_days": 90,
"secretmanager_max_rotation_days": 90,
"appengine_php_min_version": "8.2",
"mig_min_zones": 2,
"max_snapshot_age_days": 90,
}
@@ -0,0 +1,90 @@
from unittest.mock import MagicMock, patch
from googleapiclient.errors import HttpError
from prowler.providers.gcp.services.appengine.appengine_service import AppEngine
from tests.providers.gcp.gcp_fixtures import (
GCP_PROJECT_ID,
GCP_US_CENTER1_LOCATION,
mock_is_api_active,
set_mocked_gcp_provider,
)
def _make_appengine_client(versions, app_error=None):
client = MagicMock()
apps = client.apps()
if app_error:
apps.get().execute.side_effect = app_error
else:
apps.get().execute.return_value = {
"id": GCP_PROJECT_ID,
"locationId": GCP_US_CENTER1_LOCATION,
}
apps.services().list().execute.return_value = {"services": [{"id": "default"}]}
apps.services().list_next.return_value = None
apps.services().versions().list().execute.return_value = {"versions": versions}
apps.services().versions().list_next.return_value = None
return client
def _patched_service(client):
return (
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
),
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
new=lambda *_, **__: client,
),
)
class TestAppEngineService:
def test_get_versions(self):
client = _make_appengine_client(
[
{
"name": f"apps/{GCP_PROJECT_ID}/services/default/versions/v1",
"id": "v1",
"runtime": "php82",
"env": "standard",
"servingStatus": "SERVING",
},
{
"id": "v2",
"runtime": "php",
"env": "flexible",
"servingStatus": "STOPPED",
},
]
)
api_active, generate_client = _patched_service(client)
with api_active, generate_client:
appengine = AppEngine(set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]))
assert len(appengine.versions) == 2
first = appengine.versions[0]
assert first.id == f"apps/{GCP_PROJECT_ID}/services/default/versions/v1"
assert first.name == "default/v1"
assert first.version_id == "v1"
assert first.service == "default"
assert first.project_id == GCP_PROJECT_ID
assert first.location == GCP_US_CENTER1_LOCATION
assert first.runtime == "php82"
assert first.env == "standard"
assert first.serving_status == "SERVING"
second = appengine.versions[1]
assert second.id == f"apps/{GCP_PROJECT_ID}/services/default/versions/v2"
assert second.env == "flexible"
assert second.serving_status == "STOPPED"
def test_project_without_application(self):
response = MagicMock(status=404)
client = _make_appengine_client([], app_error=HttpError(response, b"not found"))
api_active, generate_client = _patched_service(client)
with api_active, generate_client:
appengine = AppEngine(set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]))
assert appengine.versions == []
@@ -0,0 +1,115 @@
from unittest import mock
import pytest
from tests.providers.gcp.gcp_fixtures import (
GCP_PROJECT_ID,
GCP_US_CENTER1_LOCATION,
set_mocked_gcp_provider,
)
CHECK_PATH = "prowler.providers.gcp.services.appengine.appengine_version_php_runtime_supported.appengine_version_php_runtime_supported"
def _version(runtime, env="standard", serving_status="SERVING"):
from prowler.providers.gcp.services.appengine.appengine_service import Version
return Version(
id=f"apps/{GCP_PROJECT_ID}/services/default/versions/v1",
name="default/v1",
version_id="v1",
service="default",
project_id=GCP_PROJECT_ID,
location=GCP_US_CENTER1_LOCATION,
runtime=runtime,
env=env,
serving_status=serving_status,
)
def _run_check(versions, audit_config=None):
appengine_client = mock.MagicMock()
appengine_client.versions = versions
appengine_client.audit_config = audit_config or {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(f"{CHECK_PATH}.appengine_client", new=appengine_client),
):
from prowler.providers.gcp.services.appengine.appengine_version_php_runtime_supported.appengine_version_php_runtime_supported import (
appengine_version_php_runtime_supported,
)
return appengine_version_php_runtime_supported().execute()
class Test_appengine_version_php_runtime_supported:
def test_no_versions(self):
assert len(_run_check([])) == 0
@pytest.mark.parametrize(
"runtime,env,serving_status",
[
("python312", "standard", "SERVING"),
("nodejs20", "standard", "SERVING"),
("php82", "standard", "STOPPED"),
("custom", "flexible", "SERVING"),
],
)
def test_skipped_versions(self, runtime, env, serving_status):
assert len(_run_check([_version(runtime, env, serving_status)])) == 0
@pytest.mark.parametrize(
"runtime,expected_status,expected_message",
[
(
"php82",
"PASS",
"App Engine version default/v1 runs PHP 8.2 ('php82'), a supported runtime that receives security fixes automatically.",
),
(
"php85",
"PASS",
"App Engine version default/v1 runs PHP 8.5 ('php85'), a supported runtime that receives security fixes automatically.",
),
(
"php81",
"FAIL",
"App Engine version default/v1 runs PHP 8.1 ('php81'), a runtime that no longer receives security fixes (minimum supported version is 8.2).",
),
(
"php55",
"FAIL",
"App Engine version default/v1 runs PHP 5.5 ('php55'), a runtime that no longer receives security fixes (minimum supported version is 8.2).",
),
],
)
def test_standard_runtime(self, runtime, expected_status, expected_message):
result = _run_check([_version(runtime)])
assert len(result) == 1
assert result[0].status == expected_status
assert result[0].status_extended == expected_message
assert (
result[0].resource_id
== f"apps/{GCP_PROJECT_ID}/services/default/versions/v1"
)
assert result[0].resource_name == "default/v1"
assert result[0].project_id == GCP_PROJECT_ID
assert result[0].location == GCP_US_CENTER1_LOCATION
def test_flexible_runtime_is_manual(self):
result = _run_check([_version("php", env="flexible")])
assert len(result) == 1
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== "App Engine version default/v1 runs on the flexible environment with runtime 'php', which is not patched in place; redeploy it on a fixed PHP release (8.2 or newer) and verify the image."
)
def test_custom_minimum(self):
result = _run_check([_version("php83")], {"appengine_php_min_version": "8.4"})
assert len(result) == 1
assert result[0].status == "FAIL"