From 1a450aaa709388807c162a95fae62cf5e066d018 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Wed, 13 May 2026 08:04:13 +0200 Subject: [PATCH 1/7] feat(config): add SDK config's validator --- docs/developer-guide/configurable-checks.mdx | 67 ++++++ prowler/config/config.py | 11 +- prowler/config/schema/__init__.py | 0 prowler/config/schema/aws.py | 162 +++++++++++++ prowler/config/schema/azure.py | 34 +++ prowler/config/schema/base.py | 17 ++ prowler/config/schema/cloudflare.py | 10 + prowler/config/schema/gcp.py | 13 ++ prowler/config/schema/github.py | 9 + prowler/config/schema/kubernetes.py | 13 ++ prowler/config/schema/m365.py | 24 ++ prowler/config/schema/mongodbatlas.py | 9 + prowler/config/schema/registry.py | 28 +++ prowler/config/schema/validator.py | 61 +++++ prowler/config/schema/vercel.py | 15 ++ tests/config/schema/__init__.py | 0 tests/config/schema/aws_schema_test.py | 218 ++++++++++++++++++ .../config/schema/loader_integration_test.py | 123 ++++++++++ .../schema/other_providers_schema_test.py | 148 ++++++++++++ tests/config/schema/validator_test.py | 175 ++++++++++++++ 20 files changed, 1136 insertions(+), 1 deletion(-) create mode 100644 prowler/config/schema/__init__.py create mode 100644 prowler/config/schema/aws.py create mode 100644 prowler/config/schema/azure.py create mode 100644 prowler/config/schema/base.py create mode 100644 prowler/config/schema/cloudflare.py create mode 100644 prowler/config/schema/gcp.py create mode 100644 prowler/config/schema/github.py create mode 100644 prowler/config/schema/kubernetes.py create mode 100644 prowler/config/schema/m365.py create mode 100644 prowler/config/schema/mongodbatlas.py create mode 100644 prowler/config/schema/registry.py create mode 100644 prowler/config/schema/validator.py create mode 100644 prowler/config/schema/vercel.py create mode 100644 tests/config/schema/__init__.py create mode 100644 tests/config/schema/aws_schema_test.py create mode 100644 tests/config/schema/loader_integration_test.py create mode 100644 tests/config/schema/other_providers_schema_test.py create mode 100644 tests/config/schema/validator_test.py diff --git a/docs/developer-guide/configurable-checks.mdx b/docs/developer-guide/configurable-checks.mdx index 76b339601d2..fb0c6ffcd09 100644 --- a/docs/developer-guide/configurable-checks.mdx +++ b/docs/developer-guide/configurable-checks.mdx @@ -40,9 +40,76 @@ When adding a new configurable check to Prowler, update the following files: # aws.awslambda_function_vpc_multi_az lambda_min_azs: 2 ``` +- **Provider Schema:** Add the typed field to the provider's Pydantic schema in `prowler/config/schema/.py`. This is required: the loader validates user configs against these schemas and the shipped `config.yaml` must round-trip with zero warnings. See [Adding a parameter to the provider schema](#adding-a-parameter-to-the-provider-schema) below. - **Test Fixtures:** If tests depend on this configuration, add the variable to `tests/config/fixtures/config.yaml`. - **Documentation:** Document the new variable in the list of configurable checks in `docs/tutorials/configuration_file.md`. For a complete list of checks that already support configuration, see the [Configuration File Tutorial](/user-guide/cli/tutorials/configuration_file). +## Adding a parameter to the provider schema + +Every provider has a typed Pydantic schema in `prowler/config/schema/`. When a config is loaded, `validate_provider_config` checks each user-supplied key against the schema, logs a warning, and drops any field that fails validation. The consumer's `.get(key, default)` then falls back to the built-in default. + +This catches typos in a value (for example, `0.2` typed as `20`, or `"medium"` for an enum that expects `"MEDIUM"`). It does NOT catch typos in a key name: `disalowed_regions` (one `l` missing) is treated as an unknown key and passes through untouched, because third-party check plugins legitimately rely on unknown keys being preserved. Reviewers should still check that any new key the YAML adds is named exactly the same as the field on the schema. + +### Where to add the field + +1. Open `prowler/config/schema/.py` (for example, `aws.py`). +2. Add a field on the provider's schema class. Always make it `Optional[...] = None` so the absence of the key is valid. +3. Apply the tightest type the value allows. Examples below. + +If you are introducing an entirely new provider rather than a new parameter, also add an entry mapping the provider name to its schema class in `prowler/config/schema/registry.py`. The loader uses that registry to find the schema for the provider it is loading. + +### Choosing the right type + +| Value kind | Field declaration | +|---|---| +| Boolean toggle | `Optional[bool] = None` | +| Strictly positive integer (days, counts) | `Optional[int] = Field(default=None, gt=0)` | +| Fraction in 0..1 (threshold) | `Optional[float] = Field(default=None, ge=0.0, le=1.0)` | +| Closed set of strings | `Optional[Literal["A", "B", "C"]] = None` | +| Free-form string | `Optional[str] = None` | +| List of strings or ints | `Optional[list[str]] = None` | + +Prefer `Literal[...]` over `str` whenever the value is one of a known set. Prefer `Field(gt=0)` over `int` whenever zero or negative would be nonsensical. The point of the schema is to catch real-world mistakes that previously passed silently. + +### Custom validators (only when needed) + +If the value has structural rules beyond type and range, add a `field_validator`. Examples already in `aws.py`: + +- `_validate_port_range` rejects ports outside `0..65535`. +- `_validate_account_ids` rejects anything that isn't a 12-digit AWS account ID. +- `_validate_trusted_ips` rejects entries that aren't a valid IP or CIDR. + +Raise `ValueError` from the validator. The framework converts the error into a warning and drops the offending key. + +### Example: adding a new parameter + +Say a new check needs `max_iam_role_session_hours`, a strictly positive integer that defaults to 12 in code. + +1. **Schema** (`prowler/config/schema/aws.py`): + ```python + # IAM + max_iam_role_session_hours: Optional[int] = Field(default=None, gt=0) + ``` +2. **Shipped config** (`prowler/config/config.yaml`): + ```yaml + # aws.iam_role_session_duration_within_limit + max_iam_role_session_hours: 12 + ``` +3. **Consumer** (the check): + ```python + max_hours = iam_client.audit_config.get("max_iam_role_session_hours", 12) + ``` +4. **Tests** in `tests/config/schema/aws_schema_test.py`: + - one test for a valid value that round-trips, + - one test for an invalid value (zero, negative, wrong type) that is dropped. + +### What the loader guarantees + +- **Unknown keys pass through.** Third-party check plugins can introduce arbitrary keys without schema edits; they will not be filtered. +- **Invalid values never crash the run.** They produce a single warning per field and the key is dropped. +- **Coerced values are normalized.** A YAML-quoted `"180"` for an `int` field arrives downstream as the integer `180`. +- **The shipped `config.yaml` must round-trip cleanly.** The integration test `test_shipped_default_config_loads_without_warnings` will fail if a key is added to the YAML without a matching schema field, so the two stay in sync. + This approach ensures that checks are easily configurable, making Prowler highly adaptable to different environments and requirements. diff --git a/prowler/config/config.py b/prowler/config/config.py index f318332e2fb..92deb103dff 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -221,6 +221,11 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict: Returns: dict: The configuration dictionary for the specified provider. """ + # Imported lazily to avoid an import cycle: schemas may eventually want to + # import from prowler.config.config (e.g. for shared constants). + from prowler.config.schema.registry import SCHEMAS + from prowler.config.schema.validator import validate_provider_config + try: with open(config_file_path, "r", encoding=encoding_format_utf_8) as f: config_file = yaml.safe_load(f) @@ -238,7 +243,11 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict: if provider in ["azure", "gcp", "kubernetes", "m365"]: config = {} - return config + return validate_provider_config( + provider=provider, + raw=config, + schema_cls=SCHEMAS.get(provider), + ) except FileNotFoundError as error: logger.error( diff --git a/prowler/config/schema/__init__.py b/prowler/config/schema/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/config/schema/aws.py b/prowler/config/schema/aws.py new file mode 100644 index 00000000000..d1eb74240fb --- /dev/null +++ b/prowler/config/schema/aws.py @@ -0,0 +1,162 @@ +from ipaddress import ip_network +from typing import Annotated, Literal, Optional + +from pydantic import AfterValidator, Field + +from prowler.config.schema.base import ProviderConfigBase + + +class _DetectSecretsPlugin(ProviderConfigBase): + """One entry inside ``detect_secrets_plugins``. + + Only ``name`` is required by the upstream library. ``limit`` is used by + the entropy detectors. Any other plugin-specific kwarg is preserved by + the ``extra="allow"`` policy inherited from ProviderConfigBase. + """ + + name: str + limit: Optional[float] = None + + +def _validate_port_range(v: Optional[list[int]]) -> Optional[list[int]]: + if v is None: + return v + for port in v: + if not 0 <= port <= 65535: + raise ValueError(f"port {port} is outside the valid range 0..65535") + return v + + +def _validate_account_ids(v: Optional[list[str]]) -> Optional[list[str]]: + if v is None: + return v + for account_id in v: + if not (account_id.isdigit() and len(account_id) == 12): + raise ValueError( + f"trusted_account_ids entry {account_id!r} is not a 12-digit AWS account id" + ) + return v + + +def _validate_trusted_ips(v: Optional[list[str]]) -> Optional[list[str]]: + if v is None: + return v + for entry in v: + try: + ip_network(entry, strict=False) + except ValueError as exc: + raise ValueError( + f"trusted_ips entry {entry!r} is not a valid IP or CIDR ({exc})" + ) from exc + return v + + +class AWSProviderConfig(ProviderConfigBase): + # IAM + mute_non_default_regions: Optional[bool] = None + disallowed_regions: Optional[list[str]] = None + max_unused_access_keys_days: Optional[int] = Field(default=None, gt=0) + max_console_access_days: Optional[int] = Field(default=None, gt=0) + max_unused_sagemaker_access_days: Optional[int] = Field(default=None, gt=0) + + # EC2 + shodan_api_key: Optional[str] = None + max_security_group_rules: Optional[int] = Field(default=None, gt=0) + max_ec2_instance_age_in_days: Optional[int] = Field(default=None, gt=0) + ec2_allowed_interface_types: Optional[list[str]] = None + ec2_allowed_instance_owners: Optional[list[str]] = None + ec2_high_risk_ports: Annotated[ + Optional[list[int]], AfterValidator(_validate_port_range) + ] = None + + # ECS + fargate_linux_latest_version: Optional[str] = None + fargate_windows_latest_version: Optional[str] = None + + # Cross-account trust + trusted_account_ids: Annotated[ + Optional[list[str]], AfterValidator(_validate_account_ids) + ] = None + trusted_ips: Annotated[ + Optional[list[str]], AfterValidator(_validate_trusted_ips) + ] = None + + # CloudWatch / CloudFormation + log_group_retention_days: Optional[int] = Field(default=None, gt=0) + recommended_cdk_bootstrap_version: Optional[int] = Field(default=None, gt=0) + + # AppStream + max_idle_disconnect_timeout_in_seconds: Optional[int] = Field(default=None, gt=0) + max_disconnect_timeout_in_seconds: Optional[int] = Field(default=None, gt=0) + max_session_duration_seconds: Optional[int] = Field(default=None, gt=0) + + # Lambda + obsolete_lambda_runtimes: Optional[list[str]] = None + lambda_min_azs: Optional[int] = Field(default=None, gt=0) + + # Organizations + organizations_enabled_regions: Optional[list[str]] = None + organizations_trusted_delegated_administrators: Optional[list[str]] = None + organizations_trusted_ids: Optional[list[str]] = None + + # ECR + ecr_repository_vulnerability_minimum_severity: Optional[ + Literal["CRITICAL", "HIGH", "MEDIUM", "LOW"] + ] = None + + # Trusted Advisor + verify_premium_support_plans: Optional[bool] = None + + # CloudTrail threat detection + threat_detection_privilege_escalation_threshold: Optional[float] = Field( + default=None, ge=0.0, le=1.0 + ) + threat_detection_privilege_escalation_minutes: Optional[int] = Field( + default=None, gt=0 + ) + threat_detection_privilege_escalation_actions: Optional[list[str]] = None + + threat_detection_enumeration_threshold: Optional[float] = Field( + default=None, ge=0.0, le=1.0 + ) + threat_detection_enumeration_minutes: Optional[int] = Field(default=None, gt=0) + threat_detection_enumeration_actions: Optional[list[str]] = None + + threat_detection_llm_jacking_threshold: Optional[float] = Field( + default=None, ge=0.0, le=1.0 + ) + threat_detection_llm_jacking_minutes: Optional[int] = Field(default=None, gt=0) + threat_detection_llm_jacking_actions: Optional[list[str]] = None + + # RDS + check_rds_instance_replicas: Optional[bool] = None + + # ACM + days_to_expire_threshold: Optional[int] = Field(default=None, gt=0) + insecure_key_algorithms: Optional[list[str]] = None + + # EKS + eks_required_log_types: Optional[list[str]] = None + eks_cluster_oldest_version_supported: Optional[str] = None + + # CodeBuild + excluded_sensitive_environment_variables: Optional[list[str]] = None + codebuild_github_allowed_organizations: Optional[list[str]] = None + + # ELB / ELBv2 + elb_min_azs: Optional[int] = Field(default=None, gt=0) + elbv2_min_azs: Optional[int] = Field(default=None, gt=0) + + # ElastiCache + minimum_snapshot_retention_period: Optional[int] = Field(default=None, gt=0) + + # Secrets + secrets_ignore_patterns: Optional[list[str]] = None + max_days_secret_unused: Optional[int] = Field(default=None, gt=0) + max_days_secret_unrotated: Optional[int] = Field(default=None, gt=0) + + # Kinesis + min_kinesis_stream_retention_hours: Optional[int] = Field(default=None, gt=0) + + # detect-secrets plugins + detect_secrets_plugins: Optional[list[_DetectSecretsPlugin]] = None diff --git a/prowler/config/schema/azure.py b/prowler/config/schema/azure.py new file mode 100644 index 00000000000..2b04ccb2e98 --- /dev/null +++ b/prowler/config/schema/azure.py @@ -0,0 +1,34 @@ +from typing import Literal, Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class AzureProviderConfig(ProviderConfigBase): + # Network + shodan_api_key: Optional[str] = None + + # Defender + defender_attack_path_minimal_risk_level: Optional[ + Literal["Low", "Medium", "High", "Critical"] + ] = None + + # App Service + php_latest_version: Optional[str] = None + python_latest_version: Optional[str] = None + java_latest_version: Optional[str] = None + + # SQL + recommended_minimal_tls_versions: Optional[list[str]] = None + + # Virtual Machines + desired_vm_sku_sizes: Optional[list[str]] = None + vm_backup_min_daily_retention_days: Optional[int] = Field(default=None, gt=0) + + # API Management threat detection + apim_threat_detection_llm_jacking_threshold: Optional[float] = Field( + default=None, ge=0.0, le=1.0 + ) + apim_threat_detection_llm_jacking_minutes: Optional[int] = Field(default=None, gt=0) + apim_threat_detection_llm_jacking_actions: Optional[list[str]] = None diff --git a/prowler/config/schema/base.py b/prowler/config/schema/base.py new file mode 100644 index 00000000000..cc473a4545d --- /dev/null +++ b/prowler/config/schema/base.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel, ConfigDict + + +class ProviderConfigBase(BaseModel): + """Base for every provider config schema. + + ``extra="allow"`` is REQUIRED for backwards compatibility: third-party + check plugins frequently introduce config keys we do not know about, + and pre-existing user configs may carry deprecated keys. Validation + must never reject these. + """ + + model_config = ConfigDict( + extra="allow", + str_strip_whitespace=True, + validate_assignment=False, + ) diff --git a/prowler/config/schema/cloudflare.py b/prowler/config/schema/cloudflare.py new file mode 100644 index 00000000000..0b612f047ff --- /dev/null +++ b/prowler/config/schema/cloudflare.py @@ -0,0 +1,10 @@ +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class CloudflareProviderConfig(ProviderConfigBase): + # 0 disables retries; negative values would loop or assert in the client. + max_retries: Optional[int] = Field(default=None, ge=0) diff --git a/prowler/config/schema/gcp.py b/prowler/config/schema/gcp.py new file mode 100644 index 00000000000..22b3c29ce3f --- /dev/null +++ b/prowler/config/schema/gcp.py @@ -0,0 +1,13 @@ +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class GCPProviderConfig(ProviderConfigBase): + shodan_api_key: Optional[str] = None + mig_min_zones: Optional[int] = Field(default=None, gt=0) + max_snapshot_age_days: Optional[int] = Field(default=None, gt=0) + max_unused_account_days: Optional[int] = Field(default=None, gt=0) + storage_min_retention_days: Optional[int] = Field(default=None, gt=0) diff --git a/prowler/config/schema/github.py b/prowler/config/schema/github.py new file mode 100644 index 00000000000..ce1bf2dbb88 --- /dev/null +++ b/prowler/config/schema/github.py @@ -0,0 +1,9 @@ +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class GitHubProviderConfig(ProviderConfigBase): + inactive_not_archived_days_threshold: Optional[int] = Field(default=None, gt=0) diff --git a/prowler/config/schema/kubernetes.py b/prowler/config/schema/kubernetes.py new file mode 100644 index 00000000000..01612ac5bf0 --- /dev/null +++ b/prowler/config/schema/kubernetes.py @@ -0,0 +1,13 @@ +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class KubernetesProviderConfig(ProviderConfigBase): + audit_log_maxbackup: Optional[int] = Field(default=None, gt=0) + audit_log_maxsize: Optional[int] = Field(default=None, gt=0) + audit_log_maxage: Optional[int] = Field(default=None, gt=0) + apiserver_strong_ciphers: Optional[list[str]] = None + kubelet_strong_ciphers: Optional[list[str]] = None diff --git a/prowler/config/schema/m365.py b/prowler/config/schema/m365.py new file mode 100644 index 00000000000..01013ac2ae0 --- /dev/null +++ b/prowler/config/schema/m365.py @@ -0,0 +1,24 @@ +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class M365ProviderConfig(ProviderConfigBase): + # Entra + sign_in_frequency: Optional[int] = Field(default=None, gt=0) + + # Teams + allowed_cloud_storage_services: Optional[list[str]] = None + + # Exchange + recommended_mailtips_large_audience_threshold: Optional[int] = Field( + default=None, gt=0 + ) + + # Defender malware policy + default_recommended_extensions: Optional[list[str]] = None + + # Mailbox auditing + audit_log_age: Optional[int] = Field(default=None, gt=0) diff --git a/prowler/config/schema/mongodbatlas.py b/prowler/config/schema/mongodbatlas.py new file mode 100644 index 00000000000..d9a210184c5 --- /dev/null +++ b/prowler/config/schema/mongodbatlas.py @@ -0,0 +1,9 @@ +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class MongoDBAtlasProviderConfig(ProviderConfigBase): + max_service_account_secret_validity_hours: Optional[int] = Field(default=None, gt=0) diff --git a/prowler/config/schema/registry.py b/prowler/config/schema/registry.py new file mode 100644 index 00000000000..fa31f1e5eb8 --- /dev/null +++ b/prowler/config/schema/registry.py @@ -0,0 +1,28 @@ +"""Mapping of provider name to its Pydantic schema class. + +Kept in its own module so the validator stays free of provider-schema imports +and callers pay the import cost only when they actually need the registry. +""" + +from prowler.config.schema.aws import AWSProviderConfig +from prowler.config.schema.azure import AzureProviderConfig +from prowler.config.schema.base import ProviderConfigBase +from prowler.config.schema.cloudflare import CloudflareProviderConfig +from prowler.config.schema.gcp import GCPProviderConfig +from prowler.config.schema.github import GitHubProviderConfig +from prowler.config.schema.kubernetes import KubernetesProviderConfig +from prowler.config.schema.m365 import M365ProviderConfig +from prowler.config.schema.mongodbatlas import MongoDBAtlasProviderConfig +from prowler.config.schema.vercel import VercelProviderConfig + +SCHEMAS: dict[str, type[ProviderConfigBase]] = { + "aws": AWSProviderConfig, + "azure": AzureProviderConfig, + "gcp": GCPProviderConfig, + "kubernetes": KubernetesProviderConfig, + "m365": M365ProviderConfig, + "github": GitHubProviderConfig, + "mongodbatlas": MongoDBAtlasProviderConfig, + "cloudflare": CloudflareProviderConfig, + "vercel": VercelProviderConfig, +} diff --git a/prowler/config/schema/validator.py b/prowler/config/schema/validator.py new file mode 100644 index 00000000000..c2acb642e99 --- /dev/null +++ b/prowler/config/schema/validator.py @@ -0,0 +1,61 @@ +from typing import Any + +from pydantic import ValidationError + +from prowler.config.schema.base import ProviderConfigBase +from prowler.lib.logger import logger + + +def validate_provider_config( + provider: str, + raw: Any, + schema_cls: type[ProviderConfigBase] | None, +) -> dict: + """Validate a provider's config dict against its Pydantic schema. + + Behavior is intentionally lenient to preserve backwards compatibility: + + - If ``raw`` is not a dict, return an empty dict (mirrors prior loader). + - If no schema is registered for ``provider``, return ``raw`` untouched. + - On validation errors, log one WARNING per offending field, DROP those + keys from the result, and continue. Consumers fall back to their own + hard-coded defaults via ``audit_config.get(key, default)``. + - Coerced values (e.g. ``"180"`` -> ``180``) replace the user's input + so that downstream checks never receive a wrongly-typed value. + """ + if not isinstance(raw, dict): + return {} + + if schema_cls is None: + return raw + + try: + model = schema_cls.model_validate(raw) + return model.model_dump(exclude_unset=True) + except ValidationError as exc: + bad_keys: set[str] = set() + for err in exc.errors(): + loc = err.get("loc") or () + if not loc: + continue + key = loc[0] + if not isinstance(key, str): + continue + bad_keys.add(key) + logger.warning( + f"prowler.config[{provider}.{key}] = {raw.get(key)!r} is invalid " + f"({err.get('msg', 'validation error')}); the value will be ignored " + f"and the built-in default will be used." + ) + + cleaned = {k: v for k, v in raw.items() if k not in bad_keys} + try: + model = schema_cls.model_validate(cleaned) + return model.model_dump(exclude_unset=True) + except ValidationError as exc2: + logger.error( + f"prowler.config[{provider}] could not be revalidated after dropping " + f"invalid keys ({bad_keys}); passing through the cleaned dict as-is. " + f"Underlying errors: {exc2.errors()}" + ) + return cleaned diff --git a/prowler/config/schema/vercel.py b/prowler/config/schema/vercel.py new file mode 100644 index 00000000000..8d0f672fd32 --- /dev/null +++ b/prowler/config/schema/vercel.py @@ -0,0 +1,15 @@ +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class VercelProviderConfig(ProviderConfigBase): + stable_branches: Optional[list[str]] = None + days_to_expire_threshold: Optional[int] = Field(default=None, gt=0) + stale_token_threshold_days: Optional[int] = Field(default=None, gt=0) + stale_invitation_threshold_days: Optional[int] = Field(default=None, gt=0) + max_owner_percentage: Optional[int] = Field(default=None, ge=0, le=100) + max_owners: Optional[int] = Field(default=None, gt=0) + secret_suffixes: Optional[list[str]] = None diff --git a/tests/config/schema/__init__.py b/tests/config/schema/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/config/schema/aws_schema_test.py b/tests/config/schema/aws_schema_test.py new file mode 100644 index 00000000000..989ebde0e2e --- /dev/null +++ b/tests/config/schema/aws_schema_test.py @@ -0,0 +1,218 @@ +"""AWS-specific schema coverage — the biggest provider, with the richest +constraint surface (CIDRs, account IDs, port ranges, enums, thresholds).""" + +import pytest + +from prowler.config.schema.aws import AWSProviderConfig +from prowler.config.schema.validator import validate_provider_config + + +def _validate(raw): + return validate_provider_config("aws", raw, AWSProviderConfig) + + +class Test_AWS_Threat_Detection_Thresholds: + """All threat detection thresholds are documented as fractions in 0..1. + The biggest risk of mistyping them is silently disabling the check.""" + + @pytest.mark.parametrize( + "key", + [ + "threat_detection_privilege_escalation_threshold", + "threat_detection_enumeration_threshold", + "threat_detection_llm_jacking_threshold", + ], + ) + def test_valid_boundary_values(self, key): + assert _validate({key: 0.0}) == {key: 0.0} + assert _validate({key: 1.0}) == {key: 1.0} + assert _validate({key: 0.5}) == {key: 0.5} + + @pytest.mark.parametrize( + "key", + [ + "threat_detection_privilege_escalation_threshold", + "threat_detection_enumeration_threshold", + "threat_detection_llm_jacking_threshold", + ], + ) + def test_invalid_values_are_dropped(self, key): + # 20 instead of 0.2 — would never trigger + assert _validate({key: 20}) == {} + # negative + assert _validate({key: -0.1}) == {} + # string + assert _validate({key: "high"}) == {} + + +class Test_AWS_Trusted_Account_Ids: + def test_valid_twelve_digit_ids(self): + ids = ["123456789012", "098765432109"] + assert _validate({"trusted_account_ids": ids}) == {"trusted_account_ids": ids} + + def test_empty_list_is_valid(self): + assert _validate({"trusted_account_ids": []}) == {"trusted_account_ids": []} + + def test_short_id_is_dropped(self): + assert _validate({"trusted_account_ids": ["12345"]}) == {} + + def test_non_numeric_id_is_dropped(self): + assert _validate({"trusted_account_ids": ["1234abcd5678"]}) == {} + + def test_id_with_dashes_is_dropped(self): + # Some users format account IDs as "1234-5678-9012" + assert _validate({"trusted_account_ids": ["1234-5678-9012"]}) == {} + + +class Test_AWS_Trusted_Ips: + def test_single_ipv4_address(self): + assert _validate({"trusted_ips": ["1.2.3.4"]}) == {"trusted_ips": ["1.2.3.4"]} + + def test_ipv4_cidr(self): + assert _validate({"trusted_ips": ["10.0.0.0/8"]}) == { + "trusted_ips": ["10.0.0.0/8"] + } + + def test_ipv6_address(self): + assert _validate({"trusted_ips": ["2001:db8::1"]}) == { + "trusted_ips": ["2001:db8::1"] + } + + def test_ipv6_cidr(self): + assert _validate({"trusted_ips": ["2001:db8::/32"]}) == { + "trusted_ips": ["2001:db8::/32"] + } + + def test_mixed_list(self): + ips = ["1.2.3.4", "10.0.0.0/8", "2001:db8::1"] + assert _validate({"trusted_ips": ips}) == {"trusted_ips": ips} + + def test_garbage_entry_is_dropped(self): + assert _validate({"trusted_ips": ["definitely-not-an-ip"]}) == {} + + def test_cidr_with_host_bits_is_accepted(self): + # We use strict=False so "10.0.0.5/8" is accepted. This matches the + # behaviour of most security tools and avoids surprising users who + # paste real-world allowlists with non-canonical CIDR notation. + assert _validate({"trusted_ips": ["10.0.0.5/8"]}) == { + "trusted_ips": ["10.0.0.5/8"] + } + + +class Test_AWS_Ports: + def test_valid_ports_in_range(self): + ports = [25, 80, 443, 65535, 0] + assert _validate({"ec2_high_risk_ports": ports}) == { + "ec2_high_risk_ports": ports + } + + def test_out_of_range_port_is_dropped(self): + assert _validate({"ec2_high_risk_ports": [70000]}) == {} + + def test_negative_port_is_dropped(self): + assert _validate({"ec2_high_risk_ports": [-1]}) == {} + + +class Test_AWS_Enums: + @pytest.mark.parametrize("level", ["CRITICAL", "HIGH", "MEDIUM", "LOW"]) + def test_valid_severity_levels(self, level): + assert _validate({"ecr_repository_vulnerability_minimum_severity": level}) == { + "ecr_repository_vulnerability_minimum_severity": level + } + + @pytest.mark.parametrize("level", ["critical", "Medium", "ANY", "", "X"]) + def test_invalid_severity_levels_are_dropped(self, level): + assert _validate({"ecr_repository_vulnerability_minimum_severity": level}) == {} + + +class Test_AWS_Detect_Secrets_Plugins: + def test_plugin_without_limit(self): + out = _validate({"detect_secrets_plugins": [{"name": "AWSKeyDetector"}]}) + assert out == {"detect_secrets_plugins": [{"name": "AWSKeyDetector"}]} + + def test_plugin_with_limit(self): + out = _validate( + { + "detect_secrets_plugins": [ + {"name": "Base64HighEntropyString", "limit": 6.0} + ] + } + ) + assert out == { + "detect_secrets_plugins": [ + {"name": "Base64HighEntropyString", "limit": 6.0} + ] + } + + def test_plugin_missing_name_drops_whole_field(self): + # ``name`` is required by the upstream library. + out = _validate({"detect_secrets_plugins": [{"limit": 6.0}]}) + assert out == {} + + def test_extra_plugin_kwargs_pass_through(self): + # Plugins can have arbitrary extra params (extra="allow" on the + # nested model). They must round-trip. + out = _validate( + { + "detect_secrets_plugins": [ + {"name": "Custom", "my_param": "abc", "other": 42} + ] + } + ) + assert out == { + "detect_secrets_plugins": [ + {"name": "Custom", "my_param": "abc", "other": 42} + ] + } + + +class Test_AWS_Booleans: + @pytest.mark.parametrize( + "key", + [ + "mute_non_default_regions", + "verify_premium_support_plans", + "check_rds_instance_replicas", + ], + ) + def test_true_and_false_round_trip(self, key): + assert _validate({key: True}) == {key: True} + assert _validate({key: False}) == {key: False} + + def test_yaml_style_boolean_coercion(self): + # YAML can produce Python str "true"/"yes" if the user quoted it. + # Pydantic v2 will refuse string booleans by default. Verify it is + # dropped, not silently treated as True (which would be dangerous + # for verify_premium_support_plans). + out = _validate({"verify_premium_support_plans": "yes"}) + # Pydantic actually DOES coerce "yes"/"no"/"true"/"false" in lax mode. + # We accept either outcome but require it to be a real bool. + if "verify_premium_support_plans" in out: + assert isinstance(out["verify_premium_support_plans"], bool) + + +class Test_AWS_Full_Default_Config_Round_Trips: + """Loading the real shipped defaults through the schema must produce + exactly the same dict. This is the regression sentinel for backwards + compatibility.""" + + def test_full_default_config_round_trip(self): + # Subset that mirrors the shipped config.yaml semantics. + raw = { + "mute_non_default_regions": False, + "disallowed_regions": ["me-south-1", "me-central-1"], + "max_unused_access_keys_days": 45, + "max_ec2_instance_age_in_days": 180, + "trusted_account_ids": [], + "trusted_ips": [], + "ecr_repository_vulnerability_minimum_severity": "MEDIUM", + "threat_detection_privilege_escalation_threshold": 0.2, + "threat_detection_enumeration_threshold": 0.3, + "threat_detection_llm_jacking_threshold": 0.4, + "ec2_high_risk_ports": [25, 110, 8088], + "detect_secrets_plugins": [ + {"name": "AWSKeyDetector"}, + {"name": "Base64HighEntropyString", "limit": 6.0}, + ], + } + assert _validate(raw) == raw diff --git a/tests/config/schema/loader_integration_test.py b/tests/config/schema/loader_integration_test.py new file mode 100644 index 00000000000..c3337f44478 --- /dev/null +++ b/tests/config/schema/loader_integration_test.py @@ -0,0 +1,123 @@ +"""End-to-end tests that exercise the real ``load_and_validate_config_file`` +through a temp YAML file. Anything that breaks here would break the actual +``prowler aws -c …`` code path.""" + +import logging +import os +import pathlib + +import pytest + +from prowler.config.config import load_and_validate_config_file + + +@pytest.fixture +def write_config(tmp_path): + def _write(content: str) -> str: + path = tmp_path / "config.yaml" + path.write_text(content) + return str(path) + + return _write + + +class Test_Loader_With_Schema_Integration: + def test_shipped_default_config_loads_without_warnings(self, caplog): + """The default ``prowler/config/config.yaml`` must round-trip every + provider WITHOUT emitting any schema warnings. If this fails, + someone added a key to the YAML without updating the schema.""" + repo_root = pathlib.Path(os.path.dirname(os.path.realpath(__file__))).parents[2] + shipped = repo_root / "prowler" / "config" / "config.yaml" + with caplog.at_level(logging.WARNING, logger="prowler"): + for provider in [ + "aws", + "azure", + "gcp", + "kubernetes", + "m365", + "github", + "mongodbatlas", + "cloudflare", + "vercel", + ]: + cfg = load_and_validate_config_file(provider, str(shipped)) + # Provider always exists in the shipped file → non-empty. + assert cfg, f"{provider} returned an empty config" + + offending = [ + r.getMessage() + for r in caplog.records + if "prowler.config[" in r.getMessage() + ] + assert not offending, ( + "Shipped config.yaml triggered schema warnings — schema or YAML out of sync:\n" + + "\n".join(offending) + ) + + def test_user_config_with_bad_threshold_falls_back(self, write_config, caplog): + path = write_config( + "aws:\n" + " threat_detection_privilege_escalation_threshold: 5.0\n" + " lambda_min_azs: 2\n" + ) + with caplog.at_level(logging.WARNING, logger="prowler"): + cfg = load_and_validate_config_file("aws", path) + assert cfg == {"lambda_min_azs": 2} + assert any( + "threat_detection_privilege_escalation_threshold" in r.getMessage() + for r in caplog.records + ) + + def test_old_format_config_still_works(self, write_config): + # Old format = flat keys, no provider header. + path = write_config( + "max_ec2_instance_age_in_days: 90\n" + "ecr_repository_vulnerability_minimum_severity: HIGH\n" + ) + cfg = load_and_validate_config_file("aws", path) + assert cfg == { + "max_ec2_instance_age_in_days": 90, + "ecr_repository_vulnerability_minimum_severity": "HIGH", + } + + def test_unknown_keys_pass_through_via_loader(self, write_config): + path = write_config( + "aws:\n" " third_party_plugin_setting: hello\n" " lambda_min_azs: 2\n" + ) + cfg = load_and_validate_config_file("aws", path) + assert cfg == { + "third_party_plugin_setting": "hello", + "lambda_min_azs": 2, + } + + def test_quoted_numeric_is_coerced_via_loader(self, write_config): + # YAML quotes the number: ``"180"`` arrives as a Python str. + # The schema must coerce it to int so downstream comparisons work. + path = write_config('aws:\n max_ec2_instance_age_in_days: "180"\n') + cfg = load_and_validate_config_file("aws", path) + assert cfg == {"max_ec2_instance_age_in_days": 180} + assert isinstance(cfg["max_ec2_instance_age_in_days"], int) + + def test_invalid_yaml_shape_list_as_string_drops_key(self, write_config, caplog): + path = write_config( + "aws:\n" + " disallowed_regions: me-south-1\n" # forgot list dashes + " lambda_min_azs: 2\n" + ) + with caplog.at_level(logging.WARNING, logger="prowler"): + cfg = load_and_validate_config_file("aws", path) + assert cfg == {"lambda_min_azs": 2} + assert any("disallowed_regions" in r.getMessage() for r in caplog.records) + + def test_other_providers_unaffected_by_aws_block(self, write_config): + path = write_config( + "aws:\n max_ec2_instance_age_in_days: 90\n" "gcp:\n mig_min_zones: 5\n" + ) + assert load_and_validate_config_file("aws", path) == { + "max_ec2_instance_age_in_days": 90 + } + assert load_and_validate_config_file("gcp", path) == {"mig_min_zones": 5} + + def test_missing_provider_block_returns_empty(self, write_config): + path = write_config("aws:\n max_ec2_instance_age_in_days: 90\n") + assert load_and_validate_config_file("azure", path) == {} diff --git a/tests/config/schema/other_providers_schema_test.py b/tests/config/schema/other_providers_schema_test.py new file mode 100644 index 00000000000..163be362043 --- /dev/null +++ b/tests/config/schema/other_providers_schema_test.py @@ -0,0 +1,148 @@ +"""Smaller-provider schema coverage. One happy path + one invalid path +per field is enough to lock in the contract; the validator behaviour +itself is covered exhaustively in validator_test.py.""" + +import pytest + +from prowler.config.schema.registry import SCHEMAS +from prowler.config.schema.validator import validate_provider_config + + +def _validate(provider, raw): + return validate_provider_config(provider, raw, SCHEMAS[provider]) + + +class Test_Azure_Schema: + @pytest.mark.parametrize("level", ["Low", "Medium", "High", "Critical"]) + def test_defender_risk_level_valid_values(self, level): + assert _validate( + "azure", {"defender_attack_path_minimal_risk_level": level} + ) == {"defender_attack_path_minimal_risk_level": level} + + def test_defender_risk_level_lowercase_dropped(self): + # Case matters: the matching check uses Title-case comparison. + assert ( + _validate("azure", {"defender_attack_path_minimal_risk_level": "high"}) + == {} + ) + + def test_apim_threshold_in_range(self): + out = _validate("azure", {"apim_threat_detection_llm_jacking_threshold": 0.1}) + assert out == {"apim_threat_detection_llm_jacking_threshold": 0.1} + + def test_apim_threshold_out_of_range(self): + out = _validate("azure", {"apim_threat_detection_llm_jacking_threshold": 1.5}) + assert out == {} + + def test_vm_backup_retention_must_be_positive(self): + assert _validate("azure", {"vm_backup_min_daily_retention_days": 7}) == { + "vm_backup_min_daily_retention_days": 7 + } + assert _validate("azure", {"vm_backup_min_daily_retention_days": 0}) == {} + assert _validate("azure", {"vm_backup_min_daily_retention_days": -1}) == {} + + +class Test_GCP_Schema: + def test_valid_values_round_trip(self): + raw = { + "mig_min_zones": 2, + "max_snapshot_age_days": 90, + "max_unused_account_days": 180, + "storage_min_retention_days": 90, + } + assert _validate("gcp", raw) == raw + + def test_zero_zone_count_dropped(self): + assert _validate("gcp", {"mig_min_zones": 0}) == {} + + +class Test_Kubernetes_Schema: + def test_valid_values_round_trip(self): + raw = { + "audit_log_maxbackup": 10, + "audit_log_maxsize": 100, + "audit_log_maxage": 30, + } + assert _validate("kubernetes", raw) == raw + + def test_negative_audit_log_dropped(self): + assert _validate("kubernetes", {"audit_log_maxage": -1}) == {} + + +class Test_M365_Schema: + def test_valid_values_round_trip(self): + raw = { + "sign_in_frequency": 4, + "recommended_mailtips_large_audience_threshold": 25, + "audit_log_age": 90, + } + assert _validate("m365", raw) == raw + + def test_negative_audit_log_age_dropped(self): + assert _validate("m365", {"audit_log_age": -10}) == {} + + +class Test_GitHub_Schema: + def test_valid_threshold(self): + assert _validate("github", {"inactive_not_archived_days_threshold": 180}) == { + "inactive_not_archived_days_threshold": 180 + } + + def test_zero_threshold_dropped(self): + assert _validate("github", {"inactive_not_archived_days_threshold": 0}) == {} + + +class Test_MongoDBAtlas_Schema: + def test_valid(self): + assert _validate( + "mongodbatlas", {"max_service_account_secret_validity_hours": 8} + ) == {"max_service_account_secret_validity_hours": 8} + + def test_invalid_negative(self): + assert ( + _validate("mongodbatlas", {"max_service_account_secret_validity_hours": -1}) + == {} + ) + + +class Test_Cloudflare_Schema: + def test_zero_retries_allowed(self): + # 0 is explicitly documented as "disable retries" in config.yaml. + assert _validate("cloudflare", {"max_retries": 0}) == {"max_retries": 0} + + def test_positive_retries_allowed(self): + assert _validate("cloudflare", {"max_retries": 3}) == {"max_retries": 3} + + def test_negative_retries_dropped(self): + assert _validate("cloudflare", {"max_retries": -1}) == {} + + +class Test_Vercel_Schema: + def test_owner_percentage_in_range(self): + assert _validate("vercel", {"max_owner_percentage": 20}) == { + "max_owner_percentage": 20 + } + assert _validate("vercel", {"max_owner_percentage": 0}) == { + "max_owner_percentage": 0 + } + assert _validate("vercel", {"max_owner_percentage": 100}) == { + "max_owner_percentage": 100 + } + + def test_owner_percentage_over_100_dropped(self): + assert _validate("vercel", {"max_owner_percentage": 150}) == {} + + def test_owner_percentage_negative_dropped(self): + assert _validate("vercel", {"max_owner_percentage": -1}) == {} + + def test_full_default_config_round_trip(self): + raw = { + "stable_branches": ["main", "master"], + "days_to_expire_threshold": 7, + "stale_token_threshold_days": 90, + "stale_invitation_threshold_days": 30, + "max_owner_percentage": 20, + "max_owners": 3, + "secret_suffixes": ["_KEY", "_SECRET", "_TOKEN"], + } + assert _validate("vercel", raw) == raw diff --git a/tests/config/schema/validator_test.py b/tests/config/schema/validator_test.py new file mode 100644 index 00000000000..398a0d10e8a --- /dev/null +++ b/tests/config/schema/validator_test.py @@ -0,0 +1,175 @@ +"""Behavioural tests for ``validate_provider_config``. + +The validator is the gatekeeper for every provider schema: its job is to +keep backwards-compatible behaviour (no exceptions, drop only the bad +keys) while loudly logging type mistakes. +""" + +import logging + +import pytest + +from prowler.config.schema.aws import AWSProviderConfig +from prowler.config.schema.registry import SCHEMAS +from prowler.config.schema.validator import validate_provider_config + + +class Test_Validate_Provider_Config_Contract: + """Generic invariants that must hold for any schema.""" + + def test_returns_empty_dict_when_raw_is_not_a_dict(self): + assert validate_provider_config("aws", None, AWSProviderConfig) == {} + assert validate_provider_config("aws", "string", AWSProviderConfig) == {} + assert validate_provider_config("aws", 42, AWSProviderConfig) == {} + assert validate_provider_config("aws", [], AWSProviderConfig) == {} + + def test_returns_raw_unchanged_when_no_schema_registered(self): + raw = {"anything": "goes", "even": [1, 2, 3]} + assert validate_provider_config("mystery_provider", raw, None) == raw + + def test_unknown_keys_pass_through_for_plugin_compatibility(self): + # Third-party plugins inject arbitrary keys; the schema must NOT + # filter them. This is the contract that lets the plugin ecosystem + # keep working when we add validation. + raw = {"plugin_custom_key": "foo", "lambda_min_azs": 2} + assert validate_provider_config("aws", raw, AWSProviderConfig) == { + "plugin_custom_key": "foo", + "lambda_min_azs": 2, + } + + def test_empty_dict_returns_empty_dict(self): + assert validate_provider_config("aws", {}, AWSProviderConfig) == {} + + def test_known_valid_value_passes_through_unchanged(self): + raw = {"max_ec2_instance_age_in_days": 180} + assert validate_provider_config("aws", raw, AWSProviderConfig) == { + "max_ec2_instance_age_in_days": 180 + } + + +class Test_Validate_Provider_Config_Coercion: + """Pydantic v2 coerces common type-mistakes automatically. We want to + keep that behaviour so quoted numerics in user configs ``Just Work``.""" + + def test_string_numeric_is_coerced_to_int(self): + out = validate_provider_config( + "aws", {"max_ec2_instance_age_in_days": "180"}, AWSProviderConfig + ) + assert out == {"max_ec2_instance_age_in_days": 180} + assert isinstance(out["max_ec2_instance_age_in_days"], int) + + def test_string_numeric_is_coerced_to_float(self): + out = validate_provider_config( + "aws", + {"threat_detection_privilege_escalation_threshold": "0.4"}, + AWSProviderConfig, + ) + assert out == {"threat_detection_privilege_escalation_threshold": 0.4} + + +class Test_Validate_Provider_Config_Drops_Invalid_Keys: + """When a field fails validation, only that key is dropped from the + returned dict. The rest of the user's config is preserved so the + consumer's ``audit_config.get(key, default)`` falls back to its own + built-in default for the offending field and uses user values for + everything else.""" + + def test_out_of_range_threshold_is_dropped(self, caplog): + with caplog.at_level(logging.WARNING): + out = validate_provider_config( + "aws", + { + "threat_detection_privilege_escalation_threshold": 2.0, + "lambda_min_azs": 2, + }, + AWSProviderConfig, + ) + assert out == {"lambda_min_azs": 2} + assert any( + "threat_detection_privilege_escalation_threshold" in r.getMessage() + for r in caplog.records + ) + + def test_invalid_enum_is_dropped(self): + out = validate_provider_config( + "aws", + {"ecr_repository_vulnerability_minimum_severity": "medum"}, + AWSProviderConfig, + ) + assert out == {} + + def test_wrong_shape_list_as_string_is_dropped(self): + # Classic YAML mistake: ``disallowed_regions: me-south-1`` without dashes. + # Pydantic refuses to silently treat a str as a single-element list, + # which is exactly the safety guarantee we want. + out = validate_provider_config( + "aws", + {"disallowed_regions": "me-south-1", "lambda_min_azs": 2}, + AWSProviderConfig, + ) + assert out == {"lambda_min_azs": 2} + + def test_negative_positive_int_is_dropped(self): + out = validate_provider_config( + "aws", {"max_ec2_instance_age_in_days": -1}, AWSProviderConfig + ) + assert out == {} + + def test_zero_is_dropped_for_strictly_positive_field(self): + # max_ec2_instance_age_in_days is gt=0. Zero would silently cause every + # instance to FAIL the age check. + out = validate_provider_config( + "aws", {"max_ec2_instance_age_in_days": 0}, AWSProviderConfig + ) + assert out == {} + + def test_multiple_invalid_keys_yield_multiple_warnings(self, caplog): + with caplog.at_level(logging.WARNING): + out = validate_provider_config( + "aws", + { + "max_ec2_instance_age_in_days": "nope", + "ecr_repository_vulnerability_minimum_severity": "medum", + "valid_extra_key": "kept", + }, + AWSProviderConfig, + ) + assert out == {"valid_extra_key": "kept"} + messages = " ".join(r.getMessage() for r in caplog.records) + assert "max_ec2_instance_age_in_days" in messages + assert "ecr_repository_vulnerability_minimum_severity" in messages + + def test_warning_message_includes_provider_and_field(self, caplog): + with caplog.at_level(logging.WARNING): + validate_provider_config( + "aws", + {"threat_detection_privilege_escalation_threshold": 5.0}, + AWSProviderConfig, + ) + assert any( + "prowler.config[aws.threat_detection_privilege_escalation_threshold]" + in r.getMessage() + for r in caplog.records + ) + + +class Test_Schemas_Registry: + """Every provider mentioned in the YAML config must have a schema.""" + + @pytest.mark.parametrize( + "provider", + [ + "aws", + "azure", + "gcp", + "kubernetes", + "m365", + "github", + "mongodbatlas", + "cloudflare", + "vercel", + ], + ) + def test_schema_registered_for_provider(self, provider): + assert provider in SCHEMAS + assert SCHEMAS[provider] is not None From ed17dc4b09d17a8d6a9b8c559b31f494e0956aa1 Mon Sep 17 00:00:00 2001 From: pedrooot Date: Tue, 9 Jun 2026 16:34:37 +0200 Subject: [PATCH 2/7] feat(sdk): scan configuration schema and validation for Prowler Cloud --- prowler/config/scan_config_schema.py | 106 +++++ prowler/config/schema/aws.py | 425 +++++++++++++++--- prowler/config/schema/azure.py | 91 +++- prowler/config/schema/cloudflare.py | 12 +- prowler/config/schema/gcp.py | 42 +- prowler/config/schema/github.py | 13 +- prowler/config/schema/kubernetes.py | 42 +- prowler/config/schema/m365.py | 50 ++- prowler/config/schema/mongodbatlas.py | 12 +- prowler/config/schema/vercel.py | 60 ++- tests/config/schema/aws_schema_test.py | 6 +- tests/config/schema/bounds_test.py | 398 ++++++++++++++++ .../schema/other_providers_schema_test.py | 16 +- 13 files changed, 1149 insertions(+), 124 deletions(-) create mode 100644 prowler/config/scan_config_schema.py create mode 100644 tests/config/schema/bounds_test.py diff --git a/prowler/config/scan_config_schema.py b/prowler/config/scan_config_schema.py new file mode 100644 index 00000000000..d4dd694e04d --- /dev/null +++ b/prowler/config/scan_config_schema.py @@ -0,0 +1,106 @@ +"""Bridge between the Pydantic-based provider schemas in +`prowler.config.schema` and the Prowler App backend (Django) + UI. + +The SDK runtime is intentionally LENIENT: invalid keys are dropped with a +warning and downstream checks fall back to their defaults +(`prowler.config.schema.validator.validate_provider_config`). + +The Prowler App, however, needs to surface those errors to the user when +they save a Scan Config from the UI, and to expose the schema as JSON so +the UI can validate live with `ajv`. This module provides: + +- `validate_scan_config(payload)` — STRICT: returns a list of + `{path, message}` errors without silently dropping anything. The DRF + serializer (`api/.../v1/serializers.py:validate_scan_config_payload`) + turns each entry into a `ValidationError`. + +- `SCAN_CONFIG_SCHEMA` — aggregated JSON Schema derived from the Pydantic + models via `model_json_schema()`. Served by the `/scan-configs/schema` + endpoint and consumed by the UI editor for in-editor live validation. +""" + +from typing import Any + +from pydantic import ValidationError + +from prowler.config.schema.registry import SCHEMAS + + +def _format_loc(loc: tuple) -> str: + """Render a Pydantic error location as `key[idx].nested`.""" + parts: list[str] = [] + for piece in loc: + if isinstance(piece, int): + if parts: + parts[-1] = f"{parts[-1]}[{piece}]" + else: + parts.append(f"[{piece}]") + else: + parts.append(str(piece)) + return ".".join(parts) if parts else "" + + +def validate_scan_config(payload: Any) -> list[dict]: + """Validate a scan config payload against the registered provider schemas. + + Strict by design: every Pydantic violation surfaces as a `{path, message}` + entry so the caller can decide how to present it. Unknown provider + sections are accepted (consistent with `additionalProperties: True` at + the top level — the SDK simply has no opinion on them). + """ + if not isinstance(payload, dict): + return [ + { + "path": "", + "message": "Scan config must be a mapping with provider sections.", + } + ] + + errors: list[dict] = [] + for provider, section in payload.items(): + schema_cls = SCHEMAS.get(provider) + if schema_cls is None: + # Unknown provider type: tolerated. The SDK will simply ignore it. + continue + if not isinstance(section, dict): + errors.append( + { + "path": str(provider), + "message": "section must be a mapping.", + } + ) + continue + try: + schema_cls.model_validate(section) + except ValidationError as exc: + for err in exc.errors(): + loc = err.get("loc") or () + path = _format_loc((str(provider), *loc)) + errors.append( + { + "path": path, + "message": err.get("msg", "validation error"), + } + ) + return errors + + +def _build_aggregated_schema() -> dict: + """Compose one JSON Schema per provider into a single top-level schema. + + The output mirrors the layout of `prowler/config/config.yaml` (a mapping + keyed by provider type) and is what the UI consumes via `ajv`. + """ + properties: dict[str, dict] = {} + for provider, schema_cls in SCHEMAS.items(): + properties[provider] = schema_cls.model_json_schema() + return { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Prowler Scan Config", + "type": "object", + "additionalProperties": True, + "properties": properties, + } + + +SCAN_CONFIG_SCHEMA: dict = _build_aggregated_schema() diff --git a/prowler/config/schema/aws.py b/prowler/config/schema/aws.py index d1eb74240fb..7f61e5ad11d 100644 --- a/prowler/config/schema/aws.py +++ b/prowler/config/schema/aws.py @@ -1,3 +1,17 @@ +"""AWS provider config schema. + +Bounds on every field are intentionally conservative: they are not the +absolute service maxima but the values that produce a useful security +check. A user is free to keep the built-in default by omitting the key — +out-of-range values are dropped with a warning at SDK runtime, and +rejected at the Prowler App backend. + +Whenever an upper bound is uncertain, the cap is set to a value that +still keeps the check meaningful (e.g. a 10-year window for date-based +thresholds) and avoids ints that obviously break downstream maths +(`min_kinesis_stream_retention_hours = 99999`). +""" + from ipaddress import ip_network from typing import Annotated, Literal, Optional @@ -5,25 +19,68 @@ from prowler.config.schema.base import ProviderConfigBase - -class _DetectSecretsPlugin(ProviderConfigBase): - """One entry inside ``detect_secrets_plugins``. - - Only ``name`` is required by the upstream library. ``limit`` is used by - the entropy detectors. Any other plugin-specific kwarg is preserved by - the ``extra="allow"`` policy inherited from ProviderConfigBase. - """ - - name: str - limit: Optional[float] = None +# ---- Reusable constants ----------------------------------------------------- + +# CloudWatch Logs only accepts these retention values (in days). Anything else +# is silently coerced to the next valid value by the API — we reject upfront. +_CLOUDWATCH_RETENTION_DAYS = ( + 1, + 3, + 5, + 7, + 14, + 30, + 60, + 90, + 120, + 150, + 180, + 365, + 400, + 545, + 731, + 1827, + 2192, + 2557, + 2922, + 3288, + 3653, +) + +_VALID_CW_RETENTION_LITERAL = Literal[ + 1, + 3, + 5, + 7, + 14, + 30, + 60, + 90, + 120, + 150, + 180, + 365, + 400, + 545, + 731, + 1827, + 2192, + 2557, + 2922, + 3288, + 3653, +] + + +# ---- Custom validators ------------------------------------------------------ def _validate_port_range(v: Optional[list[int]]) -> Optional[list[int]]: if v is None: return v for port in v: - if not 0 <= port <= 65535: - raise ValueError(f"port {port} is outside the valid range 0..65535") + if not 1 <= port <= 65535: + raise ValueError(f"port {port} is outside the valid range 1..65535") return v @@ -51,112 +108,342 @@ def _validate_trusted_ips(v: Optional[list[str]]) -> Optional[list[str]]: return v +def _validate_semver(v: Optional[str]) -> Optional[str]: + """Accept "1.4.0" style strings (used by Fargate platform versions).""" + if v is None: + return v + parts = v.split(".") + if len(parts) != 3 or not all(p.isdigit() for p in parts): + raise ValueError(f"{v!r} is not a valid semantic version (expected X.Y.Z)") + return v + + +def _validate_eks_minor(v: Optional[str]) -> Optional[str]: + """Accept "1.28" style strings (EKS minor versions).""" + if v is None: + return v + parts = v.split(".") + if len(parts) != 2 or not all(p.isdigit() for p in parts): + raise ValueError(f"{v!r} is not a valid EKS version (expected X.Y)") + return v + + +# ---- Nested models ---------------------------------------------------------- + + +class _DetectSecretsPlugin(ProviderConfigBase): + """One entry inside ``detect_secrets_plugins``. + + Only ``name`` is required by the upstream library. ``limit`` is used by + the entropy detectors. Any other plugin-specific kwarg is preserved by + the ``extra="allow"`` policy inherited from ProviderConfigBase. + """ + + name: str + limit: Optional[float] = Field( + default=None, + ge=0.0, + le=10.0, + description=( + "Entropy threshold for detect-secrets entropy plugins. Range: 0..10 " + "(Shannon entropy is bounded by log2(256)=8; >10 is meaningless)." + ), + ) + + +# ---- Main schema ------------------------------------------------------------ + + class AWSProviderConfig(ProviderConfigBase): - # IAM + # --- IAM --------------------------------------------------------------- mute_non_default_regions: Optional[bool] = None disallowed_regions: Optional[list[str]] = None - max_unused_access_keys_days: Optional[int] = Field(default=None, gt=0) - max_console_access_days: Optional[int] = Field(default=None, gt=0) - max_unused_sagemaker_access_days: Optional[int] = Field(default=None, gt=0) - - # EC2 - shodan_api_key: Optional[str] = None - max_security_group_rules: Optional[int] = Field(default=None, gt=0) - max_ec2_instance_age_in_days: Optional[int] = Field(default=None, gt=0) + max_unused_access_keys_days: Optional[int] = Field( + default=None, + ge=30, + le=180, + description=( + "Days an IAM user access key can stay unused before being flagged. " + "Range: 30..180 days (CIS AWS 1.13 recommends 45; NIST IA-5 ≤90)." + ), + ) + max_console_access_days: Optional[int] = Field( + default=None, + ge=30, + le=180, + description=( + "Days an IAM console password can stay unused before being flagged. " + "Range: 30..180 days (CIS AWS 1.12 recommends 45)." + ), + ) + max_unused_sagemaker_access_days: Optional[int] = Field( + default=None, + ge=7, + le=180, + description=( + "Days a SageMaker user access key can stay unused. Range: 7..180 " + "(SageMaker tokens are usually high-privilege over S3/KMS)." + ), + ) + + # --- EC2 --------------------------------------------------------------- + shodan_api_key: Optional[str] = Field( + default=None, + max_length=512, + description="API key for Shodan lookups on EC2 public IPs.", + ) + max_security_group_rules: Optional[int] = Field( + default=None, + ge=1, + le=1000, + description="Max ingress+egress rules per security group. AWS hard limit is 1000.", + ) + max_ec2_instance_age_in_days: Optional[int] = Field( + default=None, + ge=1, + le=1095, + description=( + "Days an EC2 instance can run before being flagged as old. " + "Range: 1..1095 (3 years; instances should be refreshed for patching " + "per NIST CM-3 — anything older is a security smell)." + ), + ) ec2_allowed_interface_types: Optional[list[str]] = None ec2_allowed_instance_owners: Optional[list[str]] = None ec2_high_risk_ports: Annotated[ Optional[list[int]], AfterValidator(_validate_port_range) - ] = None + ] = Field( + default=None, + description="TCP/UDP ports considered high-risk when reachable from the Internet (1..65535; port 0 is reserved).", + ) - # ECS - fargate_linux_latest_version: Optional[str] = None - fargate_windows_latest_version: Optional[str] = None + # --- ECS --------------------------------------------------------------- + fargate_linux_latest_version: Annotated[ + Optional[str], AfterValidator(_validate_semver) + ] = Field(default=None, description="Fargate Linux platform version (X.Y.Z).") + fargate_windows_latest_version: Annotated[ + Optional[str], AfterValidator(_validate_semver) + ] = Field(default=None, description="Fargate Windows platform version (X.Y.Z).") - # Cross-account trust + # --- Cross-account trust ---------------------------------------------- trusted_account_ids: Annotated[ Optional[list[str]], AfterValidator(_validate_account_ids) - ] = None + ] = Field( + default=None, + description="Additional 12-digit AWS account IDs trusted by cross-account checks.", + ) trusted_ips: Annotated[ Optional[list[str]], AfterValidator(_validate_trusted_ips) - ] = None + ] = Field( + default=None, + description="IPv4/IPv6 addresses or CIDR ranges that are NOT considered public.", + ) - # CloudWatch / CloudFormation - log_group_retention_days: Optional[int] = Field(default=None, gt=0) - recommended_cdk_bootstrap_version: Optional[int] = Field(default=None, gt=0) + # --- CloudWatch / CloudFormation -------------------------------------- + log_group_retention_days: Optional[_VALID_CW_RETENTION_LITERAL] = Field( + default=None, + description=( + "Required CloudWatch Logs retention in days. Must match one of the " + f"values accepted by the AWS API: {list(_CLOUDWATCH_RETENTION_DAYS)}." + ), + ) + recommended_cdk_bootstrap_version: Optional[int] = Field( + default=None, + ge=1, + le=100, + description="Min CDK bootstrap version expected on the account.", + ) - # AppStream - max_idle_disconnect_timeout_in_seconds: Optional[int] = Field(default=None, gt=0) - max_disconnect_timeout_in_seconds: Optional[int] = Field(default=None, gt=0) - max_session_duration_seconds: Optional[int] = Field(default=None, gt=0) + # --- AppStream -------------------------------------------------------- + max_idle_disconnect_timeout_in_seconds: Optional[int] = Field( + default=None, + ge=60, + le=1800, + description=( + "AppStream idle disconnect timeout (seconds). Range: 60..1800 " + "(NIST AC-12: sensitive sessions ≤15 min — cap at 30 min)." + ), + ) + max_disconnect_timeout_in_seconds: Optional[int] = Field( + default=None, + ge=60, + le=3600, + description="AppStream disconnect timeout (seconds). Range: 60..3600.", + ) + max_session_duration_seconds: Optional[int] = Field( + default=None, + ge=600, + le=86400, + description=( + "AppStream max session duration (seconds). Range: 600..86400 " + "(10 min .. 24 h — AWS AppStream hard limit per session)." + ), + ) - # Lambda + # --- Lambda ----------------------------------------------------------- obsolete_lambda_runtimes: Optional[list[str]] = None - lambda_min_azs: Optional[int] = Field(default=None, gt=0) + lambda_min_azs: Optional[int] = Field( + default=None, + ge=1, + le=6, + description="Min number of AZs a VPC-bound Lambda must span. Range: 1..6.", + ) - # Organizations + # --- Organizations ---------------------------------------------------- organizations_enabled_regions: Optional[list[str]] = None - organizations_trusted_delegated_administrators: Optional[list[str]] = None + organizations_trusted_delegated_administrators: Annotated[ + Optional[list[str]], AfterValidator(_validate_account_ids) + ] = None organizations_trusted_ids: Optional[list[str]] = None - # ECR + # --- ECR -------------------------------------------------------------- ecr_repository_vulnerability_minimum_severity: Optional[ - Literal["CRITICAL", "HIGH", "MEDIUM", "LOW"] - ] = None + Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"] + ] = Field( + default=None, + description="Highest severity tolerated for ECR images.", + ) - # Trusted Advisor + # --- Trusted Advisor -------------------------------------------------- verify_premium_support_plans: Optional[bool] = None - # CloudTrail threat detection + # --- CloudTrail threat detection: privilege escalation ---------------- threat_detection_privilege_escalation_threshold: Optional[float] = Field( - default=None, ge=0.0, le=1.0 + default=None, + ge=0.0, + le=1.0, + description="Fraction of suspicious actions that triggers the priv-esc detection.", ) threat_detection_privilege_escalation_minutes: Optional[int] = Field( - default=None, gt=0 + default=None, + ge=5, + le=43200, + description=( + "Lookback window (minutes) for priv-esc detection. Range: 5..43200 " + "(under 5 min the signal is dominated by false positives)." + ), ) threat_detection_privilege_escalation_actions: Optional[list[str]] = None + # --- CloudTrail threat detection: enumeration ------------------------- threat_detection_enumeration_threshold: Optional[float] = Field( - default=None, ge=0.0, le=1.0 + default=None, + ge=0.0, + le=1.0, + description="Fraction of suspicious actions that triggers the enumeration detection.", + ) + threat_detection_enumeration_minutes: Optional[int] = Field( + default=None, + ge=5, + le=43200, + description="Lookback window (minutes) for enumeration detection. Range: 5..43200.", ) - threat_detection_enumeration_minutes: Optional[int] = Field(default=None, gt=0) threat_detection_enumeration_actions: Optional[list[str]] = None + # --- CloudTrail threat detection: LLM jacking ------------------------- threat_detection_llm_jacking_threshold: Optional[float] = Field( - default=None, ge=0.0, le=1.0 + default=None, + ge=0.0, + le=1.0, + description="Fraction of suspicious actions that triggers the LLM-jacking detection.", + ) + threat_detection_llm_jacking_minutes: Optional[int] = Field( + default=None, + ge=5, + le=43200, + description="Lookback window (minutes) for LLM-jacking detection. Range: 5..43200.", ) - threat_detection_llm_jacking_minutes: Optional[int] = Field(default=None, gt=0) threat_detection_llm_jacking_actions: Optional[list[str]] = None - # RDS + # --- RDS -------------------------------------------------------------- check_rds_instance_replicas: Optional[bool] = None - # ACM - days_to_expire_threshold: Optional[int] = Field(default=None, gt=0) + # --- ACM -------------------------------------------------------------- + days_to_expire_threshold: Optional[int] = Field( + default=None, + ge=7, + le=365, + description=( + "Days before certificate expiration to flag. Range: 7..365 " + "(PCI-DSS 4.2.1.1: alert ≥30 days before expiry; <7 days is too " + "tight to actually act on)." + ), + ) insecure_key_algorithms: Optional[list[str]] = None - # EKS - eks_required_log_types: Optional[list[str]] = None - eks_cluster_oldest_version_supported: Optional[str] = None + # --- EKS -------------------------------------------------------------- + eks_required_log_types: Optional[ + list[ + Literal[ + "api", + "audit", + "authenticator", + "controllerManager", + "scheduler", + ] + ] + ] = Field( + default=None, + description="EKS control plane log types that must be enabled.", + ) + eks_cluster_oldest_version_supported: Annotated[ + Optional[str], AfterValidator(_validate_eks_minor) + ] = Field( + default=None, + description='Minimum supported EKS minor version, expected as "X.Y".', + ) - # CodeBuild + # --- CodeBuild -------------------------------------------------------- excluded_sensitive_environment_variables: Optional[list[str]] = None codebuild_github_allowed_organizations: Optional[list[str]] = None - # ELB / ELBv2 - elb_min_azs: Optional[int] = Field(default=None, gt=0) - elbv2_min_azs: Optional[int] = Field(default=None, gt=0) + # --- ELB / ELBv2 ------------------------------------------------------ + elb_min_azs: Optional[int] = Field( + default=None, + ge=1, + le=6, + description="Min AZs a Classic ELB must span. Range: 1..6.", + ) + elbv2_min_azs: Optional[int] = Field( + default=None, + ge=1, + le=6, + description="Min AZs an Application/Network LB must span. Range: 1..6.", + ) - # ElastiCache - minimum_snapshot_retention_period: Optional[int] = Field(default=None, gt=0) + # --- ElastiCache ----------------------------------------------------- + minimum_snapshot_retention_period: Optional[int] = Field( + default=None, + ge=1, + le=35, + description="Days an ElastiCache backup must be retained. Range: 1..35 (service hard limit).", + ) - # Secrets + # --- Secrets --------------------------------------------------------- secrets_ignore_patterns: Optional[list[str]] = None - max_days_secret_unused: Optional[int] = Field(default=None, gt=0) - max_days_secret_unrotated: Optional[int] = Field(default=None, gt=0) + max_days_secret_unused: Optional[int] = Field( + default=None, + ge=7, + le=365, + description="Days a Secrets Manager secret can stay unused. Range: 7..365.", + ) + max_days_secret_unrotated: Optional[int] = Field( + default=None, + ge=1, + le=180, + description=( + "Days a Secrets Manager secret can go without rotation. Range: 1..180 " + "(NIST IA-5: rotate quarterly; CIS recommends ≤90)." + ), + ) - # Kinesis - min_kinesis_stream_retention_hours: Optional[int] = Field(default=None, gt=0) + # --- Kinesis --------------------------------------------------------- + min_kinesis_stream_retention_hours: Optional[int] = Field( + default=None, + ge=24, + le=8760, + description="Hours of Kinesis stream retention. Range: 24..8760 (1 day .. 1 year).", + ) - # detect-secrets plugins + # --- detect-secrets plugin list ------------------------------------- detect_secrets_plugins: Optional[list[_DetectSecretsPlugin]] = None diff --git a/prowler/config/schema/azure.py b/prowler/config/schema/azure.py index 2b04ccb2e98..26953d80297 100644 --- a/prowler/config/schema/azure.py +++ b/prowler/config/schema/azure.py @@ -1,34 +1,91 @@ -from typing import Literal, Optional +"""Azure provider config schema with safety bounds. -from pydantic import Field +Bounds aim for values that produce a meaningful security check; out-of-range +values are dropped (SDK runtime) or rejected (Prowler App backend). +""" + +from typing import Annotated, Literal, Optional + +from pydantic import AfterValidator, Field from prowler.config.schema.base import ProviderConfigBase +def _validate_dotted_version(v: Optional[str]) -> Optional[str]: + """Accept ``"8.2"``, ``"3.12"``, ``"17"`` style version strings. + + Used by App Service language version fields where the upstream APIs + accept either ``MAJOR`` or ``MAJOR.MINOR`` notation. + """ + if v is None: + return v + parts = v.split(".") + if not (1 <= len(parts) <= 2) or not all(p.isdigit() for p in parts): + raise ValueError(f"{v!r} is not a valid version (expected 'X' or 'X.Y')") + return v + + class AzureProviderConfig(ProviderConfigBase): - # Network - shodan_api_key: Optional[str] = None + # --- Network --------------------------------------------------------- + shodan_api_key: Optional[str] = Field( + default=None, + max_length=512, + description="API key for Shodan lookups on Azure public IPs.", + ) - # Defender + # --- Defender -------------------------------------------------------- defender_attack_path_minimal_risk_level: Optional[ Literal["Low", "Medium", "High", "Critical"] - ] = None + ] = Field( + default=None, + description="Minimum attack-path risk level worth a notification.", + ) - # App Service - php_latest_version: Optional[str] = None - python_latest_version: Optional[str] = None - java_latest_version: Optional[str] = None + # --- App Service ---------------------------------------------------- + php_latest_version: Annotated[ + Optional[str], AfterValidator(_validate_dotted_version) + ] = Field(default=None, description='PHP minimum acceptable version, e.g. "8.2".') + python_latest_version: Annotated[ + Optional[str], AfterValidator(_validate_dotted_version) + ] = Field( + default=None, description='Python minimum acceptable version, e.g. "3.12".' + ) + java_latest_version: Annotated[ + Optional[str], AfterValidator(_validate_dotted_version) + ] = Field(default=None, description='Java minimum acceptable version, e.g. "17".') - # SQL - recommended_minimal_tls_versions: Optional[list[str]] = None + # --- SQL ------------------------------------------------------------ + recommended_minimal_tls_versions: Optional[list[Literal["1.2", "1.3"]]] = Field( + default=None, + description="TLS versions accepted on Azure SQL Server.", + ) - # Virtual Machines + # --- Virtual Machines ----------------------------------------------- desired_vm_sku_sizes: Optional[list[str]] = None - vm_backup_min_daily_retention_days: Optional[int] = Field(default=None, gt=0) + vm_backup_min_daily_retention_days: Optional[int] = Field( + default=None, + ge=7, + le=9999, + description=( + "Min daily backup retention days. Range: 7..9999 " + "(Azure Backup hard limit; <7 days defeats DR/ransomware recovery)." + ), + ) - # API Management threat detection + # --- API Management threat detection (LLM jacking) ----------------- apim_threat_detection_llm_jacking_threshold: Optional[float] = Field( - default=None, ge=0.0, le=1.0 + default=None, + ge=0.0, + le=1.0, + description="Fraction of suspicious actions that triggers the detection.", + ) + apim_threat_detection_llm_jacking_minutes: Optional[int] = Field( + default=None, + ge=5, + le=43200, + description=( + "Lookback window (minutes) for LLM-jacking detection. Range: 5..43200 " + "(under 5 min the signal is dominated by false positives)." + ), ) - apim_threat_detection_llm_jacking_minutes: Optional[int] = Field(default=None, gt=0) apim_threat_detection_llm_jacking_actions: Optional[list[str]] = None diff --git a/prowler/config/schema/cloudflare.py b/prowler/config/schema/cloudflare.py index 0b612f047ff..914aa754aa4 100644 --- a/prowler/config/schema/cloudflare.py +++ b/prowler/config/schema/cloudflare.py @@ -1,3 +1,5 @@ +"""Cloudflare provider config schema with safety bounds.""" + from typing import Optional from pydantic import Field @@ -6,5 +8,11 @@ class CloudflareProviderConfig(ProviderConfigBase): - # 0 disables retries; negative values would loop or assert in the client. - max_retries: Optional[int] = Field(default=None, ge=0) + max_retries: Optional[int] = Field( + default=None, + ge=0, + le=10, + description=( + "Max retries for Cloudflare API requests. Range: 0..10 (0 disables retries)." + ), + ) diff --git a/prowler/config/schema/gcp.py b/prowler/config/schema/gcp.py index 22b3c29ce3f..59e1a162fab 100644 --- a/prowler/config/schema/gcp.py +++ b/prowler/config/schema/gcp.py @@ -1,3 +1,5 @@ +"""GCP provider config schema with safety bounds.""" + from typing import Optional from pydantic import Field @@ -6,8 +8,38 @@ class GCPProviderConfig(ProviderConfigBase): - shodan_api_key: Optional[str] = None - mig_min_zones: Optional[int] = Field(default=None, gt=0) - max_snapshot_age_days: Optional[int] = Field(default=None, gt=0) - max_unused_account_days: Optional[int] = Field(default=None, gt=0) - storage_min_retention_days: Optional[int] = Field(default=None, gt=0) + shodan_api_key: Optional[str] = Field( + default=None, + max_length=512, + description="API key for Shodan lookups on GCP public IPs.", + ) + mig_min_zones: Optional[int] = Field( + default=None, + ge=1, + le=5, + description="Min zones a Managed Instance Group must span. Range: 1..5.", + ) + max_snapshot_age_days: Optional[int] = Field( + default=None, + ge=1, + le=1095, + description=( + "Days a disk snapshot can age before being flagged. Range: 1..1095 " + "(3 years; older snapshots typically miss data-class compliance)." + ), + ) + max_unused_account_days: Optional[int] = Field( + default=None, + ge=30, + le=365, + description=( + "Days a service account or user-managed key can stay unused. " + "Range: 30..365." + ), + ) + storage_min_retention_days: Optional[int] = Field( + default=None, + ge=1, + le=3650, + description="Min retention period on Cloud Storage buckets. Range: 1..3650.", + ) diff --git a/prowler/config/schema/github.py b/prowler/config/schema/github.py index ce1bf2dbb88..30b5a13b858 100644 --- a/prowler/config/schema/github.py +++ b/prowler/config/schema/github.py @@ -1,3 +1,5 @@ +"""GitHub provider config schema with safety bounds.""" + from typing import Optional from pydantic import Field @@ -6,4 +8,13 @@ class GitHubProviderConfig(ProviderConfigBase): - inactive_not_archived_days_threshold: Optional[int] = Field(default=None, gt=0) + inactive_not_archived_days_threshold: Optional[int] = Field( + default=None, + ge=30, + le=3650, + description=( + "Days a repository can stay inactive without being archived before " + "being flagged. Range: 30..3650 (CIS GitHub recommends 180; " + "<30 days produces false positives on seasonal projects)." + ), + ) diff --git a/prowler/config/schema/kubernetes.py b/prowler/config/schema/kubernetes.py index 01612ac5bf0..3235971500a 100644 --- a/prowler/config/schema/kubernetes.py +++ b/prowler/config/schema/kubernetes.py @@ -1,3 +1,5 @@ +"""Kubernetes provider config schema with safety bounds.""" + from typing import Optional from pydantic import Field @@ -6,8 +8,38 @@ class KubernetesProviderConfig(ProviderConfigBase): - audit_log_maxbackup: Optional[int] = Field(default=None, gt=0) - audit_log_maxsize: Optional[int] = Field(default=None, gt=0) - audit_log_maxage: Optional[int] = Field(default=None, gt=0) - apiserver_strong_ciphers: Optional[list[str]] = None - kubelet_strong_ciphers: Optional[list[str]] = None + audit_log_maxbackup: Optional[int] = Field( + default=None, + ge=2, + le=1000, + description=( + "API server audit log file rotations to keep. Range: 2..1000 " + "(CIS Kubernetes 1.2.18 recommends ≥10)." + ), + ) + audit_log_maxsize: Optional[int] = Field( + default=None, + ge=10, + le=10000, + description=( + "Max MB per audit log file before rotation. Range: 10..10000 MB " + "(CIS Kubernetes 1.2.19 recommends ≥100 MB)." + ), + ) + audit_log_maxage: Optional[int] = Field( + default=None, + ge=7, + le=3650, + description=( + "Days an audit log file is retained. Range: 7..3650 " + "(CIS Kubernetes 1.2.17 recommends ≥30 days)." + ), + ) + apiserver_strong_ciphers: Optional[list[str]] = Field( + default=None, + description="Whitelist of strong TLS cipher suites required on the API server.", + ) + kubelet_strong_ciphers: Optional[list[str]] = Field( + default=None, + description="Whitelist of strong TLS cipher suites required on kubelet.", + ) diff --git a/prowler/config/schema/m365.py b/prowler/config/schema/m365.py index 01013ac2ae0..ff1ff98285d 100644 --- a/prowler/config/schema/m365.py +++ b/prowler/config/schema/m365.py @@ -1,3 +1,5 @@ +"""M365 provider config schema with safety bounds.""" + from typing import Optional from pydantic import Field @@ -6,19 +8,47 @@ class M365ProviderConfig(ProviderConfigBase): - # Entra - sign_in_frequency: Optional[int] = Field(default=None, gt=0) + # --- Entra (sign-in policy) ---------------------------------------- + sign_in_frequency: Optional[int] = Field( + default=None, + ge=1, + le=168, + description=( + "Hours between forced sign-ins for admin users. Range: 1..168 (1 h .. 7 days). " + "Microsoft Conditional Access baseline for admin roles is ≤24 h." + ), + ) - # Teams - allowed_cloud_storage_services: Optional[list[str]] = None + # --- Teams --------------------------------------------------------- + allowed_cloud_storage_services: Optional[list[str]] = Field( + default=None, + description="External cloud storage services allowed in Teams.", + ) - # Exchange + # --- Exchange ------------------------------------------------------ recommended_mailtips_large_audience_threshold: Optional[int] = Field( - default=None, gt=0 + default=None, + ge=5, + le=10000, + description=( + "Recipient count that should trigger a 'large audience' MailTip. " + "Range: 5..10000 (Microsoft default 25)." + ), ) - # Defender malware policy - default_recommended_extensions: Optional[list[str]] = None + # --- Defender malware policy -------------------------------------- + default_recommended_extensions: Optional[list[str]] = Field( + default=None, + description="File extensions blocked by the malware policy.", + ) - # Mailbox auditing - audit_log_age: Optional[int] = Field(default=None, gt=0) + # --- Mailbox auditing --------------------------------------------- + audit_log_age: Optional[int] = Field( + default=None, + ge=30, + le=3650, + description=( + "Days mailbox audit logs must be retained. Range: 30..3650 " + "(M365 E3 default is 90 days; SEC/FINRA require ≥7 years)." + ), + ) diff --git a/prowler/config/schema/mongodbatlas.py b/prowler/config/schema/mongodbatlas.py index d9a210184c5..ab8b9a79d67 100644 --- a/prowler/config/schema/mongodbatlas.py +++ b/prowler/config/schema/mongodbatlas.py @@ -1,3 +1,5 @@ +"""MongoDB Atlas provider config schema with safety bounds.""" + from typing import Optional from pydantic import Field @@ -6,4 +8,12 @@ class MongoDBAtlasProviderConfig(ProviderConfigBase): - max_service_account_secret_validity_hours: Optional[int] = Field(default=None, gt=0) + max_service_account_secret_validity_hours: Optional[int] = Field( + default=None, + ge=1, + le=720, + description=( + "Max hours a service account secret can stay valid. " + "Range: 1..720 (1 h .. 30 days)." + ), + ) diff --git a/prowler/config/schema/vercel.py b/prowler/config/schema/vercel.py index 8d0f672fd32..69e5511a833 100644 --- a/prowler/config/schema/vercel.py +++ b/prowler/config/schema/vercel.py @@ -1,3 +1,5 @@ +"""Vercel provider config schema with safety bounds.""" + from typing import Optional from pydantic import Field @@ -6,10 +8,54 @@ class VercelProviderConfig(ProviderConfigBase): - stable_branches: Optional[list[str]] = None - days_to_expire_threshold: Optional[int] = Field(default=None, gt=0) - stale_token_threshold_days: Optional[int] = Field(default=None, gt=0) - stale_invitation_threshold_days: Optional[int] = Field(default=None, gt=0) - max_owner_percentage: Optional[int] = Field(default=None, ge=0, le=100) - max_owners: Optional[int] = Field(default=None, gt=0) - secret_suffixes: Optional[list[str]] = None + stable_branches: Optional[list[str]] = Field( + default=None, + description="Branches considered stable for production deployments.", + ) + days_to_expire_threshold: Optional[int] = Field( + default=None, + ge=7, + le=365, + description=( + "Days before token/certificate expiration to flag. Range: 7..365 " + "(PCI-DSS 4.2.1.1: alert ≥30 days before expiry)." + ), + ) + stale_token_threshold_days: Optional[int] = Field( + default=None, + ge=30, + le=3650, + description=( + "Days of inactivity before a token is considered stale. Range: 30..3650 " + "(NIST AC-2(3) typical window 30..90 days)." + ), + ) + stale_invitation_threshold_days: Optional[int] = Field( + default=None, + ge=7, + le=365, + description=( + "Days a pending invitation can stay open. Range: 7..365 " + "(OWASP ASVS 2.7.1 recommends short-lived invitations)." + ), + ) + max_owner_percentage: Optional[int] = Field( + default=None, + ge=1, + le=50, + description=( + "Max percentage of team members that can have the OWNER role. " + "Range: 1..50 (PoLP — having >50% of a team as OWNER defeats RBAC; " + "industry guidance recommends ≤25%)." + ), + ) + max_owners: Optional[int] = Field( + default=None, + ge=1, + le=1000, + description="Absolute max owners (overrides percentage for large teams). Range: 1..1000.", + ) + secret_suffixes: Optional[list[str]] = Field( + default=None, + description="Suffixes that mark a project env var as secret-like.", + ) diff --git a/tests/config/schema/aws_schema_test.py b/tests/config/schema/aws_schema_test.py index 989ebde0e2e..9bc7e02d49c 100644 --- a/tests/config/schema/aws_schema_test.py +++ b/tests/config/schema/aws_schema_test.py @@ -101,11 +101,15 @@ def test_cidr_with_host_bits_is_accepted(self): class Test_AWS_Ports: def test_valid_ports_in_range(self): - ports = [25, 80, 443, 65535, 0] + ports = [25, 80, 443, 65535, 1] assert _validate({"ec2_high_risk_ports": ports}) == { "ec2_high_risk_ports": ports } + def test_port_zero_is_dropped(self): + # Port 0 is reserved and not a valid security signal. + assert _validate({"ec2_high_risk_ports": [0]}) == {} + def test_out_of_range_port_is_dropped(self): assert _validate({"ec2_high_risk_ports": [70000]}) == {} diff --git a/tests/config/schema/bounds_test.py b/tests/config/schema/bounds_test.py new file mode 100644 index 00000000000..4235a81582a --- /dev/null +++ b/tests/config/schema/bounds_test.py @@ -0,0 +1,398 @@ +"""Boundary tests for the safety bounds added on top of the upstream schemas. + +Each parametrised case checks (a) the min and max values are accepted and +(b) one step outside the range is rejected. Custom validators (semver, +EKS minor, dotted version, port range, account IDs, IPs) get focused +positive/negative tests. + +Tests use the public adapter ``prowler.config.scan_config_schema``: a +schema violation surfaces as a list of ``{"path", "message"}`` entries. +This keeps the contract the Prowler App backend depends on under test. +""" + +import pytest + +from prowler.config.scan_config_schema import validate_scan_config + + +def _has_error_for(errors, path_substr: str) -> bool: + return any(path_substr in e["path"] for e in errors) + + +# Each tuple: (provider, key, min_allowed, max_allowed) +INT_BOUND_CASES = [ + # AWS + ("aws", "max_unused_access_keys_days", 30, 180), + ("aws", "max_console_access_days", 30, 180), + ("aws", "max_unused_sagemaker_access_days", 7, 180), + ("aws", "max_security_group_rules", 1, 1000), + ("aws", "max_ec2_instance_age_in_days", 1, 1095), + ("aws", "recommended_cdk_bootstrap_version", 1, 100), + ("aws", "max_idle_disconnect_timeout_in_seconds", 60, 1800), + ("aws", "max_disconnect_timeout_in_seconds", 60, 3600), + ("aws", "max_session_duration_seconds", 600, 86400), + ("aws", "lambda_min_azs", 1, 6), + ("aws", "threat_detection_privilege_escalation_minutes", 5, 43200), + ("aws", "threat_detection_enumeration_minutes", 5, 43200), + ("aws", "threat_detection_llm_jacking_minutes", 5, 43200), + ("aws", "days_to_expire_threshold", 7, 365), + ("aws", "elb_min_azs", 1, 6), + ("aws", "elbv2_min_azs", 1, 6), + ("aws", "minimum_snapshot_retention_period", 1, 35), + ("aws", "max_days_secret_unused", 7, 365), + ("aws", "max_days_secret_unrotated", 1, 180), + ("aws", "min_kinesis_stream_retention_hours", 24, 8760), + # Azure + ("azure", "vm_backup_min_daily_retention_days", 7, 9999), + ("azure", "apim_threat_detection_llm_jacking_minutes", 5, 43200), + # GCP + ("gcp", "mig_min_zones", 1, 5), + ("gcp", "max_snapshot_age_days", 1, 1095), + ("gcp", "max_unused_account_days", 30, 365), + ("gcp", "storage_min_retention_days", 1, 3650), + # Kubernetes + ("kubernetes", "audit_log_maxbackup", 2, 1000), + ("kubernetes", "audit_log_maxsize", 10, 10000), + ("kubernetes", "audit_log_maxage", 7, 3650), + # M365 + ("m365", "sign_in_frequency", 1, 168), + ("m365", "recommended_mailtips_large_audience_threshold", 5, 10000), + ("m365", "audit_log_age", 30, 3650), + # GitHub + ("github", "inactive_not_archived_days_threshold", 30, 3650), + # MongoDB Atlas + ("mongodbatlas", "max_service_account_secret_validity_hours", 1, 720), + # Cloudflare + ("cloudflare", "max_retries", 0, 10), + # Vercel + ("vercel", "days_to_expire_threshold", 7, 365), + ("vercel", "stale_token_threshold_days", 30, 3650), + ("vercel", "stale_invitation_threshold_days", 7, 365), + ("vercel", "max_owner_percentage", 1, 50), + ("vercel", "max_owners", 1, 1000), +] + + +FLOAT_THRESHOLD_FIELDS = [ + ("aws", "threat_detection_privilege_escalation_threshold"), + ("aws", "threat_detection_enumeration_threshold"), + ("aws", "threat_detection_llm_jacking_threshold"), + ("azure", "apim_threat_detection_llm_jacking_threshold"), +] + + +class TestIntegerBounds: + """Each int field accepts both ends of its range and rejects ±1 outside.""" + + @pytest.mark.parametrize("provider, key, lo, hi", INT_BOUND_CASES) + def test_min_accepted(self, provider, key, lo, hi): + assert validate_scan_config({provider: {key: lo}}) == [] + + @pytest.mark.parametrize("provider, key, lo, hi", INT_BOUND_CASES) + def test_max_accepted(self, provider, key, lo, hi): + assert validate_scan_config({provider: {key: hi}}) == [] + + @pytest.mark.parametrize("provider, key, lo, hi", INT_BOUND_CASES) + def test_below_min_rejected(self, provider, key, lo, hi): + errors = validate_scan_config({provider: {key: lo - 1}}) + assert _has_error_for(errors, f"{provider}.{key}"), errors + + @pytest.mark.parametrize("provider, key, lo, hi", INT_BOUND_CASES) + def test_above_max_rejected(self, provider, key, lo, hi): + errors = validate_scan_config({provider: {key: hi + 1}}) + assert _has_error_for(errors, f"{provider}.{key}"), errors + + +class TestFloatThresholds: + """Threshold floats must stay within 0..1 inclusive.""" + + @pytest.mark.parametrize("provider, key", FLOAT_THRESHOLD_FIELDS) + def test_zero_and_one_accepted(self, provider, key): + assert validate_scan_config({provider: {key: 0.0}}) == [] + assert validate_scan_config({provider: {key: 1.0}}) == [] + assert validate_scan_config({provider: {key: 0.5}}) == [] + + @pytest.mark.parametrize("provider, key", FLOAT_THRESHOLD_FIELDS) + def test_negative_rejected(self, provider, key): + errors = validate_scan_config({provider: {key: -0.01}}) + assert _has_error_for(errors, f"{provider}.{key}") + + @pytest.mark.parametrize("provider, key", FLOAT_THRESHOLD_FIELDS) + def test_above_one_rejected(self, provider, key): + errors = validate_scan_config({provider: {key: 1.01}}) + assert _has_error_for(errors, f"{provider}.{key}") + + +class TestCloudWatchRetention: + """`log_group_retention_days` only accepts the AWS-approved enum values.""" + + @pytest.mark.parametrize("value", [1, 7, 30, 365, 731, 3653]) + def test_valid_values_accepted(self, value): + assert validate_scan_config({"aws": {"log_group_retention_days": value}}) == [] + + @pytest.mark.parametrize("value", [0, 2, 42, 500, 999, 4000]) + def test_invalid_values_rejected(self, value): + errors = validate_scan_config({"aws": {"log_group_retention_days": value}}) + assert _has_error_for(errors, "aws.log_group_retention_days") + + +class TestSemverValidator: + """AWS Fargate platform versions: X.Y.Z.""" + + @pytest.mark.parametrize("value", ["1.4.0", "1.0.0", "0.0.1", "10.20.30"]) + def test_accepts_semver(self, value): + assert ( + validate_scan_config({"aws": {"fargate_linux_latest_version": value}}) == [] + ) + + @pytest.mark.parametrize("value", ["1.4", "1", "v1.4.0", "1.4.0-beta", "a.b.c", ""]) + def test_rejects_non_semver(self, value): + errors = validate_scan_config({"aws": {"fargate_linux_latest_version": value}}) + assert _has_error_for(errors, "aws.fargate_linux_latest_version") + + +class TestEksVersionValidator: + """`eks_cluster_oldest_version_supported` expects MAJOR.MINOR.""" + + @pytest.mark.parametrize("value", ["1.28", "1.29", "1.30", "2.0"]) + def test_accepts_minor(self, value): + assert ( + validate_scan_config( + {"aws": {"eks_cluster_oldest_version_supported": value}} + ) + == [] + ) + + @pytest.mark.parametrize("value", ["1.28.0", "v1.28", "1", "1.x", ""]) + def test_rejects_invalid(self, value): + errors = validate_scan_config( + {"aws": {"eks_cluster_oldest_version_supported": value}} + ) + assert _has_error_for(errors, "aws.eks_cluster_oldest_version_supported") + + +class TestEksLogTypesEnum: + """Only the documented log types are accepted.""" + + def test_full_enum_accepted(self): + assert ( + validate_scan_config( + { + "aws": { + "eks_required_log_types": [ + "api", + "audit", + "authenticator", + "controllerManager", + "scheduler", + ] + } + } + ) + == [] + ) + + def test_unknown_type_rejected(self): + errors = validate_scan_config( + {"aws": {"eks_required_log_types": ["api", "telemetry"]}} + ) + assert _has_error_for(errors, "aws.eks_required_log_types") + + +class TestAzureDottedVersion: + """App Service versions accept 'X' and 'X.Y' but not 'X.Y.Z' or junk.""" + + @pytest.mark.parametrize("value", ["8.2", "3.12", "17"]) + def test_accepts(self, value): + assert validate_scan_config({"azure": {"php_latest_version": value}}) == [] + assert validate_scan_config({"azure": {"python_latest_version": value}}) == [] + assert validate_scan_config({"azure": {"java_latest_version": value}}) == [] + + @pytest.mark.parametrize("value", ["8.2.0", "v8", "8.x", ""]) + def test_rejects(self, value): + errors = validate_scan_config({"azure": {"php_latest_version": value}}) + assert _has_error_for(errors, "azure.php_latest_version") + + +class TestAzureTlsLiteralEnum: + """Only TLS 1.2 and 1.3 are tolerated by the recommended list.""" + + def test_accepted_versions(self): + assert ( + validate_scan_config( + {"azure": {"recommended_minimal_tls_versions": ["1.2", "1.3"]}} + ) + == [] + ) + + @pytest.mark.parametrize("value", ["1.0", "1.1", "2.0", ""]) + def test_unknown_version_rejected(self, value): + errors = validate_scan_config( + {"azure": {"recommended_minimal_tls_versions": [value]}} + ) + assert _has_error_for(errors, "azure.recommended_minimal_tls_versions") + + +class TestAzureRiskLevelLiteral: + """Defender attack-path risk level is a closed enum.""" + + @pytest.mark.parametrize("value", ["Low", "Medium", "High", "Critical"]) + def test_accepted(self, value): + assert ( + validate_scan_config( + {"azure": {"defender_attack_path_minimal_risk_level": value}} + ) + == [] + ) + + @pytest.mark.parametrize("value", ["low", "CRITICAL", "Severe", ""]) + def test_rejected(self, value): + errors = validate_scan_config( + {"azure": {"defender_attack_path_minimal_risk_level": value}} + ) + assert _has_error_for(errors, "azure.defender_attack_path_minimal_risk_level") + + +class TestECRSeverityLiteral: + """ECR severity is a closed enum (with INFORMATIONAL allowed).""" + + @pytest.mark.parametrize( + "value", + ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFORMATIONAL"], + ) + def test_accepted(self, value): + assert ( + validate_scan_config( + {"aws": {"ecr_repository_vulnerability_minimum_severity": value}} + ) + == [] + ) + + @pytest.mark.parametrize("value", ["URGENT", "low", "Crit", ""]) + def test_rejected(self, value): + errors = validate_scan_config( + {"aws": {"ecr_repository_vulnerability_minimum_severity": value}} + ) + assert _has_error_for( + errors, "aws.ecr_repository_vulnerability_minimum_severity" + ) + + +class TestPortRangeValidator: + """Each entry of `ec2_high_risk_ports` must be 1..65535 (0 is reserved).""" + + def test_valid_ports(self): + assert ( + validate_scan_config({"aws": {"ec2_high_risk_ports": [1, 22, 8080, 65535]}}) + == [] + ) + + @pytest.mark.parametrize("value", [-1, 0, 65536, 99999]) + def test_invalid_port_rejected(self, value): + errors = validate_scan_config({"aws": {"ec2_high_risk_ports": [80, value]}}) + assert _has_error_for(errors, "aws.ec2_high_risk_ports") + + +class TestAccountIdsValidator: + """AWS account IDs are 12-digit strings.""" + + def test_valid(self): + assert ( + validate_scan_config( + {"aws": {"trusted_account_ids": ["123456789012", "098765432109"]}} + ) + == [] + ) + + @pytest.mark.parametrize( + "value", ["12345", "12345678901", "1234567890123", "12345678901a"] + ) + def test_invalid_rejected(self, value): + errors = validate_scan_config({"aws": {"trusted_account_ids": [value]}}) + assert _has_error_for(errors, "aws.trusted_account_ids") + + +class TestTrustedIpsValidator: + """Trusted IPs accept IPv4, IPv6, and CIDR; reject junk.""" + + @pytest.mark.parametrize( + "value", + ["1.2.3.4", "10.0.0.0/8", "2001:db8::1", "2001:db8::/32"], + ) + def test_valid(self, value): + assert validate_scan_config({"aws": {"trusted_ips": [value]}}) == [] + + @pytest.mark.parametrize( + "value", ["not.an.ip", "1.2.3.300", "10.0.0.0/40", "::ffff:::"] + ) + def test_invalid_rejected(self, value): + errors = validate_scan_config({"aws": {"trusted_ips": [value]}}) + assert _has_error_for(errors, "aws.trusted_ips") + + +class TestDetectSecretsEntropyBound: + """`detect_secrets_plugins[].limit` is Shannon entropy: 0..10.""" + + @pytest.mark.parametrize("value", [0.0, 3.5, 4.5, 8.0, 10.0]) + def test_valid(self, value): + assert ( + validate_scan_config( + { + "aws": { + "detect_secrets_plugins": [ + {"name": "Base64HighEntropyString", "limit": value} + ] + } + } + ) + == [] + ) + + @pytest.mark.parametrize("value", [-0.1, 10.01, 50]) + def test_invalid(self, value): + errors = validate_scan_config( + { + "aws": { + "detect_secrets_plugins": [ + {"name": "Base64HighEntropyString", "limit": value} + ] + } + } + ) + assert _has_error_for(errors, "aws.detect_secrets_plugins") + + +class TestAdapterRobustness: + """Top-level adapter behaviour the Prowler App backend depends on.""" + + def test_non_dict_payload(self): + errors = validate_scan_config([1, 2, 3]) + assert len(errors) == 1 + assert errors[0]["path"] == "" + + def test_unknown_provider_section_tolerated(self): + # additionalProperties: True at the root level by design. + assert validate_scan_config({"newprovider": {"foo": "bar"}}) == [] + + def test_unknown_key_tolerated_by_pydantic_extra_allow(self): + # ProviderConfigBase has extra="allow" for forward compatibility. + assert validate_scan_config({"aws": {"completely_new_knob": 1}}) == [] + + def test_provider_section_must_be_mapping(self): + errors = validate_scan_config({"aws": "not a mapping"}) + assert _has_error_for(errors, "aws") + + def test_multiple_errors_surfaced(self): + errors = validate_scan_config( + { + "aws": { + "max_unused_access_keys_days": 5, # below min 30 + "max_security_group_rules": 99999, # above max 1000 + "ec2_high_risk_ports": [80, 70000], # port out of range + } + } + ) + # All three should surface independently. + assert _has_error_for(errors, "aws.max_unused_access_keys_days") + assert _has_error_for(errors, "aws.max_security_group_rules") + assert _has_error_for(errors, "aws.ec2_high_risk_ports") diff --git a/tests/config/schema/other_providers_schema_test.py b/tests/config/schema/other_providers_schema_test.py index 163be362043..f4dc5184ab4 100644 --- a/tests/config/schema/other_providers_schema_test.py +++ b/tests/config/schema/other_providers_schema_test.py @@ -122,17 +122,21 @@ def test_owner_percentage_in_range(self): assert _validate("vercel", {"max_owner_percentage": 20}) == { "max_owner_percentage": 20 } - assert _validate("vercel", {"max_owner_percentage": 0}) == { - "max_owner_percentage": 0 + assert _validate("vercel", {"max_owner_percentage": 1}) == { + "max_owner_percentage": 1 } - assert _validate("vercel", {"max_owner_percentage": 100}) == { - "max_owner_percentage": 100 + assert _validate("vercel", {"max_owner_percentage": 50}) == { + "max_owner_percentage": 50 } - def test_owner_percentage_over_100_dropped(self): + def test_owner_percentage_over_max_dropped(self): + # Tightened to 1..50 — anything above (incl. previous 100) is dropped. + assert _validate("vercel", {"max_owner_percentage": 51}) == {} assert _validate("vercel", {"max_owner_percentage": 150}) == {} - def test_owner_percentage_negative_dropped(self): + def test_owner_percentage_zero_or_negative_dropped(self): + # 0 is no longer a valid configuration (defeats PoLP signal). + assert _validate("vercel", {"max_owner_percentage": 0}) == {} assert _validate("vercel", {"max_owner_percentage": -1}) == {} def test_full_default_config_round_trip(self): From 88926cc052e129da3132f5770b5a72598b41b06e Mon Sep 17 00:00:00 2001 From: pedrooot Date: Tue, 9 Jun 2026 16:40:20 +0200 Subject: [PATCH 3/7] chore(changelog): update with latest changes --- prowler/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index aafcd9f3454..aa34d7e8c6e 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_service_principal_no_secrets_for_permanent_tier0_roles` check for M365 provider [(#10788)](https://github.com/prowler-cloud/prowler/pull/10788) - `iam_user_access_not_stale_to_sagemaker` check for AWS provider with configurable `max_unused_sagemaker_access_days` (default 90) [(#11000)](https://github.com/prowler-cloud/prowler/pull/11000) - `cloudtrail_bedrock_logging_enabled` check for AWS provider [(#10858)](https://github.com/prowler-cloud/prowler/pull/10858) +- Per-provider scan configuration schema with bounds validation that drops out-of-range values with a warning on config load [(#11518)](https://github.com/prowler-cloud/prowler/pull/11518) ### 🔄 Changed From 1673bdf0a69f4eccdb7059aad599cb4979b1a410 Mon Sep 17 00:00:00 2001 From: pedrooot Date: Thu, 18 Jun 2026 13:04:44 +0200 Subject: [PATCH 4/7] chore(revision): solve comments --- docs/developer-guide/configurable-checks.mdx | 14 ++-- prowler/config/scan_config_schema.py | 12 +++- prowler/config/schema/aws.py | 55 ++++---------- prowler/config/schema/azure.py | 18 ++--- prowler/config/schema/cloudflare.py | 6 ++ prowler/config/schema/mongodbatlas.py | 6 ++ prowler/config/schema/validator.py | 5 ++ prowler/config/schema/validators.py | 71 +++++++++++++++++++ prowler/config/schema/vercel.py | 7 ++ tests/config/schema/aws_schema_test.py | 14 ++-- tests/config/schema/bounds_test.py | 2 +- .../config/schema/loader_integration_test.py | 3 +- 12 files changed, 142 insertions(+), 71 deletions(-) create mode 100644 prowler/config/schema/validators.py diff --git a/docs/developer-guide/configurable-checks.mdx b/docs/developer-guide/configurable-checks.mdx index fb0c6ffcd09..aa209e5ac31 100644 --- a/docs/developer-guide/configurable-checks.mdx +++ b/docs/developer-guide/configurable-checks.mdx @@ -40,19 +40,19 @@ When adding a new configurable check to Prowler, update the following files: # aws.awslambda_function_vpc_multi_az lambda_min_azs: 2 ``` -- **Provider Schema:** Add the typed field to the provider's Pydantic schema in `prowler/config/schema/.py`. This is required: the loader validates user configs against these schemas and the shipped `config.yaml` must round-trip with zero warnings. See [Adding a parameter to the provider schema](#adding-a-parameter-to-the-provider-schema) below. +- **Provider Schema:** Add the typed field to the provider's Pydantic schema in `prowler/config/schema/.py`. This is required: the loader validates user configs against these schemas and the shipped `config.yaml` must round-trip with zero warnings. See [Adding a Parameter to the Provider Schema](#adding-a-parameter-to-the-provider-schema) below. - **Test Fixtures:** If tests depend on this configuration, add the variable to `tests/config/fixtures/config.yaml`. - **Documentation:** Document the new variable in the list of configurable checks in `docs/tutorials/configuration_file.md`. For a complete list of checks that already support configuration, see the [Configuration File Tutorial](/user-guide/cli/tutorials/configuration_file). -## Adding a parameter to the provider schema +## Adding a Parameter to the Provider Schema Every provider has a typed Pydantic schema in `prowler/config/schema/`. When a config is loaded, `validate_provider_config` checks each user-supplied key against the schema, logs a warning, and drops any field that fails validation. The consumer's `.get(key, default)` then falls back to the built-in default. This catches typos in a value (for example, `0.2` typed as `20`, or `"medium"` for an enum that expects `"MEDIUM"`). It does NOT catch typos in a key name: `disalowed_regions` (one `l` missing) is treated as an unknown key and passes through untouched, because third-party check plugins legitimately rely on unknown keys being preserved. Reviewers should still check that any new key the YAML adds is named exactly the same as the field on the schema. -### Where to add the field +### Where to Add the Field 1. Open `prowler/config/schema/.py` (for example, `aws.py`). 2. Add a field on the provider's schema class. Always make it `Optional[...] = None` so the absence of the key is valid. @@ -60,7 +60,7 @@ This catches typos in a value (for example, `0.2` typed as `20`, or `"medium"` f If you are introducing an entirely new provider rather than a new parameter, also add an entry mapping the provider name to its schema class in `prowler/config/schema/registry.py`. The loader uses that registry to find the schema for the provider it is loading. -### Choosing the right type +### Choosing the Right Type | Value kind | Field declaration | |---|---| @@ -73,7 +73,7 @@ If you are introducing an entirely new provider rather than a new parameter, als Prefer `Literal[...]` over `str` whenever the value is one of a known set. Prefer `Field(gt=0)` over `int` whenever zero or negative would be nonsensical. The point of the schema is to catch real-world mistakes that previously passed silently. -### Custom validators (only when needed) +### Custom Validators (Only When Needed) If the value has structural rules beyond type and range, add a `field_validator`. Examples already in `aws.py`: @@ -83,7 +83,7 @@ If the value has structural rules beyond type and range, add a `field_validator` Raise `ValueError` from the validator. The framework converts the error into a warning and drops the offending key. -### Example: adding a new parameter +### Example: Adding a New Parameter Say a new check needs `max_iam_role_session_hours`, a strictly positive integer that defaults to 12 in code. @@ -105,7 +105,7 @@ Say a new check needs `max_iam_role_session_hours`, a strictly positive integer - one test for a valid value that round-trips, - one test for an invalid value (zero, negative, wrong type) that is dropped. -### What the loader guarantees +### What the Loader Guarantees - **Unknown keys pass through.** Third-party check plugins can introduce arbitrary keys without schema edits; they will not be filtered. - **Invalid values never crash the run.** They produce a single warning per field and the key is dropped. diff --git a/prowler/config/scan_config_schema.py b/prowler/config/scan_config_schema.py index d4dd694e04d..ac00250c78a 100644 --- a/prowler/config/scan_config_schema.py +++ b/prowler/config/scan_config_schema.py @@ -27,7 +27,17 @@ def _format_loc(loc: tuple) -> str: - """Render a Pydantic error location as `key[idx].nested`.""" + """Render a Pydantic error location as a dot-separated path. + + Integer elements (array indices) are formatted as `[idx]` appended to the + previous component. String elements are joined with dots. An empty location + is rendered as ``. + + Examples: + ("aws", "regions", 0) -> "aws.regions[0]" + ("aws", "threshold") -> "aws.threshold" + () -> "" + """ parts: list[str] = [] for piece in loc: if isinstance(piece, int): diff --git a/prowler/config/schema/aws.py b/prowler/config/schema/aws.py index 7f61e5ad11d..1a44bb9bcc7 100644 --- a/prowler/config/schema/aws.py +++ b/prowler/config/schema/aws.py @@ -12,12 +12,16 @@ (`min_kinesis_stream_retention_hours = 99999`). """ -from ipaddress import ip_network from typing import Annotated, Literal, Optional from pydantic import AfterValidator, Field from prowler.config.schema.base import ProviderConfigBase +from prowler.config.schema.validators import ( + make_dotted_version_validator, + validate_ip_networks, + validate_port_range, +) # ---- Reusable constants ----------------------------------------------------- @@ -39,6 +43,7 @@ 400, 545, 731, + 1096, 1827, 2192, 2557, @@ -63,6 +68,7 @@ 400, 545, 731, + 1096, 1827, 2192, 2557, @@ -75,13 +81,13 @@ # ---- Custom validators ------------------------------------------------------ -def _validate_port_range(v: Optional[list[int]]) -> Optional[list[int]]: - if v is None: - return v - for port in v: - if not 1 <= port <= 65535: - raise ValueError(f"port {port} is outside the valid range 1..65535") - return v +# Reusable validators shared across providers (see schema/validators.py). +_validate_port_range = validate_port_range +_validate_trusted_ips = validate_ip_networks +# "1.4.0" style strings (used by Fargate platform versions). +_validate_semver = make_dotted_version_validator(3, 3) +# "1.28" style strings (EKS minor versions). +_validate_eks_minor = make_dotted_version_validator(2, 2) def _validate_account_ids(v: Optional[list[str]]) -> Optional[list[str]]: @@ -95,39 +101,6 @@ def _validate_account_ids(v: Optional[list[str]]) -> Optional[list[str]]: return v -def _validate_trusted_ips(v: Optional[list[str]]) -> Optional[list[str]]: - if v is None: - return v - for entry in v: - try: - ip_network(entry, strict=False) - except ValueError as exc: - raise ValueError( - f"trusted_ips entry {entry!r} is not a valid IP or CIDR ({exc})" - ) from exc - return v - - -def _validate_semver(v: Optional[str]) -> Optional[str]: - """Accept "1.4.0" style strings (used by Fargate platform versions).""" - if v is None: - return v - parts = v.split(".") - if len(parts) != 3 or not all(p.isdigit() for p in parts): - raise ValueError(f"{v!r} is not a valid semantic version (expected X.Y.Z)") - return v - - -def _validate_eks_minor(v: Optional[str]) -> Optional[str]: - """Accept "1.28" style strings (EKS minor versions).""" - if v is None: - return v - parts = v.split(".") - if len(parts) != 2 or not all(p.isdigit() for p in parts): - raise ValueError(f"{v!r} is not a valid EKS version (expected X.Y)") - return v - - # ---- Nested models ---------------------------------------------------------- diff --git a/prowler/config/schema/azure.py b/prowler/config/schema/azure.py index 26953d80297..75954c602b9 100644 --- a/prowler/config/schema/azure.py +++ b/prowler/config/schema/azure.py @@ -9,20 +9,12 @@ from pydantic import AfterValidator, Field from prowler.config.schema.base import ProviderConfigBase +from prowler.config.schema.validators import make_dotted_version_validator - -def _validate_dotted_version(v: Optional[str]) -> Optional[str]: - """Accept ``"8.2"``, ``"3.12"``, ``"17"`` style version strings. - - Used by App Service language version fields where the upstream APIs - accept either ``MAJOR`` or ``MAJOR.MINOR`` notation. - """ - if v is None: - return v - parts = v.split(".") - if not (1 <= len(parts) <= 2) or not all(p.isdigit() for p in parts): - raise ValueError(f"{v!r} is not a valid version (expected 'X' or 'X.Y')") - return v +# Accept "8.2", "3.12", "17" style version strings. Used by App Service +# language version fields where the upstream APIs accept either MAJOR or +# MAJOR.MINOR notation. +_validate_dotted_version = make_dotted_version_validator(1, 2) class AzureProviderConfig(ProviderConfigBase): diff --git a/prowler/config/schema/cloudflare.py b/prowler/config/schema/cloudflare.py index 914aa754aa4..417092d6df4 100644 --- a/prowler/config/schema/cloudflare.py +++ b/prowler/config/schema/cloudflare.py @@ -8,6 +8,12 @@ class CloudflareProviderConfig(ProviderConfigBase): + """Cloudflare provider configuration schema. + + Defines optional configuration parameters for Cloudflare security checks, + including API retry behavior. + """ + max_retries: Optional[int] = Field( default=None, ge=0, diff --git a/prowler/config/schema/mongodbatlas.py b/prowler/config/schema/mongodbatlas.py index ab8b9a79d67..552e0a7bbd9 100644 --- a/prowler/config/schema/mongodbatlas.py +++ b/prowler/config/schema/mongodbatlas.py @@ -8,6 +8,12 @@ class MongoDBAtlasProviderConfig(ProviderConfigBase): + """MongoDB Atlas provider configuration schema. + + Defines optional configuration parameters for MongoDB Atlas security checks, + including service account secret validity constraints. + """ + max_service_account_secret_validity_hours: Optional[int] = Field( default=None, ge=1, diff --git a/prowler/config/schema/validator.py b/prowler/config/schema/validator.py index c2acb642e99..8113302855f 100644 --- a/prowler/config/schema/validator.py +++ b/prowler/config/schema/validator.py @@ -49,6 +49,11 @@ def validate_provider_config( ) cleaned = {k: v for k, v in raw.items() if k not in bad_keys} + # Retry validation with the cleaned dict. Dropping invalid keys handles + # common field-level mismatches, but revalidation can still fail due to + # higher-level structural constraints (e.g. nested validation errors not + # captured in the top-level bad_keys). In that case, log and return the + # cleaned dict so consumers fall back to their own defaults. try: model = schema_cls.model_validate(cleaned) return model.model_dump(exclude_unset=True) diff --git a/prowler/config/schema/validators.py b/prowler/config/schema/validators.py new file mode 100644 index 00000000000..3c87f532c70 --- /dev/null +++ b/prowler/config/schema/validators.py @@ -0,0 +1,71 @@ +"""Reusable field validators shared across provider config schemas. + +These are factored out so multiple providers can reuse the same validation +logic (version strings, port ranges, IP/CIDR entries) instead of duplicating +it per schema. Each validator accepts ``None`` so optional fields stay valid +when the key is absent. +""" + +from ipaddress import ip_network +from typing import Callable, Optional + +_VERSION_PART_LABELS = ("X", "Y", "Z", "W") + + +def make_dotted_version_validator( + min_parts: int, max_parts: int +) -> Callable[[Optional[str]], Optional[str]]: + """Build a validator for dotted numeric version strings. + + The returned validator accepts ``None`` and strings made of between + ``min_parts`` and ``max_parts`` dot-separated numeric components. Anything + else raises ``ValueError``. + + Examples: + ``make_dotted_version_validator(3, 3)`` accepts ``"1.4.0"`` (semver). + ``make_dotted_version_validator(2, 2)`` accepts ``"1.28"`` (EKS minor). + ``make_dotted_version_validator(1, 2)`` accepts ``"17"`` or ``"8.2"``. + """ + if min_parts == max_parts: + expected = ".".join(_VERSION_PART_LABELS[:min_parts]) + else: + expected = " or ".join( + f"'{'.'.join(_VERSION_PART_LABELS[:n])}'" + for n in range(min_parts, max_parts + 1) + ) + + def _validate(v: Optional[str]) -> Optional[str]: + if v is None: + return v + parts = v.split(".") + if not (min_parts <= len(parts) <= max_parts) or not all( + p.isdigit() for p in parts + ): + raise ValueError(f"{v!r} is not a valid version (expected {expected})") + return v + + return _validate + + +def validate_port_range(v: Optional[list[int]]) -> Optional[list[int]]: + """Reject ports outside the valid ``1..65535`` range.""" + if v is None: + return v + for port in v: + if not 1 <= port <= 65535: + raise ValueError(f"port {port} is outside the valid range 1..65535") + return v + + +def validate_ip_networks(v: Optional[list[str]]) -> Optional[list[str]]: + """Reject entries that are not a valid IP address or CIDR network.""" + if v is None: + return v + for entry in v: + try: + ip_network(entry, strict=False) + except ValueError as exc: + raise ValueError( + f"entry {entry!r} is not a valid IP or CIDR ({exc})" + ) from exc + return v diff --git a/prowler/config/schema/vercel.py b/prowler/config/schema/vercel.py index 69e5511a833..2e466e20498 100644 --- a/prowler/config/schema/vercel.py +++ b/prowler/config/schema/vercel.py @@ -8,6 +8,13 @@ class VercelProviderConfig(ProviderConfigBase): + """Vercel provider configuration schema. + + Defines optional configuration parameters for Vercel security checks, + including deployment branch policies, credential staleness thresholds, + RBAC ownership limits, and secret detection patterns. + """ + stable_branches: Optional[list[str]] = Field( default=None, description="Branches considered stable for production deployments.", diff --git a/tests/config/schema/aws_schema_test.py b/tests/config/schema/aws_schema_test.py index 9bc7e02d49c..ad08e84e3be 100644 --- a/tests/config/schema/aws_schema_test.py +++ b/tests/config/schema/aws_schema_test.py @@ -185,14 +185,14 @@ def test_true_and_false_round_trip(self, key): def test_yaml_style_boolean_coercion(self): # YAML can produce Python str "true"/"yes" if the user quoted it. - # Pydantic v2 will refuse string booleans by default. Verify it is - # dropped, not silently treated as True (which would be dangerous - # for verify_premium_support_plans). + # Pydantic v2 deterministically coerces "yes"/"no"/"true"/"false" to a + # real bool in lax mode, so the value is normalized rather than passed + # through as a string (which would be dangerous for + # verify_premium_support_plans). out = _validate({"verify_premium_support_plans": "yes"}) - # Pydantic actually DOES coerce "yes"/"no"/"true"/"false" in lax mode. - # We accept either outcome but require it to be a real bool. - if "verify_premium_support_plans" in out: - assert isinstance(out["verify_premium_support_plans"], bool) + assert "verify_premium_support_plans" in out + assert isinstance(out["verify_premium_support_plans"], bool) + assert out["verify_premium_support_plans"] is True class Test_AWS_Full_Default_Config_Round_Trips: diff --git a/tests/config/schema/bounds_test.py b/tests/config/schema/bounds_test.py index 4235a81582a..0e5cad6056f 100644 --- a/tests/config/schema/bounds_test.py +++ b/tests/config/schema/bounds_test.py @@ -15,7 +15,7 @@ from prowler.config.scan_config_schema import validate_scan_config -def _has_error_for(errors, path_substr: str) -> bool: +def _has_error_for(errors: list[dict], path_substr: str) -> bool: return any(path_substr in e["path"] for e in errors) diff --git a/tests/config/schema/loader_integration_test.py b/tests/config/schema/loader_integration_test.py index c3337f44478..fa995fb9dfe 100644 --- a/tests/config/schema/loader_integration_test.py +++ b/tests/config/schema/loader_integration_test.py @@ -5,6 +5,7 @@ import logging import os import pathlib +from typing import Callable import pytest @@ -12,7 +13,7 @@ @pytest.fixture -def write_config(tmp_path): +def write_config(tmp_path: pathlib.Path) -> Callable[[str], str]: def _write(content: str) -> str: path = tmp_path / "config.yaml" path.write_text(content) From ff7ec1f8b38a4b7ee95929324b0dfb2c4f373240 Mon Sep 17 00:00:00 2001 From: pedrooot Date: Thu, 18 Jun 2026 13:13:11 +0200 Subject: [PATCH 5/7] chore(revision): solve comments --- docs/developer-guide/configurable-checks.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/configurable-checks.mdx b/docs/developer-guide/configurable-checks.mdx index aa209e5ac31..d7b1089060a 100644 --- a/docs/developer-guide/configurable-checks.mdx +++ b/docs/developer-guide/configurable-checks.mdx @@ -48,7 +48,7 @@ For a complete list of checks that already support configuration, see the [Confi ## Adding a Parameter to the Provider Schema -Every provider has a typed Pydantic schema in `prowler/config/schema/`. When a config is loaded, `validate_provider_config` checks each user-supplied key against the schema, logs a warning, and drops any field that fails validation. The consumer's `.get(key, default)` then falls back to the built-in default. +Most providers have a typed Pydantic schema in `prowler/config/schema/`, registered in `prowler/config/schema/registry.py`. When a config is loaded and the provider has a registered schema, `validate_provider_config` checks each user-supplied key against it, logs a warning, and drops any field that fails validation. The consumer's `.get(key, default)` then falls back to the built-in default. Providers without a registered schema are passed through unchanged. This catches typos in a value (for example, `0.2` typed as `20`, or `"medium"` for an enum that expects `"MEDIUM"`). It does NOT catch typos in a key name: `disalowed_regions` (one `l` missing) is treated as an unknown key and passes through untouched, because third-party check plugins legitimately rely on unknown keys being preserved. Reviewers should still check that any new key the YAML adds is named exactly the same as the field on the schema. From edcffcaf442bdc3931bb8b80c01b028c865f000d Mon Sep 17 00:00:00 2001 From: pedrooot Date: Tue, 23 Jun 2026 10:54:57 +0200 Subject: [PATCH 6/7] feat(config): add compliance guardrails for the SDK config --- .../alibabacloud/cis_2.0_alibabacloud.json | 24 + .../prowler_threatscore_alibabacloud.json | 16 + .../aws/asd_essential_eight_aws.json | 24 + .../aws_account_security_onboarding_aws.json | 192 +++++++ .../aws/aws_ai_security_framework_aws.json | 40 +- ...ndational_security_best_practices_aws.json | 79 +++ ...aws_foundational_technical_review_aws.json | 8 + ...itected_framework_security_pillar_aws.json | 30 ++ prowler/compliance/aws/c5_aws.json | 238 +++++++++ prowler/compliance/aws/ccc_aws.json | 84 +++ prowler/compliance/aws/cis_1.4_aws.json | 30 ++ prowler/compliance/aws/cis_1.5_aws.json | 38 ++ prowler/compliance/aws/cis_2.0_aws.json | 38 ++ prowler/compliance/aws/cis_3.0_aws.json | 38 ++ prowler/compliance/aws/cis_4.0_aws.json | 38 ++ prowler/compliance/aws/cis_5.0_aws.json | 38 ++ prowler/compliance/aws/cis_6.0_aws.json | 38 ++ prowler/compliance/aws/cisa_aws.json | 28 + prowler/compliance/aws/ens_rd2022_aws.json | 136 +++++ .../aws/fedramp_20x_ksi_low_aws.json | 38 ++ .../aws/fedramp_low_revision_4_aws.json | 80 +++ .../aws/fedramp_moderate_revision_4_aws.json | 310 +++++++++++ prowler/compliance/aws/ffiec_aws.json | 156 ++++++ prowler/compliance/aws/gdpr_aws.json | 16 + .../aws/gxp_21_cfr_part_11_aws.json | 14 + .../compliance/aws/gxp_eu_annex_11_aws.json | 32 ++ prowler/compliance/aws/hipaa_aws.json | 126 +++++ prowler/compliance/aws/iso27001_2013_aws.json | 68 +++ prowler/compliance/aws/iso27001_2022_aws.json | 96 ++++ .../compliance/aws/kisa_isms_p_2023_aws.json | 114 +++++ .../aws/kisa_isms_p_2023_korean_aws.json | 114 +++++ prowler/compliance/aws/mitre_attack_aws.json | 292 +++++++++++ prowler/compliance/aws/nis2_aws.json | 54 ++ .../aws/nist_800_171_revision_2_aws.json | 226 ++++++++ .../aws/nist_800_53_revision_4_aws.json | 244 +++++++++ .../aws/nist_800_53_revision_5_aws.json | 482 ++++++++++++++++++ prowler/compliance/aws/nist_csf_1.1_aws.json | 266 ++++++++++ prowler/compliance/aws/nist_csf_2.0_aws.json | 171 +++++++ prowler/compliance/aws/pci_3.2.1_aws.json | 88 ++++ prowler/compliance/aws/pci_4.0_aws.json | 155 ++++++ .../aws/prowler_threatscore_aws.json | 38 ++ .../aws/rbi_cyber_security_framework_aws.json | 8 + .../compliance/aws/secnumcloud_3.2_aws.json | 125 +++++ prowler/compliance/aws/soc2_aws.json | 138 +++++ prowler/compliance/azure/c5_azure.json | 77 +++ prowler/compliance/azure/ccc_azure.json | 29 ++ prowler/compliance/azure/cis_4.0_azure.json | 10 + prowler/compliance/azure/cis_5.0_azure.json | 10 + prowler/compliance/azure/hipaa_azure.json | 22 + prowler/compliance/azure/nis2_azure.json | 33 ++ .../azure/secnumcloud_3.2_azure.json | 19 + prowler/compliance/azure/soc2_azure.json | 22 + prowler/compliance/csa_ccm_4.0.json | 213 +++++++- prowler/compliance/dora.json | 155 +++++- prowler/compliance/gcp/ccc_gcp.json | 22 + .../kubernetes/cis_1.10_kubernetes.json | 8 + .../kubernetes/cis_1.11_kubernetes.json | 8 + .../kubernetes/cis_1.12_kubernetes.json | 8 + .../kubernetes/cis_1.8_kubernetes.json | 8 + .../kubernetes/pci_4.0_kubernetes.json | 32 ++ .../prowler_threatscore_kubernetes.json | 8 + prowler/lib/check/compliance_config_eval.py | 220 ++++++++ prowler/lib/check/compliance_models.py | 38 +- .../asd_essential_eight.py | 21 +- .../asd_essential_eight_aws.py | 20 +- .../aws_well_architected.py | 19 +- prowler/lib/outputs/compliance/c5/c5.py | 21 +- prowler/lib/outputs/compliance/c5/c5_aws.py | 20 +- prowler/lib/outputs/compliance/c5/c5_azure.py | 20 +- prowler/lib/outputs/compliance/c5/c5_gcp.py | 20 +- prowler/lib/outputs/compliance/ccc/ccc.py | 21 +- prowler/lib/outputs/compliance/ccc/ccc_aws.py | 20 +- .../lib/outputs/compliance/ccc/ccc_azure.py | 20 +- prowler/lib/outputs/compliance/ccc/ccc_gcp.py | 20 +- prowler/lib/outputs/compliance/cis/cis.py | 25 +- .../compliance/cis/cis_alibabacloud.py | 19 +- prowler/lib/outputs/compliance/cis/cis_aws.py | 22 +- .../lib/outputs/compliance/cis/cis_azure.py | 19 +- prowler/lib/outputs/compliance/cis/cis_gcp.py | 19 +- .../lib/outputs/compliance/cis/cis_github.py | 19 +- .../compliance/cis/cis_googleworkspace.py | 19 +- .../outputs/compliance/cis/cis_kubernetes.py | 19 +- .../lib/outputs/compliance/cis/cis_m365.py | 19 +- .../outputs/compliance/cis/cis_oraclecloud.py | 19 +- .../cisa_scuba/cisa_scuba_googleworkspace.py | 19 +- prowler/lib/outputs/compliance/ens/ens.py | 21 +- prowler/lib/outputs/compliance/ens/ens_aws.py | 20 +- .../lib/outputs/compliance/ens/ens_azure.py | 20 +- prowler/lib/outputs/compliance/ens/ens_gcp.py | 20 +- .../lib/outputs/compliance/generic/generic.py | 24 +- .../compliance/generic/generic_table.py | 28 +- .../compliance/iso27001/iso27001_aws.py | 19 +- .../compliance/iso27001/iso27001_azure.py | 19 +- .../compliance/iso27001/iso27001_gcp.py | 19 +- .../iso27001/iso27001_kubernetes.py | 19 +- .../compliance/iso27001/iso27001_m365.py | 19 +- .../compliance/iso27001/iso27001_nhn.py | 19 +- .../compliance/kisa_ismsp/kisa_ismsp.py | 21 +- .../compliance/kisa_ismsp/kisa_ismsp_aws.py | 20 +- .../compliance/mitre_attack/mitre_attack.py | 21 +- .../mitre_attack/mitre_attack_aws.py | 20 +- .../mitre_attack/mitre_attack_azure.py | 20 +- .../mitre_attack/mitre_attack_gcp.py | 20 +- .../prowler_threatscore.py | 26 +- .../prowler_threatscore_alibaba.py | 20 +- .../prowler_threatscore_aws.py | 20 +- .../prowler_threatscore_azure.py | 20 +- .../prowler_threatscore_gcp.py | 20 +- .../prowler_threatscore_kubernetes.py | 20 +- .../prowler_threatscore_m365.py | 20 +- .../compliance/universal/ocsf_compliance.py | 28 +- .../compliance/universal/universal_table.py | 53 +- ...compliance_config_constraint_model_test.py | 113 ++++ .../lib/check/compliance_config_eval_test.py | 240 +++++++++ ...ompliance_config_requirements_data_test.py | 161 ++++++ .../cis/cis_aws_config_requirements_test.py | 89 ++++ .../cis/cis_azure_config_requirements_test.py | 75 +++ .../ens/ens_aws_config_requirements_test.py | 61 +++ ...csf_compliance_config_requirements_test.py | 191 +++++++ ...niversal_table_config_requirements_test.py | 90 ++++ 120 files changed, 7467 insertions(+), 139 deletions(-) create mode 100644 prowler/lib/check/compliance_config_eval.py create mode 100644 tests/lib/check/compliance_config_constraint_model_test.py create mode 100644 tests/lib/check/compliance_config_eval_test.py create mode 100644 tests/lib/check/compliance_config_requirements_data_test.py create mode 100644 tests/lib/outputs/compliance/cis/cis_aws_config_requirements_test.py create mode 100644 tests/lib/outputs/compliance/cis/cis_azure_config_requirements_test.py create mode 100644 tests/lib/outputs/compliance/ens/ens_aws_config_requirements_test.py create mode 100644 tests/lib/outputs/compliance/universal/ocsf_compliance_config_requirements_test.py create mode 100644 tests/lib/outputs/compliance/universal/universal_table_config_requirements_test.py diff --git a/prowler/compliance/alibabacloud/cis_2.0_alibabacloud.json b/prowler/compliance/alibabacloud/cis_2.0_alibabacloud.json index 7cda08efbb8..1cac54b318c 100644 --- a/prowler/compliance/alibabacloud/cis_2.0_alibabacloud.json +++ b/prowler/compliance/alibabacloud/cis_2.0_alibabacloud.json @@ -109,6 +109,14 @@ ], "Checks": [ "ram_user_console_access_unused" + ], + "ConfigRequirements": [ + { + "Check": "ram_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + } ] }, { @@ -841,6 +849,14 @@ ], "Checks": [ "sls_logstore_retention_period" + ], + "ConfigRequirements": [ + { + "Check": "sls_logstore_retention_period", + "ConfigKey": "min_log_retention_days", + "Operator": "gte", + "Value": 365 + } ] }, { @@ -1353,6 +1369,14 @@ ], "Checks": [ "rds_instance_sql_audit_retention" + ], + "ConfigRequirements": [ + { + "Check": "rds_instance_sql_audit_retention", + "ConfigKey": "min_rds_audit_retention_days", + "Operator": "gte", + "Value": 180 + } ] }, { diff --git a/prowler/compliance/alibabacloud/prowler_threatscore_alibabacloud.json b/prowler/compliance/alibabacloud/prowler_threatscore_alibabacloud.json index ca7a030dc20..9f16b6cc4dd 100644 --- a/prowler/compliance/alibabacloud/prowler_threatscore_alibabacloud.json +++ b/prowler/compliance/alibabacloud/prowler_threatscore_alibabacloud.json @@ -47,6 +47,14 @@ "Checks": [ "ram_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "ram_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + } + ], "Attributes": [ { "Title": "Inactive users disabled for console access", @@ -695,6 +703,14 @@ "Checks": [ "rds_instance_sql_audit_retention" ], + "ConfigRequirements": [ + { + "Check": "rds_instance_sql_audit_retention", + "ConfigKey": "min_rds_audit_retention_days", + "Operator": "gte", + "Value": 180 + } + ], "Attributes": [ { "Title": "RDS SQL audit retention configured", diff --git a/prowler/compliance/aws/asd_essential_eight_aws.json b/prowler/compliance/aws/asd_essential_eight_aws.json index 00b39817e35..dd39c442681 100644 --- a/prowler/compliance/aws/asd_essential_eight_aws.json +++ b/prowler/compliance/aws/asd_essential_eight_aws.json @@ -13,6 +13,14 @@ "config_recorder_all_regions_enabled", "inspector2_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "1 Patch applications", @@ -260,6 +268,14 @@ "config_recorder_all_regions_enabled", "inspector2_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "2 Patch operating systems", @@ -742,6 +758,14 @@ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "4 Restrict administrative privileges", diff --git a/prowler/compliance/aws/aws_account_security_onboarding_aws.json b/prowler/compliance/aws/aws_account_security_onboarding_aws.json index 1d038537f00..1910bac2236 100644 --- a/prowler/compliance/aws/aws_account_security_onboarding_aws.json +++ b/prowler/compliance/aws/aws_account_security_onboarding_aws.json @@ -37,6 +37,26 @@ "guardduty_is_enabled", "accessanalyzer_enabled", "macie_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -259,6 +279,20 @@ "Checks": [ "guardduty_is_enabled", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -514,6 +548,14 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -530,6 +572,20 @@ "securityhub_enabled", "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -666,6 +722,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -680,6 +744,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -694,6 +766,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -708,6 +788,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -722,6 +810,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -736,6 +832,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -762,6 +866,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -777,6 +889,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_centrally_managed" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -792,6 +912,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -807,6 +935,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -822,6 +958,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -837,6 +981,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -852,6 +1004,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -867,6 +1027,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -882,6 +1050,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -897,6 +1073,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -912,6 +1096,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/aws_ai_security_framework_aws.json b/prowler/compliance/aws/aws_ai_security_framework_aws.json index c6a0a8504bd..9b87f7464d4 100644 --- a/prowler/compliance/aws/aws_ai_security_framework_aws.json +++ b/prowler/compliance/aws/aws_ai_security_framework_aws.json @@ -404,6 +404,14 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -860,6 +868,20 @@ "guardduty_lambda_protection_enabled", "guardduty_rds_protection_enabled", "guardduty_ec2_malware_protection_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_delegated_admin_enabled_all_regions", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -894,6 +916,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -964,6 +994,14 @@ "Checks": [ "config_recorder_all_regions_enabled", "config_recorder_using_aws_service_role" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1157,4 +1195,4 @@ "Checks": [] } ] -} \ No newline at end of file +} diff --git a/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json b/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json index cea7ad16558..f38ee93d28f 100644 --- a/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json +++ b/prowler/compliance/aws/aws_foundational_security_best_practices_aws.json @@ -12,6 +12,14 @@ "Checks": [ "acm_certificates_expiration_check" ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_expiration_check", + "ConfigKey": "days_to_expire_threshold", + "Operator": "gte", + "Value": 30 + } + ], "Attributes": [ { "ItemId": "ACM.1", @@ -29,6 +37,17 @@ "Checks": [ "acm_certificates_with_secure_key_algorithms" ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "ItemId": "ACM.2", @@ -777,6 +796,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "Config.1", @@ -892,6 +919,14 @@ "Checks": [ "documentdb_cluster_backup_enabled" ], + "ConfigRequirements": [ + { + "Check": "documentdb_cluster_backup_enabled", + "ConfigKey": "minimum_backup_retention_period", + "Operator": "gte", + "Value": 7 + } + ], "Attributes": [ { "ItemId": "DocumentDB.2", @@ -2370,6 +2405,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "GuardDuty.1", @@ -2547,6 +2590,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + } + ], "Attributes": [ { "ItemId": "IAM.8", @@ -2635,6 +2692,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "ItemId": "IAM.22", @@ -2951,6 +3022,14 @@ "Checks": [ "neptune_cluster_backup_enabled" ], + "ConfigRequirements": [ + { + "Check": "neptune_cluster_backup_enabled", + "ConfigKey": "minimum_backup_retention_period", + "Operator": "gte", + "Value": 7 + } + ], "Attributes": [ { "ItemId": "Neptune.5", diff --git a/prowler/compliance/aws/aws_foundational_technical_review_aws.json b/prowler/compliance/aws/aws_foundational_technical_review_aws.json index 691a8ba7f13..9d8e3cfdc81 100644 --- a/prowler/compliance/aws/aws_foundational_technical_review_aws.json +++ b/prowler/compliance/aws/aws_foundational_technical_review_aws.json @@ -176,6 +176,14 @@ "iam_user_with_temporary_credentials", "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json index 94c50eb68ab..abd4b61fd02 100644 --- a/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json +++ b/prowler/compliance/aws/aws_well_architected_framework_security_pillar_aws.json @@ -585,6 +585,14 @@ "cloudtrail_multi_region_enabled", "vpc_flow_logs_enabled", "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -646,6 +654,20 @@ "guardduty_no_high_severity_findings", "macie_is_enabled", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -778,6 +800,14 @@ "guardduty_is_enabled", "vpc_flow_logs_enabled", "apigateway_restapi_authorizers_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/c5_aws.json b/prowler/compliance/aws/c5_aws.json index 88474691549..269a5ce3082 100644 --- a/prowler/compliance/aws/c5_aws.json +++ b/prowler/compliance/aws/c5_aws.json @@ -382,6 +382,14 @@ "cloudtrail_multi_region_enabled", "config_recorder_all_regions_enabled", "s3_multi_region_access_point_public_access_block" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2234,6 +2242,14 @@ "vpc_different_regions", "autoscaling_group_multiple_az", "storagegateway_gateway_fault_tolerant" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2261,6 +2277,14 @@ "organizations_scp_check_deny_regions", "s3_multi_region_access_point_public_access_block", "vpc_different_regions" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2308,6 +2332,14 @@ "organizations_scp_check_deny_regions", "s3_multi_region_access_point_public_access_block", "vpc_different_regions" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2978,6 +3010,14 @@ "guardduty_is_enabled", "athena_workgroup_enforce_configuration", "shield_advanced_protection_in_global_accelerators" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -3481,6 +3521,14 @@ "cloudtrail_cloudwatch_logging_enabled", "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4299,6 +4347,14 @@ "guardduty_no_high_severity_findings", "guardduty_rds_protection_enabled", "guardduty_s3_protection_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4920,6 +4976,17 @@ "elbv2_nlb_tls_termination_enabled", "transfer_server_in_transit_encryption_enabled", "kafka_cluster_mutual_tls_authentication_enabled" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -4946,6 +5013,17 @@ "elbv2_nlb_tls_termination_enabled", "transfer_server_in_transit_encryption_enabled", "kafka_cluster_mutual_tls_authentication_enabled" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -5220,6 +5298,14 @@ "rds_instance_default_admin", "accessanalyzer_enabled", "efs_access_point_enforce_user_identity" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5737,6 +5823,14 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6100,6 +6194,17 @@ "cloudfront_distributions_origin_traffic_encrypted", "glue_development_endpoints_job_bookmark_encryption_enabled", "cloudtrail_kms_encryption_enabled" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -6196,6 +6301,17 @@ "elb_ssl_listeners_use_acm_certificate", "iam_no_expired_server_certificates_stored", "rds_instance_certificate_expiration" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -6307,6 +6423,17 @@ "elb_ssl_listeners_use_acm_certificate", "iam_no_expired_server_certificates_stored", "rds_instance_certificate_expiration" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -6393,6 +6520,14 @@ "sns_topics_not_publicly_accessible", "sqs_queues_not_publicly_accessible", "vpc_peering_routing_tables_with_least_privilege" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6412,6 +6547,14 @@ "ec2_instance_profile_attached", "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6587,6 +6730,17 @@ "kms_cmk_not_multi_region", "kms_key_not_publicly_accessible", "ec2_ebs_volume_encryption" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -6809,6 +6963,17 @@ "secretsmanager_not_publicly_accessible", "secretsmanager_secret_rotated_periodically", "secretsmanager_secret_unused" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -6842,6 +7007,17 @@ ], "Checks": [ "acm_certificates_with_secure_key_algorithms" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -6915,6 +7091,17 @@ "secretsmanager_secret_rotated_periodically", "secretsmanager_secret_unused", "acm_certificates_with_secure_key_algorithms" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -6937,6 +7124,17 @@ "secretsmanager_secret_rotated_periodically", "secretsmanager_secret_unused", "acm_certificates_with_secure_key_algorithms" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -8042,6 +8240,14 @@ "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events", "cloudtrail_log_file_validation_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -8810,6 +9016,14 @@ "guardduty_is_enabled", "cloudtrail_log_file_validation_enabled", "ssmincidents_enabled_with_plans" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -9732,6 +9946,14 @@ "accessanalyzer_enabled_without_findings", "cloudfront_distributions_s3_origin_access_control", "cloudtrail_logs_s3_bucket_access_logging_enabled" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -10367,6 +10589,14 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -10457,6 +10687,14 @@ "ec2_instance_profile_attached", "iam_role_cross_account_readonlyaccess_policy", "iam_securityaudit_role_created" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/ccc_aws.json b/prowler/compliance/aws/ccc_aws.json index d1a20ee3d06..f6f3361fbee 100644 --- a/prowler/compliance/aws/ccc_aws.json +++ b/prowler/compliance/aws/ccc_aws.json @@ -272,6 +272,17 @@ "acm_certificates_expiration_check", "acm_certificates_with_secure_key_algorithms", "acm_certificates_transparency_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -791,6 +802,17 @@ ], "Checks": [ "acm_certificates_with_secure_key_algorithms" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -1501,6 +1523,14 @@ "iam_policy_no_full_access_to_kms", "iam_policy_no_full_access_to_cloudtrail", "iam_policy_attached_only_to_group_or_roles" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1663,6 +1693,14 @@ "cloudwatch_changes_to_network_route_tables_alarm_configured", "cloudwatch_changes_to_vpcs_alarm_configured", "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1788,6 +1826,14 @@ "cloudtrail_threat_detection_enumeration", "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4308,6 +4354,14 @@ ], "Checks": [ "acm_certificates_expiration_check" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_expiration_check", + "ConfigKey": "days_to_expire_threshold", + "Operator": "gte", + "Value": 30 + } ] }, { @@ -6173,6 +6227,20 @@ "Checks": [ "iam_user_accesskey_unused", "iam_user_console_access_unused" + ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + } ] }, { @@ -6269,6 +6337,14 @@ "cloudwatch_log_metric_filter_root_usage", "cloudwatch_log_metric_filter_sign_in_without_mfa", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6371,6 +6447,14 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/cis_1.4_aws.json b/prowler/compliance/aws/cis_1.4_aws.json index 3efc29fd5b1..b373a04665c 100644 --- a/prowler/compliance/aws/cis_1.4_aws.json +++ b/prowler/compliance/aws/cis_1.4_aws.json @@ -75,6 +75,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -265,6 +279,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -736,6 +758,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "3 Logging", diff --git a/prowler/compliance/aws/cis_1.5_aws.json b/prowler/compliance/aws/cis_1.5_aws.json index f7307bb7f4e..6a60d786a22 100644 --- a/prowler/compliance/aws/cis_1.5_aws.json +++ b/prowler/compliance/aws/cis_1.5_aws.json @@ -75,6 +75,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -265,6 +279,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -802,6 +824,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "3 Logging", @@ -1054,6 +1084,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "4 Monitoring", diff --git a/prowler/compliance/aws/cis_2.0_aws.json b/prowler/compliance/aws/cis_2.0_aws.json index 4f8c4b2c23a..e255bd43e1d 100644 --- a/prowler/compliance/aws/cis_2.0_aws.json +++ b/prowler/compliance/aws/cis_2.0_aws.json @@ -75,6 +75,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -265,6 +279,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -802,6 +824,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "3 Logging", @@ -1054,6 +1084,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "4 Monitoring", diff --git a/prowler/compliance/aws/cis_3.0_aws.json b/prowler/compliance/aws/cis_3.0_aws.json index dd4c75a2f79..5540bc40cf3 100644 --- a/prowler/compliance/aws/cis_3.0_aws.json +++ b/prowler/compliance/aws/cis_3.0_aws.json @@ -75,6 +75,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -265,6 +279,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -756,6 +778,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "3 Logging", @@ -1008,6 +1038,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "4 Monitoring", diff --git a/prowler/compliance/aws/cis_4.0_aws.json b/prowler/compliance/aws/cis_4.0_aws.json index 0c40f8ac09f..c8787ef4096 100644 --- a/prowler/compliance/aws/cis_4.0_aws.json +++ b/prowler/compliance/aws/cis_4.0_aws.json @@ -254,6 +254,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -431,6 +445,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -750,6 +772,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "3 Logging", @@ -1234,6 +1264,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "4 Monitoring", diff --git a/prowler/compliance/aws/cis_5.0_aws.json b/prowler/compliance/aws/cis_5.0_aws.json index e870878c6b3..0c8d46e170a 100644 --- a/prowler/compliance/aws/cis_5.0_aws.json +++ b/prowler/compliance/aws/cis_5.0_aws.json @@ -232,6 +232,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -409,6 +423,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "1 Identity and Access Management", @@ -728,6 +750,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "3 Logging", @@ -1212,6 +1242,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "4 Monitoring", diff --git a/prowler/compliance/aws/cis_6.0_aws.json b/prowler/compliance/aws/cis_6.0_aws.json index 643c192b7bf..7ad62a4b658 100644 --- a/prowler/compliance/aws/cis_6.0_aws.json +++ b/prowler/compliance/aws/cis_6.0_aws.json @@ -232,6 +232,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Section": "2 Identity and Access Management", @@ -409,6 +423,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "2 Identity and Access Management", @@ -728,6 +750,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "4 Logging", @@ -1212,6 +1242,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "5 Monitoring", diff --git a/prowler/compliance/aws/cisa_aws.json b/prowler/compliance/aws/cisa_aws.json index fa8e27a0611..27b06a1ea48 100644 --- a/prowler/compliance/aws/cisa_aws.json +++ b/prowler/compliance/aws/cisa_aws.json @@ -136,6 +136,20 @@ "ec2_securitygroup_default_restrict_traffic", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_securitygroup_allow_ingress_from_internet_to_all_ports" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -367,6 +381,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/ens_rd2022_aws.json b/prowler/compliance/aws/ens_rd2022_aws.json index f6c574daef4..8f8c9127807 100644 --- a/prowler/compliance/aws/ens_rd2022_aws.json +++ b/prowler/compliance/aws/ens_rd2022_aws.json @@ -598,6 +598,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -624,6 +632,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -755,6 +771,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -781,6 +805,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -913,6 +945,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -940,6 +980,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -966,6 +1014,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1743,6 +1799,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1821,6 +1885,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1873,6 +1945,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1925,6 +2005,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1951,6 +2039,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1977,6 +2073,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2003,6 +2107,14 @@ ], "Checks": [ "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2056,6 +2168,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2082,6 +2202,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4304,6 +4432,14 @@ ], "Checks": [ "drs_job_exist" + ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json index 6c6c5005826..15763ef48ed 100644 --- a/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json +++ b/prowler/compliance/aws/fedramp_20x_ksi_low_aws.json @@ -37,6 +37,14 @@ "ssm_managed_compliant_patching", "ssm_managed_instance_compliance_association_compliant", "ssm_managed_instance_compliance_patch_compliant" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -146,6 +154,20 @@ "inspector2_active_findings_exist", "securityhub_enabled", "sns_topics_kms_encryption_at_rest_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -205,6 +227,14 @@ "resourceexplorer_indexes_found", "ssm_managed_instance_compliance_association_compliant", "trustedadvisor_premium_support_plan_subscribed" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -349,6 +379,14 @@ "config_recorder_all_regions_enabled", "inspector2_is_enabled", "resourceexplorer_indexes_found" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] } ] diff --git a/prowler/compliance/aws/fedramp_low_revision_4_aws.json b/prowler/compliance/aws/fedramp_low_revision_4_aws.json index 9824798552c..059de696754 100644 --- a/prowler/compliance/aws/fedramp_low_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_low_revision_4_aws.json @@ -46,6 +46,20 @@ "redshift_cluster_audit_logging", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -115,6 +129,20 @@ "ec2_networkacl_allow_ingress_any_port", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_networkacl_allow_ingress_any_port" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -173,6 +201,14 @@ ], "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" + ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 90 + } ] }, { @@ -198,6 +234,20 @@ "rds_instance_enhanced_monitoring_enabled", "redshift_cluster_audit_logging", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -251,6 +301,14 @@ "guardduty_is_enabled", "ssm_managed_compliant_patching", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -336,6 +394,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -373,6 +445,14 @@ "rds_instance_multi_az", "redshift_cluster_automated_snapshot", "s3_bucket_object_versioning" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json index c914a58b2c5..5ac74662be4 100644 --- a/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json +++ b/prowler/compliance/aws/fedramp_moderate_revision_4_aws.json @@ -36,6 +36,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -65,6 +79,20 @@ "redshift_cluster_audit_logging", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -82,6 +110,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -140,6 +182,20 @@ "redshift_cluster_audit_logging", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -191,6 +247,38 @@ "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" + ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_access_not_stale_to_bedrock", + "ConfigKey": "max_unused_bedrock_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_role_access_not_stale_to_bedrock", + "ConfigKey": "max_unused_bedrock_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_access_not_stale_to_sagemaker", + "ConfigKey": "max_unused_sagemaker_access_days", + "Operator": "lte", + "Value": 90 + } ] }, { @@ -371,6 +459,20 @@ "ec2_networkacl_allow_ingress_any_port", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_networkacl_allow_ingress_any_port" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -507,6 +609,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -575,6 +691,14 @@ ], "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" + ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 90 + } ] }, { @@ -631,6 +755,20 @@ "rds_instance_enhanced_monitoring_enabled", "redshift_cluster_audit_logging", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -720,6 +858,14 @@ "guardduty_is_enabled", "ssm_managed_compliant_patching", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -887,6 +1033,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -909,6 +1069,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -927,6 +1101,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -945,6 +1133,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -961,6 +1163,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -995,6 +1205,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1061,6 +1285,14 @@ "guardduty_is_enabled", "rds_instance_multi_az", "s3_bucket_object_versioning" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1279,6 +1511,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1301,6 +1541,20 @@ "guardduty_is_enabled", "redshift_cluster_audit_logging", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1328,6 +1582,20 @@ "guardduty_is_enabled", "redshift_cluster_audit_logging", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1355,6 +1623,20 @@ "guardduty_is_enabled", "redshift_cluster_audit_logging", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1382,6 +1664,20 @@ "guardduty_is_enabled", "redshift_cluster_audit_logging", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1408,6 +1704,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/ffiec_aws.json b/prowler/compliance/aws/ffiec_aws.json index 23ab8953b6c..8668c8ef2ea 100644 --- a/prowler/compliance/aws/ffiec_aws.json +++ b/prowler/compliance/aws/ffiec_aws.json @@ -37,6 +37,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -74,6 +82,20 @@ "cloudtrail_cloudwatch_logging_enabled", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -148,6 +170,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -166,6 +202,20 @@ "guardduty_is_enabled", "securityhub_enabled", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -183,6 +233,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -237,6 +301,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -254,6 +332,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -367,6 +459,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -386,6 +486,20 @@ "guardduty_is_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -404,6 +518,20 @@ "guardduty_is_enabled", "securityhub_enabled", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -823,6 +951,20 @@ "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -868,6 +1010,20 @@ "redshift_cluster_audit_logging", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/gdpr_aws.json b/prowler/compliance/aws/gdpr_aws.json index 930af1db587..a97a11e3dcd 100644 --- a/prowler/compliance/aws/gdpr_aws.json +++ b/prowler/compliance/aws/gdpr_aws.json @@ -59,6 +59,14 @@ "cloudwatch_log_metric_filter_security_group_changes", "cloudwatch_log_metric_filter_unauthorized_api_calls", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -85,6 +93,14 @@ "kms_cmk_rotation_enabled", "redshift_cluster_audit_logging", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json b/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json index 39534c4fdc6..94d9eb6f769 100644 --- a/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json +++ b/prowler/compliance/aws/gxp_21_cfr_part_11_aws.json @@ -347,6 +347,20 @@ "cloudtrail_cloudwatch_logging_enabled", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] } ] diff --git a/prowler/compliance/aws/gxp_eu_annex_11_aws.json b/prowler/compliance/aws/gxp_eu_annex_11_aws.json index 1ceb3f817bb..fdca6d17474 100644 --- a/prowler/compliance/aws/gxp_eu_annex_11_aws.json +++ b/prowler/compliance/aws/gxp_eu_annex_11_aws.json @@ -19,6 +19,14 @@ "Checks": [ "cloudtrail_multi_region_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -146,6 +154,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -238,6 +254,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -253,6 +277,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/hipaa_aws.json b/prowler/compliance/aws/hipaa_aws.json index 036489d7123..9eb243e6ccf 100644 --- a/prowler/compliance/aws/hipaa_aws.json +++ b/prowler/compliance/aws/hipaa_aws.json @@ -19,6 +19,20 @@ "Checks": [ "config_recorder_all_regions_enabled", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -102,6 +116,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -161,6 +189,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -328,6 +370,20 @@ "guardduty_is_enabled", "cloudwatch_log_metric_filter_authentication_failures", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -373,6 +429,20 @@ "cloudwatch_log_metric_filter_authentication_failures", "cloudwatch_log_metric_filter_root_usage", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -402,6 +472,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -514,6 +598,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -649,6 +747,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -756,6 +868,20 @@ "s3_bucket_secure_transport_policy", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/iso27001_2013_aws.json b/prowler/compliance/aws/iso27001_2013_aws.json index ad31ea0af47..1a81a283fdf 100644 --- a/prowler/compliance/aws/iso27001_2013_aws.json +++ b/prowler/compliance/aws/iso27001_2013_aws.json @@ -308,6 +308,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -872,6 +880,38 @@ "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" + ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_access_not_stale_to_bedrock", + "ConfigKey": "max_unused_bedrock_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_role_access_not_stale_to_bedrock", + "ConfigKey": "max_unused_bedrock_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_access_not_stale_to_sagemaker", + "ConfigKey": "max_unused_sagemaker_access_days", + "Operator": "lte", + "Value": 90 + } ] }, { @@ -1049,6 +1089,20 @@ "Checks": [ "iam_user_accesskey_unused", "iam_user_console_access_unused" + ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + } ] }, { @@ -1258,6 +1312,20 @@ "Checks": [ "iam_user_accesskey_unused", "iam_user_console_access_unused" + ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + } ] }, { diff --git a/prowler/compliance/aws/iso27001_2022_aws.json b/prowler/compliance/aws/iso27001_2022_aws.json index d47bfcf1d12..563b856317a 100644 --- a/prowler/compliance/aws/iso27001_2022_aws.json +++ b/prowler/compliance/aws/iso27001_2022_aws.json @@ -20,6 +20,14 @@ "Checks": [ "securityhub_enabled", "wellarchitected_workload_no_high_or_medium_risks" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -277,6 +285,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -331,6 +347,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -362,6 +386,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -378,6 +410,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -424,6 +464,14 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "guardduty_centrally_managed" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -472,6 +520,14 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "guardduty_centrally_managed" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -490,6 +546,14 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "guardduty_centrally_managed" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1004,6 +1068,14 @@ "organizations_account_part_of_organizations", "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1080,6 +1152,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1111,6 +1191,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1749,6 +1837,14 @@ "vpc_default_security_group_closed", "vpc_flow_logs_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json index d172e615dd9..56ac227a1da 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -1211,6 +1211,14 @@ "rds_instance_default_admin", "redshift_cluster_non_default_database_name" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -1416,6 +1424,14 @@ "iam_user_administrator_access_policy", "organizations_delegated_administrators" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -1486,6 +1502,14 @@ "ssm_documents_set_as_public", "vpc_endpoint_services_allowed_principals_trust_boundaries" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -2079,6 +2103,17 @@ "transfer_server_in_transit_encryption_enabled", "workspaces_volume_encryption_enabled" ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -2816,6 +2851,20 @@ "wafv2_webacl_rule_logging_enabled", "wafv2_webacl_with_rules" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -3313,6 +3362,47 @@ "workspaces_volume_encryption_enabled", "workspaces_vpc_2private_1public_subnets_nat" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -3705,6 +3795,14 @@ "s3_bucket_event_notifications_enabled", "trustedadvisor_errors_and_warnings" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -3823,6 +3921,14 @@ "s3_bucket_object_lock", "s3_bucket_object_versioning" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -3860,6 +3966,14 @@ "Checks": [ "drs_job_exist" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. Control Measures Requirements", diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json index 1748d96442c..e2344a1cee4 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -1211,6 +1211,14 @@ "rds_instance_default_admin", "redshift_cluster_non_default_database_name" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -1416,6 +1424,14 @@ "iam_user_administrator_access_policy", "organizations_delegated_administrators" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -1485,6 +1501,14 @@ "ssm_documents_set_as_public", "vpc_endpoint_services_allowed_principals_trust_boundaries" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -2081,6 +2105,17 @@ "transfer_server_in_transit_encryption_enabled", "workspaces_volume_encryption_enabled" ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -2819,6 +2854,20 @@ "wafv2_webacl_rule_logging_enabled", "wafv2_webacl_with_rules" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -3316,6 +3365,47 @@ "workspaces_volume_encryption_enabled", "workspaces_vpc_2private_1public_subnets_nat" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -3708,6 +3798,14 @@ "s3_bucket_event_notifications_enabled", "trustedadvisor_errors_and_warnings" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -3826,6 +3924,14 @@ "s3_bucket_object_lock", "s3_bucket_object_versioning" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -3863,6 +3969,14 @@ "Checks": [ "drs_job_exist" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Domain": "2. 보호대책 요구사항", diff --git a/prowler/compliance/aws/mitre_attack_aws.json b/prowler/compliance/aws/mitre_attack_aws.json index 3d1d5fd378f..3ac8cf04323 100644 --- a/prowler/compliance/aws/mitre_attack_aws.json +++ b/prowler/compliance/aws/mitre_attack_aws.json @@ -35,6 +35,32 @@ "awslambda_function_not_publicly_accessible", "ec2_instance_public_ip" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudEndure Disaster Recovery", @@ -200,6 +226,26 @@ "organizations_scp_check_deny_regions", "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "Amazon GuardDuty", @@ -348,6 +394,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -393,6 +447,26 @@ "guardduty_is_enabled", "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -444,6 +518,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -557,6 +639,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -634,6 +724,26 @@ "inspector2_is_enabled", "inspector2_active_findings_exist" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -821,6 +931,26 @@ "inspector2_is_enabled", "inspector2_active_findings_exist" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -984,6 +1114,14 @@ "cloudfront_distributions_https_enabled", "s3_bucket_secure_transport_policy" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudWatch", @@ -1057,6 +1195,14 @@ "ssm_document_secrets", "secretsmanager_automatic_rotation_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudHSM", @@ -1143,6 +1289,14 @@ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Network Firewall", @@ -1218,6 +1372,14 @@ "s3_bucket_default_encryption", "rds_instance_storage_encrypted" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -1264,6 +1426,20 @@ "securityhub_enabled", "macie_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -1441,6 +1617,20 @@ "s3_bucket_object_versioning", "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudEndure Disaster Recovery", @@ -1518,6 +1708,20 @@ "efs_have_backup_enabled", "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudEndure Disaster Recovery", @@ -1566,6 +1770,20 @@ "drs_job_exist", "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudEndure Disaster Recovery", @@ -1639,6 +1857,14 @@ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Shield", @@ -1686,6 +1912,14 @@ "drs_job_exist", "rds_instance_backup_enabled" ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudEndure Disaster Recovery", @@ -1743,6 +1977,20 @@ "cloudwatch_log_metric_filter_sign_in_without_mfa", "cloudwatch_log_metric_filter_unauthorized_api_calls" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS CloudWatch", @@ -1819,6 +2067,20 @@ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Config", @@ -1910,6 +2172,20 @@ "iam_policy_no_full_access_to_cloudtrail", "iam_policy_no_full_access_to_kms" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS Organizations", @@ -1993,6 +2269,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "Amazon GuardDuty", @@ -2071,6 +2355,14 @@ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "AWSService": "AWS IoT Device Defender", diff --git a/prowler/compliance/aws/nis2_aws.json b/prowler/compliance/aws/nis2_aws.json index d7f193c6c08..3a4d567d256 100644 --- a/prowler/compliance/aws/nis2_aws.json +++ b/prowler/compliance/aws/nis2_aws.json @@ -597,6 +597,14 @@ "accessanalyzer_enabled", "cloudwatch_log_metric_filter_root_usage" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "3 INCIDENT HANDLING (ARTICLE 21(2), POINT (B), OF DIRECTIVE (EU) 2022/2555)", @@ -1511,6 +1519,17 @@ "Checks": [ "acm_certificates_with_secure_key_algorithms" ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "Section": "9 CRYPTOGRAPHY (ARTICLE 21(2), POINT (H), OF DIRECTIVE (EU) 2022/2555)", @@ -1528,6 +1547,17 @@ "route53_domains_privacy_protection_enabled", "iam_no_expired_server_certificates_stored" ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "Section": "9 CRYPTOGRAPHY (ARTICLE 21(2), POINT (H), OF DIRECTIVE (EU) 2022/2555)", @@ -1645,6 +1675,14 @@ "efs_access_point_enforce_user_identity", "efs_not_publicly_accessible" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "11 ACCESS CONTROL (ARTICLE 21(2), POINTS (I) AND (J), OF DIRECTIVE (EU) 2022/2555)", @@ -1676,6 +1714,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "11 ACCESS CONTROL (ARTICLE 21(2), POINTS (I) AND (J), OF DIRECTIVE (EU) 2022/2555)", @@ -1726,6 +1772,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "11 ACCESS CONTROL (ARTICLE 21(2), POINTS (I) AND (J), OF DIRECTIVE (EU) 2022/2555)", diff --git a/prowler/compliance/aws/nist_800_171_revision_2_aws.json b/prowler/compliance/aws/nist_800_171_revision_2_aws.json index 8e28ee383da..2092e9d02c3 100644 --- a/prowler/compliance/aws/nist_800_171_revision_2_aws.json +++ b/prowler/compliance/aws/nist_800_171_revision_2_aws.json @@ -230,6 +230,20 @@ "rds_instance_integration_cloudwatch_logs", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -321,6 +335,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -344,6 +372,14 @@ "guardduty_is_enabled", "rds_instance_integration_cloudwatch_logs", "s3_bucket_server_access_logging_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -383,6 +419,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -400,6 +450,20 @@ "cloudtrail_cloudwatch_logging_enabled", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -684,6 +748,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -712,6 +790,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -729,6 +821,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -746,6 +852,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -769,6 +889,20 @@ "guardduty_is_enabled", "rds_instance_enhanced_monitoring_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -806,6 +940,20 @@ "ec2_networkacl_allow_ingress_any_port", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_networkacl_allow_ingress_any_port" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1025,6 +1173,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1044,6 +1206,20 @@ "securityhub_enabled", "ssm_managed_compliant_patching", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1061,6 +1237,20 @@ "guardduty_is_enabled", "securityhub_enabled", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1076,6 +1266,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1102,6 +1300,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1128,6 +1340,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] } ] diff --git a/prowler/compliance/aws/nist_800_53_revision_4_aws.json b/prowler/compliance/aws/nist_800_53_revision_4_aws.json index deb2a3cc254..8bd36a3910e 100644 --- a/prowler/compliance/aws/nist_800_53_revision_4_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_4_aws.json @@ -27,6 +27,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -47,6 +61,38 @@ "iam_user_access_not_stale_to_sagemaker", "iam_user_accesskey_unused", "iam_user_console_access_unused" + ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_access_not_stale_to_bedrock", + "ConfigKey": "max_unused_bedrock_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_role_access_not_stale_to_bedrock", + "ConfigKey": "max_unused_bedrock_access_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_user_access_not_stale_to_sagemaker", + "ConfigKey": "max_unused_sagemaker_access_days", + "Operator": "lte", + "Value": 90 + } ] }, { @@ -73,6 +119,20 @@ "rds_instance_integration_cloudwatch_logs", "redshift_cluster_audit_logging", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -90,6 +150,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -125,6 +199,20 @@ "redshift_cluster_audit_logging", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -270,6 +358,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -399,6 +501,20 @@ "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -421,6 +537,20 @@ "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -534,6 +664,20 @@ "guardduty_is_enabled", "rds_instance_enhanced_monitoring_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -827,6 +971,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -860,6 +1012,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1110,6 +1276,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1133,6 +1307,20 @@ "ec2_instance_imdsv2_enabled", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1155,6 +1343,20 @@ "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1177,6 +1379,20 @@ "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1194,6 +1410,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1218,6 +1448,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/nist_800_53_revision_5_aws.json b/prowler/compliance/aws/nist_800_53_revision_5_aws.json index 858df01fd92..c3fde656c41 100644 --- a/prowler/compliance/aws/nist_800_53_revision_5_aws.json +++ b/prowler/compliance/aws/nist_800_53_revision_5_aws.json @@ -220,6 +220,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -944,6 +952,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1629,6 +1645,14 @@ "Checks": [ "cloudtrail_multi_region_enabled", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1828,6 +1852,20 @@ "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1906,6 +1944,20 @@ "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2290,6 +2342,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2352,6 +2418,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2387,6 +2467,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2466,6 +2560,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2487,6 +2595,20 @@ "guardduty_is_enabled", "rds_instance_enhanced_monitoring_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2522,6 +2644,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -2904,6 +3040,14 @@ "guardduty_is_enabled", "ssm_managed_compliant_patching", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4079,6 +4223,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4095,6 +4247,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4149,6 +4309,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4184,6 +4358,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4199,6 +4387,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4270,6 +4466,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4286,6 +4496,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4303,6 +4521,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4320,6 +4546,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4336,6 +4570,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4353,6 +4595,14 @@ "Checks": [ "guardduty_is_enabled", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4369,6 +4619,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4385,6 +4643,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4401,6 +4667,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4418,6 +4692,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4435,6 +4717,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4516,6 +4806,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4558,6 +4856,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4575,6 +4881,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4591,6 +4905,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -4607,6 +4929,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5677,6 +6007,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5844,6 +6182,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5884,6 +6230,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5901,6 +6255,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5918,6 +6280,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5934,6 +6304,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5950,6 +6328,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -5982,6 +6368,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6006,6 +6400,14 @@ "rds_instance_integration_cloudwatch_logs", "redshift_cluster_audit_logging", "s3_bucket_server_access_logging_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6022,6 +6424,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6039,6 +6449,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6056,6 +6474,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6072,6 +6498,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6108,6 +6542,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6124,6 +6566,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6191,6 +6641,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6207,6 +6665,14 @@ ], "Checks": [ "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6227,6 +6693,14 @@ "cloudwatch_changes_to_network_route_tables_alarm_configured", "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -6247,6 +6721,14 @@ "cloudwatch_changes_to_network_route_tables_alarm_configured", "cloudwatch_changes_to_vpcs_alarm_configured", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/nist_csf_1.1_aws.json b/prowler/compliance/aws/nist_csf_1.1_aws.json index a55097e70c5..9921efce568 100644 --- a/prowler/compliance/aws/nist_csf_1.1_aws.json +++ b/prowler/compliance/aws/nist_csf_1.1_aws.json @@ -48,6 +48,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -99,6 +113,20 @@ "guardduty_no_high_severity_findings", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -144,6 +172,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -179,6 +221,26 @@ "cloudwatch_log_metric_filter_unauthorized_api_calls", "rds_instance_enhanced_monitoring_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -201,6 +263,20 @@ "guardduty_is_enabled", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -218,6 +294,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -243,6 +333,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -265,6 +369,20 @@ "guardduty_is_enabled", "s3_bucket_server_access_logging_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -291,6 +409,20 @@ "s3_bucket_server_access_logging_enabled", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -316,6 +448,20 @@ "guardduty_is_enabled", "guardduty_no_high_severity_findings", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -349,6 +495,14 @@ "Checks": [ "config_recorder_all_regions_enabled", "ec2_instance_managed_by_ssm" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -454,6 +608,20 @@ "guardduty_is_enabled", "securityhub_enabled", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -471,6 +639,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -488,6 +670,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -523,6 +719,26 @@ "cloudwatch_log_metric_filter_unauthorized_api_calls", "rds_instance_enhanced_monitoring_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -554,6 +770,26 @@ "cloudwatch_log_metric_filter_unauthorized_api_calls", "rds_instance_enhanced_monitoring_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -827,6 +1063,20 @@ "sagemaker_notebook_instance_without_direct_internet_access_configured", "securityhub_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -881,6 +1131,14 @@ "Checks": [ "ec2_instance_managed_by_ssm", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1035,6 +1293,14 @@ "ec2_instance_managed_by_ssm", "ssm_managed_compliant_patching", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/nist_csf_2.0_aws.json b/prowler/compliance/aws/nist_csf_2.0_aws.json index e890b085734..c06eeec11d3 100644 --- a/prowler/compliance/aws/nist_csf_2.0_aws.json +++ b/prowler/compliance/aws/nist_csf_2.0_aws.json @@ -72,6 +72,20 @@ "securityhub_enabled", "wellarchitected_workload_no_high_or_medium_risks", "servicecatalog_portfolio_shared_within_organization_only" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -322,6 +336,26 @@ "wellarchitected_workload_no_high_or_medium_risks", "organizations_delegated_administrators", "organizations_tags_policies_enabled_and_attached" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -352,6 +386,26 @@ "vpc_flow_logs_enabled", "iam_root_mfa_enabled", "iam_root_credentials_management_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -408,6 +462,20 @@ "accessanalyzer_enabled", "guardduty_no_high_severity_findings", "trustedadvisor_errors_and_warnings" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -442,6 +510,26 @@ "organizations_scp_check_deny_regions", "organizations_tags_policies_enabled_and_attached", "organizations_delegated_administrators" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -574,6 +662,14 @@ "opensearch_service_domains_encryption_at_rest_enabled", "redshift_cluster_encrypted_at_rest", "sns_topics_kms_encryption_at_rest_enabled" + ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -610,6 +706,17 @@ "iam_inline_policy_allows_privilege_escalation", "ssm_documents_set_as_public", "s3_bucket_shadow_resource_vulnerability" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -726,6 +833,14 @@ "iam_role_administratoraccess_policy", "iam_policy_no_full_access_to_cloudtrail", "iam_policy_no_full_access_to_kms" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -853,6 +968,14 @@ "iam_customer_unattached_policy_no_administrative_privileges", "accessanalyzer_enabled", "cognito_user_pool_password_policy_symbol" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1224,6 +1347,14 @@ "inspector2_active_findings_exist", "secretsmanager_automatic_rotation_enabled", "secretsmanager_secret_rotated_periodically" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1265,6 +1396,14 @@ "Checks": [ "ssmincidents_enabled_with_plans", "drs_job_exist" + ], + "ConfigRequirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1283,6 +1422,14 @@ "inspector2_is_enabled", "guardduty_is_enabled", "inspector2_active_findings_exist" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1329,6 +1476,14 @@ "vpc_flow_logs_enabled", "config_recorder_all_regions_enabled", "config_recorder_using_aws_service_role" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1540,6 +1695,14 @@ "guardduty_is_enabled", "inspector2_is_enabled", "accessanalyzer_enabled_without_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1662,6 +1825,14 @@ "guardduty_rds_protection_enabled", "guardduty_lambda_protection_enabled", "guardduty_eks_runtime_monitoring_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/pci_3.2.1_aws.json b/prowler/compliance/aws/pci_3.2.1_aws.json index c240548f6ae..ca8e968bf9f 100644 --- a/prowler/compliance/aws/pci_3.2.1_aws.json +++ b/prowler/compliance/aws/pci_3.2.1_aws.json @@ -628,6 +628,14 @@ "ssm_managed_compliant_patching", "ec2_elastic_ip_unassigned" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "2.4", @@ -643,6 +651,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "2.4.a", @@ -2413,6 +2429,14 @@ "cloudtrail_log_file_validation_enabled", "s3_bucket_cross_region_replication" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "10.5", @@ -2430,6 +2454,14 @@ "s3_bucket_object_versioning", "cloudtrail_log_file_validation_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "10.5.2", @@ -2616,6 +2648,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "11.4", @@ -2631,6 +2671,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "11.4.a", @@ -2646,6 +2694,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "11.4.b", @@ -2661,6 +2717,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "11.4.c", @@ -2676,6 +2740,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "11.5", @@ -2691,6 +2763,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "11.5.a", @@ -2706,6 +2786,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "ItemId": "11.5.b", diff --git a/prowler/compliance/aws/pci_4.0_aws.json b/prowler/compliance/aws/pci_4.0_aws.json index dc8fab91406..e21b5435568 100644 --- a/prowler/compliance/aws/pci_4.0_aws.json +++ b/prowler/compliance/aws/pci_4.0_aws.json @@ -4403,6 +4403,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "10.2.1.1: Audit logs are implemented to support the detection of anomalies and suspicious activity, and the forensic analysis of events. ", @@ -9281,6 +9289,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "10.4.1.1: Audit logs are reviewed to identify anomalies or suspicious activity. ", @@ -9363,6 +9379,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "10.4.1: Audit logs are reviewed to identify anomalies or suspicious activity. ", @@ -9459,6 +9483,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "10.4.2: Audit logs are reviewed to identify anomalies or suspicious activity. ", @@ -9551,6 +9583,14 @@ "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "10.5.1: Audit log history is retained and available for analysis. ", @@ -10179,6 +10219,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "10.6.3: Time-synchronization mechanisms support consistent time settings across all systems. ", @@ -10343,6 +10391,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "10.7.1: Failures of critical security control systems are detected, reported, and responded to promptly. ", @@ -10451,6 +10507,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "10.7.2: Failures of critical security control systems are detected, reported, and responded to promptly. ", @@ -10625,6 +10689,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "11.5.1.1: Network intrusions and unexpected file changes are detected and responded to. ", @@ -10653,6 +10725,14 @@ "Checks": [ "guardduty_is_enabled" ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "11.5.1: Network intrusions and unexpected file changes are detected and responded to. ", @@ -11445,6 +11525,14 @@ "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "3.2.1: Storage of account data is kept to a minimum. ", @@ -11567,6 +11655,14 @@ "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "3.3.1.1: Sensitive authentication data (SAD) is not stored after authorization. ", @@ -11689,6 +11785,14 @@ "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "3.3.1.3: Sensitive authentication data (SAD) is not stored after authorization. ", @@ -11811,6 +11915,14 @@ "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "3.3.2: Sensitive authentication data (SAD) is not stored after authorization. ", @@ -11933,6 +12045,14 @@ "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "3.3.3: Sensitive authentication data (SAD) is not stored after authorization. ", @@ -13573,6 +13693,17 @@ "Checks": [ "acm_certificates_with_secure_key_algorithms" ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ], "Attributes": [ { "Section": "3.7.1: Where cryptography is used to protect stored account data, key management processes and procedures covering all aspects of the key lifecycle are defined and implemented. ", @@ -15001,6 +15132,14 @@ "Checks": [ "cloudwatch_log_group_retention_policy_specific_days_enabled" ], + "ConfigRequirements": [ + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "5.3.4: Anti-malware mechanisms and processes are active, maintained, and monitored. ", @@ -22504,6 +22643,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "A3.3.1: PCI DSS is incorporated into business-as-usual (BAU) activities. ", @@ -23000,6 +23147,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Section": "A3.5.1: Suspicious events are identified and responded to. ", diff --git a/prowler/compliance/aws/prowler_threatscore_aws.json b/prowler/compliance/aws/prowler_threatscore_aws.json index 902beb8abd9..c8c093907a4 100644 --- a/prowler/compliance/aws/prowler_threatscore_aws.json +++ b/prowler/compliance/aws/prowler_threatscore_aws.json @@ -174,6 +174,20 @@ "iam_user_accesskey_unused", "iam_user_console_access_unused" ], + "ConfigRequirements": [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45 + }, + { + "Check": "iam_user_console_access_unused", + "ConfigKey": "max_console_access_days", + "Operator": "lte", + "Value": 45 + } + ], "Attributes": [ { "Title": "IAM credentials unused disabled", @@ -336,6 +350,14 @@ "Checks": [ "accessanalyzer_enabled" ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Title": "Access Analyzer enabled", @@ -1541,6 +1563,14 @@ "Checks": [ "config_recorder_all_regions_enabled" ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Title": "AWS Config is enabled", @@ -1829,6 +1859,14 @@ "Checks": [ "securityhub_enabled" ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ], "Attributes": [ { "Title": "Security Hub enabled", diff --git a/prowler/compliance/aws/rbi_cyber_security_framework_aws.json b/prowler/compliance/aws/rbi_cyber_security_framework_aws.json index b4566a8929e..b292e7d4115 100644 --- a/prowler/compliance/aws/rbi_cyber_security_framework_aws.json +++ b/prowler/compliance/aws/rbi_cyber_security_framework_aws.json @@ -182,6 +182,14 @@ "securityhub_enabled", "vpc_flow_logs_enabled", "opensearch_service_domains_audit_logging_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/secnumcloud_3.2_aws.json b/prowler/compliance/aws/secnumcloud_3.2_aws.json index 1e157b47c79..3115dbb5814 100644 --- a/prowler/compliance/aws/secnumcloud_3.2_aws.json +++ b/prowler/compliance/aws/secnumcloud_3.2_aws.json @@ -202,6 +202,14 @@ "Checks": [ "config_recorder_all_regions_enabled", "ec2_instance_managed_by_ssm" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -323,6 +331,14 @@ "iam_role_administratoraccess_policy", "iam_user_administrator_access_policy", "iam_user_two_active_access_key" + ], + "ConfigRequirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -560,6 +576,17 @@ "Checks": [ "acm_certificates_expiration_check", "acm_certificates_with_secure_key_algorithms" + ], + "ConfigRequirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } ] }, { @@ -732,6 +759,14 @@ "config_recorder_all_regions_enabled", "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -771,6 +806,14 @@ "guardduty_lambda_protection_enabled", "guardduty_eks_audit_log_enabled", "guardduty_eks_runtime_monitoring_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -908,6 +951,20 @@ "cloudwatch_changes_to_network_gateways_alarm_configured", "cloudwatch_changes_to_network_route_tables_alarm_configured", "cloudwatch_changes_to_vpcs_alarm_configured" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1011,6 +1068,14 @@ "config_recorder_all_regions_enabled", "config_recorder_using_aws_service_role", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1057,6 +1122,14 @@ "Checks": [ "guardduty_is_enabled", "vpc_flow_logs_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1088,6 +1161,14 @@ "Checks": [ "config_recorder_all_regions_enabled", "cloudtrail_multi_region_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1265,6 +1346,20 @@ "guardduty_is_enabled", "securityhub_enabled", "cloudwatch_alarm_actions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1415,6 +1510,14 @@ "Checks": [ "backup_plans_exist", "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1478,6 +1581,20 @@ "Checks": [ "securityhub_enabled", "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -1495,6 +1612,14 @@ "Checks": [ "inspector2_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/aws/soc2_aws.json b/prowler/compliance/aws/soc2_aws.json index 5a027d04163..c0041a9daeb 100644 --- a/prowler/compliance/aws/soc2_aws.json +++ b/prowler/compliance/aws/soc2_aws.json @@ -43,6 +43,14 @@ "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_multi_region_enabled", "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -61,6 +69,26 @@ "guardduty_is_enabled", "securityhub_enabled", "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -80,6 +108,14 @@ "ssm_managed_compliant_patching", "guardduty_no_high_severity_findings", "guardduty_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -116,6 +152,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -133,6 +177,14 @@ "Checks": [ "guardduty_is_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -312,6 +364,20 @@ "Checks": [ "guardduty_is_enabled", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -331,6 +397,20 @@ "securityhub_enabled", "ec2_instance_managed_by_ssm", "ssm_managed_compliant_patching" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -367,6 +447,20 @@ "guardduty_is_enabled", "apigateway_restapi_logging_enabled", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22" + ], + "ConfigRequirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -399,6 +493,20 @@ "cloudwatch_log_group_retention_policy_specific_days_enabled", "vpc_flow_logs_enabled", "guardduty_no_high_severity_findings" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -426,6 +534,20 @@ "redshift_cluster_automated_snapshot", "s3_bucket_object_versioning", "securityhub_enabled" + ], + "ConfigRequirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -463,6 +585,14 @@ ], "Checks": [ "config_recorder_all_regions_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { @@ -600,6 +730,14 @@ "rds_cluster_integration_cloudwatch_logs", "glue_etl_jobs_logging_enabled", "stepfunctions_statemachine_logging_enabled" + ], + "ConfigRequirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } ] }, { diff --git a/prowler/compliance/azure/c5_azure.json b/prowler/compliance/azure/c5_azure.json index 4ac3b4dd53f..6fff7a36915 100644 --- a/prowler/compliance/azure/c5_azure.json +++ b/prowler/compliance/azure/c5_azure.json @@ -2681,6 +2681,17 @@ "app_function_latest_runtime_version", "mysql_flexible_server_minimum_tls_version_12", "sqlserver_recommended_minimal_tls_version" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -2705,6 +2716,17 @@ "app_function_latest_runtime_version", "mysql_flexible_server_minimum_tls_version_12", "sqlserver_recommended_minimal_tls_version" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -3903,6 +3925,17 @@ "app_ensure_php_version_is_latest", "storage_ensure_minimum_tls_version_12", "storage_smb_protocol_version_is_latest" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -4352,6 +4385,17 @@ "sqlserver_recommended_minimal_tls_version", "sqlserver_tde_encrypted_with_cmk", "sqlserver_tde_encryption_enabled" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -5743,6 +5787,17 @@ "storage_ensure_minimum_tls_version_12", "sqlserver_tde_encrypted_with_cmk", "sqlserver_tde_encryption_enabled" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -5770,6 +5825,17 @@ "storage_ensure_minimum_tls_version_12", "sqlserver_tde_encrypted_with_cmk", "sqlserver_tde_encryption_enabled" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -6513,6 +6579,17 @@ "mysql_flexible_server_minimum_tls_version_12", "sqlserver_recommended_minimal_tls_version", "storage_ensure_minimum_tls_version_12" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { diff --git a/prowler/compliance/azure/ccc_azure.json b/prowler/compliance/azure/ccc_azure.json index cb87346d13c..661d38302fe 100644 --- a/prowler/compliance/azure/ccc_azure.json +++ b/prowler/compliance/azure/ccc_azure.json @@ -56,6 +56,25 @@ "app_ensure_using_http20", "app_ftp_deployment_disabled", "app_function_ftps_deployment_disabled" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + }, + { + "Check": "storage_smb_channel_encryption_with_secure_algorithm", + "ConfigKey": "recommended_smb_channel_encryption_algorithms", + "Operator": "subset", + "Value": [ + "AES-256-GCM" + ] + } ] }, { @@ -726,6 +745,16 @@ ], "Checks": [ "storage_smb_channel_encryption_with_secure_algorithm" + ], + "ConfigRequirements": [ + { + "Check": "storage_smb_channel_encryption_with_secure_algorithm", + "ConfigKey": "recommended_smb_channel_encryption_algorithms", + "Operator": "subset", + "Value": [ + "AES-256-GCM" + ] + } ] }, { diff --git a/prowler/compliance/azure/cis_4.0_azure.json b/prowler/compliance/azure/cis_4.0_azure.json index c075ff40472..2e3a311aca9 100644 --- a/prowler/compliance/azure/cis_4.0_azure.json +++ b/prowler/compliance/azure/cis_4.0_azure.json @@ -375,6 +375,16 @@ "Checks": [ "storage_smb_channel_encryption_with_secure_algorithm" ], + "ConfigRequirements": [ + { + "Check": "storage_smb_channel_encryption_with_secure_algorithm", + "ConfigKey": "recommended_smb_channel_encryption_algorithms", + "Operator": "subset", + "Value": [ + "AES-256-GCM" + ] + } + ], "Attributes": [ { "Section": "10 Storage Services", diff --git a/prowler/compliance/azure/cis_5.0_azure.json b/prowler/compliance/azure/cis_5.0_azure.json index 21785d92b69..da76891b203 100644 --- a/prowler/compliance/azure/cis_5.0_azure.json +++ b/prowler/compliance/azure/cis_5.0_azure.json @@ -3000,6 +3000,16 @@ "Checks": [ "storage_smb_channel_encryption_with_secure_algorithm" ], + "ConfigRequirements": [ + { + "Check": "storage_smb_channel_encryption_with_secure_algorithm", + "ConfigKey": "recommended_smb_channel_encryption_algorithms", + "Operator": "subset", + "Value": [ + "AES-256-GCM" + ] + } + ], "Attributes": [ { "Section": "9 Storage Services", diff --git a/prowler/compliance/azure/hipaa_azure.json b/prowler/compliance/azure/hipaa_azure.json index 332d081590e..3f525332d2f 100644 --- a/prowler/compliance/azure/hipaa_azure.json +++ b/prowler/compliance/azure/hipaa_azure.json @@ -762,6 +762,17 @@ "mysql_flexible_server_minimum_tls_version_12", "mysql_flexible_server_ssl_connection_enabled", "postgresql_flexible_server_enforce_ssl_enabled" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -814,6 +825,17 @@ "mysql_flexible_server_ssl_connection_enabled", "postgresql_flexible_server_enforce_ssl_enabled", "databricks_workspace_cmk_encryption_enabled" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] } ] diff --git a/prowler/compliance/azure/nis2_azure.json b/prowler/compliance/azure/nis2_azure.json index 5c48c22f6b7..0ae814fad7c 100644 --- a/prowler/compliance/azure/nis2_azure.json +++ b/prowler/compliance/azure/nis2_azure.json @@ -1133,6 +1133,17 @@ "defender_ensure_defender_for_dns_is_on", "sqlserver_tde_encryption_enabled" ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } + ], "Attributes": [ { "Section": "6 SECURITY IN NETWORK AND INFORMATION SYSTEMS ACQUISITION, DEVELOPMENT AND MAINTENANCE (ARTICLE 21(2), POINT (E), OF DIRECTIVE (EU) 2022/2555)", @@ -1164,6 +1175,17 @@ "network_udp_internet_access_restricted", "network_watcher_enabled" ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } + ], "Attributes": [ { "Section": "6 SECURITY IN NETWORK AND INFORMATION SYSTEMS ACQUISITION, DEVELOPMENT AND MAINTENANCE (ARTICLE 21(2), POINT (E), OF DIRECTIVE (EU) 2022/2555)", @@ -1887,6 +1909,17 @@ "sqlserver_tde_encrypted_with_cmk", "sqlserver_tde_encryption_enabled" ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } + ], "Attributes": [ { "Section": "12 ASSET MANAGEMENT (ARTICLE 21(2), POINT (I), OF DIRECTIVE (EU) 2022/2555)", diff --git a/prowler/compliance/azure/secnumcloud_3.2_azure.json b/prowler/compliance/azure/secnumcloud_3.2_azure.json index ef8c94113ec..100181c9313 100644 --- a/prowler/compliance/azure/secnumcloud_3.2_azure.json +++ b/prowler/compliance/azure/secnumcloud_3.2_azure.json @@ -439,6 +439,25 @@ "postgresql_flexible_server_enforce_ssl_enabled", "mysql_flexible_server_ssl_connection_enabled", "mysql_flexible_server_minimum_tls_version_12" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + }, + { + "Check": "storage_smb_channel_encryption_with_secure_algorithm", + "ConfigKey": "recommended_smb_channel_encryption_algorithms", + "Operator": "subset", + "Value": [ + "AES-256-GCM" + ] + } ] }, { diff --git a/prowler/compliance/azure/soc2_azure.json b/prowler/compliance/azure/soc2_azure.json index d8936f54859..80a6ed99296 100644 --- a/prowler/compliance/azure/soc2_azure.json +++ b/prowler/compliance/azure/soc2_azure.json @@ -265,6 +265,17 @@ "sqlserver_tde_encryption_enabled", "sqlserver_unrestricted_inbound_access", "storage_secure_transfer_required_is_enabled" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { @@ -308,6 +319,17 @@ "mysql_flexible_server_minimum_tls_version_12", "sqlserver_recommended_minimal_tls_version", "storage_ensure_minimum_tls_version_12" + ], + "ConfigRequirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } ] }, { diff --git a/prowler/compliance/csa_ccm_4.0.json b/prowler/compliance/csa_ccm_4.0.json index e014c5f4082..6a0372dcb99 100644 --- a/prowler/compliance/csa_ccm_4.0.json +++ b/prowler/compliance/csa_ccm_4.0.json @@ -229,7 +229,15 @@ "oraclecloud": [ "cloudguard_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "A&A-04", @@ -334,7 +342,21 @@ "oraclecloud": [ "cloudguard_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "AIS-04", @@ -978,7 +1000,15 @@ "defender_ensure_defender_for_server_is_on", "vm_backup_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "drs_job_exist", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "BCR-11", @@ -1414,7 +1444,27 @@ "events_rule_security_list_changes", "events_rule_vcn_changes" ] - } + }, + "config_requirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "CEK-03", @@ -1656,7 +1706,17 @@ "filestorage_file_system_encrypted_with_cmk", "objectstorage_bucket_encrypted_with_cmk" ] - } + }, + "config_requirements": [ + { + "Check": "storage_smb_channel_encryption_with_secure_algorithm", + "ConfigKey": "recommended_smb_channel_encryption_algorithms", + "Operator": "subset", + "Value": [ + "AES-256-GCM" + ] + } + ] }, { "id": "CEK-04", @@ -1798,7 +1858,27 @@ "dns_rsasha1_in_use_to_key_sign_in_dnssec", "dns_rsasha1_in_use_to_zone_sign_in_dnssec" ] - } + }, + "config_requirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + }, + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } + ] }, { "id": "CEK-08", @@ -2341,7 +2421,15 @@ "alibabacloud": [ "securitycenter_all_assets_agent_installed" ] - } + }, + "config_requirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DSP-02", @@ -2579,7 +2667,15 @@ "alibabacloud": [ "securitycenter_all_assets_agent_installed" ] - } + }, + "config_requirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DSP-04", @@ -2993,7 +3089,18 @@ "oraclecloud": [ "compute_instance_in_transit_encryption_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": [ + "1.2", + "1.3" + ] + } + ] }, { "id": "DSP-16", @@ -3399,7 +3506,21 @@ "oraclecloud": [ "cloudguard_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "IAM-02", @@ -6251,7 +6372,15 @@ "cloudguard_enabled", "events_rule_cloudguard_problems" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "LOG-02", @@ -6554,7 +6683,21 @@ "events_notification_topic_and_subscription_exists", "events_rule_local_user_authentication" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "LOG-04", @@ -7598,7 +7741,15 @@ "events_rule_cloudguard_problems", "events_notification_topic_and_subscription_exists" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "SEF-03", @@ -7876,7 +8027,21 @@ "oraclecloud": [ "cloudguard_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "SEF-08", @@ -8457,7 +8622,15 @@ "oraclecloud": [ "cloudguard_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "TVM-05", @@ -8725,7 +8898,15 @@ "oraclecloud": [ "cloudguard_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "UEM-08", diff --git a/prowler/compliance/dora.json b/prowler/compliance/dora.json index 2378a307c6d..6dbf435bf64 100644 --- a/prowler/compliance/dora.json +++ b/prowler/compliance/dora.json @@ -137,7 +137,33 @@ "guardduty_centrally_managed", "guardduty_delegated_admin_enabled_all_regions" ] - } + }, + "config_requirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_delegated_admin_enabled_all_regions", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art7", @@ -168,7 +194,18 @@ "cloudfront_distributions_https_enabled", "rds_instance_transport_encrypted" ] - } + }, + "config_requirements": [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": [ + "RSA-1024", + "P-192" + ] + } + ] }, { "id": "DORA-Art8", @@ -190,7 +227,15 @@ "ec2_networkacl_unused", "secretsmanager_secret_unused" ] - } + }, + "config_requirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art9", @@ -267,7 +312,21 @@ "inspector2_active_findings_exist", "ec2_elastic_ip_shodan" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art11", @@ -349,7 +408,15 @@ "accessanalyzer_enabled_without_findings", "cloudtrail_insights_exist" ] - } + }, + "config_requirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art14", @@ -430,7 +497,21 @@ "cloudtrail_threat_detection_llm_jacking", "cloudtrail_threat_detection_privilege_escalation" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_delegated_admin_enabled_all_regions", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art19", @@ -480,7 +561,15 @@ "ec2_instance_with_outdated_ami", "ssm_managed_compliant_patching" ] - } + }, + "config_requirements": [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art25", @@ -507,7 +596,21 @@ "iam_no_expired_server_certificates_stored", "ssm_managed_compliant_patching" ] - } + }, + "config_requirements": [ + { + "Check": "config_recorder_all_regions_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art28", @@ -538,7 +641,15 @@ "vpc_peering_routing_tables_with_least_privilege", "awslambda_function_using_cross_account_layers" ] - } + }, + "config_requirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art30", @@ -568,7 +679,15 @@ "iam_policy_attached_only_to_group_or_roles", "accessanalyzer_enabled" ] - } + }, + "config_requirements": [ + { + "Check": "accessanalyzer_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] }, { "id": "DORA-Art45", @@ -591,7 +710,21 @@ "cloudtrail_threat_detection_privilege_escalation", "accessanalyzer_enabled_without_findings" ] - } + }, + "config_requirements": [ + { + "Check": "guardduty_is_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + }, + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": false + } + ] } ] } diff --git a/prowler/compliance/gcp/ccc_gcp.json b/prowler/compliance/gcp/ccc_gcp.json index 67a75841ef6..520b6b534a4 100644 --- a/prowler/compliance/gcp/ccc_gcp.json +++ b/prowler/compliance/gcp/ccc_gcp.json @@ -924,6 +924,14 @@ "cloudsql_instance_automated_backups", "cloudstorage_bucket_log_retention_policy_lock", "cloudstorage_bucket_sufficient_retention_period" + ], + "ConfigRequirements": [ + { + "Check": "cloudstorage_bucket_sufficient_retention_period", + "ConfigKey": "storage_min_retention_days", + "Operator": "gte", + "Value": 30 + } ] }, { @@ -5841,6 +5849,20 @@ "Checks": [ "iam_sa_user_managed_key_unused", "iam_service_account_unused" + ], + "ConfigRequirements": [ + { + "Check": "iam_sa_user_managed_key_unused", + "ConfigKey": "max_unused_account_days", + "Operator": "lte", + "Value": 90 + }, + { + "Check": "iam_service_account_unused", + "ConfigKey": "max_unused_account_days", + "Operator": "lte", + "Value": 90 + } ] }, { diff --git a/prowler/compliance/kubernetes/cis_1.10_kubernetes.json b/prowler/compliance/kubernetes/cis_1.10_kubernetes.json index a049efc040f..6fea9c2291c 100644 --- a/prowler/compliance/kubernetes/cis_1.10_kubernetes.json +++ b/prowler/compliance/kubernetes/cis_1.10_kubernetes.json @@ -820,6 +820,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 30 + } + ], "Attributes": [ { "Section": "1 Control Plane Components", diff --git a/prowler/compliance/kubernetes/cis_1.11_kubernetes.json b/prowler/compliance/kubernetes/cis_1.11_kubernetes.json index 6a19ea41618..b14cc1f9499 100644 --- a/prowler/compliance/kubernetes/cis_1.11_kubernetes.json +++ b/prowler/compliance/kubernetes/cis_1.11_kubernetes.json @@ -820,6 +820,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 30 + } + ], "Attributes": [ { "Section": "1 Control Plane Components", diff --git a/prowler/compliance/kubernetes/cis_1.12_kubernetes.json b/prowler/compliance/kubernetes/cis_1.12_kubernetes.json index 1ba1e55a886..292b1085ce2 100644 --- a/prowler/compliance/kubernetes/cis_1.12_kubernetes.json +++ b/prowler/compliance/kubernetes/cis_1.12_kubernetes.json @@ -820,6 +820,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 30 + } + ], "Attributes": [ { "Section": "1 Control Plane Components", diff --git a/prowler/compliance/kubernetes/cis_1.8_kubernetes.json b/prowler/compliance/kubernetes/cis_1.8_kubernetes.json index 24a5733a050..f3762757aaa 100644 --- a/prowler/compliance/kubernetes/cis_1.8_kubernetes.json +++ b/prowler/compliance/kubernetes/cis_1.8_kubernetes.json @@ -843,6 +843,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 30 + } + ], "Attributes": [ { "Section": "1 Control Plane Components", diff --git a/prowler/compliance/kubernetes/pci_4.0_kubernetes.json b/prowler/compliance/kubernetes/pci_4.0_kubernetes.json index 0b301a06456..38d9557d5f0 100644 --- a/prowler/compliance/kubernetes/pci_4.0_kubernetes.json +++ b/prowler/compliance/kubernetes/pci_4.0_kubernetes.json @@ -8268,6 +8268,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "10.5.1: Audit log history is retained and available for analysis.", @@ -10054,6 +10062,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "3.3.1.3: Sensitive authentication data (SAD) is not stored after authorization.", @@ -10250,6 +10266,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "3.3.3: Sensitive authentication data (SAD) is not stored after authorization.", @@ -13004,6 +13028,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 365 + } + ], "Attributes": [ { "Section": "5.3.4: Anti-malware mechanisms and processes are active, maintained, and monitored.", diff --git a/prowler/compliance/kubernetes/prowler_threatscore_kubernetes.json b/prowler/compliance/kubernetes/prowler_threatscore_kubernetes.json index 11ffe42485d..b8b6a606656 100644 --- a/prowler/compliance/kubernetes/prowler_threatscore_kubernetes.json +++ b/prowler/compliance/kubernetes/prowler_threatscore_kubernetes.json @@ -1199,6 +1199,14 @@ "Checks": [ "apiserver_audit_log_maxage_set" ], + "ConfigRequirements": [ + { + "Check": "apiserver_audit_log_maxage_set", + "ConfigKey": "audit_log_maxage", + "Operator": "gte", + "Value": 30 + } + ], "Attributes": [ { "Title": "API Server audit log retention configured", diff --git a/prowler/lib/check/compliance_config_eval.py b/prowler/lib/check/compliance_config_eval.py new file mode 100644 index 00000000000..2eefe086932 --- /dev/null +++ b/prowler/lib/check/compliance_config_eval.py @@ -0,0 +1,220 @@ +"""Shared evaluation of a requirement's configuration constraints. + +Some compliance requirements only hold if the configurable checks they map to +ran with a configuration strict enough for the requirement. For example CIS AWS +6.0 requirement 2.11 ("credentials unused for 45 days or more are disabled") +maps `iam_user_accesskey_unused` (config `max_unused_access_keys_days`); if the +user loosens that to 120 days the check can PASS while the requirement is, in +fact, not satisfied. + +A requirement declares its expectations via ``ConfigRequirements`` (a list of +``{Check, ConfigKey, Operator, Value}``). The configuration a scan applied is a +single, scan-global mapping (the provider's ``audit_config``), so the rules are +evaluated against that mapping directly. This module is consumed by the SDK +compliance outputs (CSV + CLI table) and by the Prowler App backend so the rule +lives in one place. +""" + +from typing import Any, Optional + +# Prefix prepended to a finding's ``status_extended`` when its requirement's +# config constraints are not satisfied and the status is forced to FAIL. +CONFIG_NOT_VALID_PREFIX = "[CONFIG NOT VALID]" + + +def _check_operator(applied: Any, operator: str, expected: Any) -> bool: + """Return whether ``applied`` satisfies ``operator`` against ``expected``.""" + try: + if operator == "lte": + return applied <= expected + if operator == "gte": + return applied >= expected + if operator == "eq": + return applied == expected + if operator == "in": + return applied in expected + if operator in ("subset", "superset"): + # Set comparisons for list-valued configs (allowlists / denylists). + # Both sides must be collections; anything else is not satisfiable. + if not isinstance(applied, (list, tuple, set)) or not isinstance( + expected, (list, tuple, set) + ): + return False + applied_set, expected_set = set(applied), set(expected) + if operator == "subset": + return applied_set <= expected_set + return applied_set >= expected_set + except TypeError: + # Mismatched/unhashable types → treat as not satisfied. + return False + # Unknown operator: do not block the requirement on a malformed constraint. + return True + + +def evaluate_config_constraints( + config_requirements: Optional[list], + audit_config: Optional[dict], +) -> tuple[bool, str]: + """Evaluate a requirement's config constraints against the scan's config. + + Args: + config_requirements: list of constraints, each a mapping (or object with + the same attributes) holding ``Check``, ``ConfigKey``, ``Operator`` + and ``Value``. ``None``/empty means the requirement has no config + expectations. + audit_config: the scan-global configuration mapping (the provider's + ``audit_config``, i.e. ``{config_key: value}``). The applied config + is identical across every resource and region of a scan. + + Returns: + ``(is_compliant, reason)``. ``is_compliant`` is ``True`` when there are + no constraints or every explicitly-set value satisfies its constraint. + When a configured value violates a constraint, returns ``(False, reason)`` + describing the first violation. A constraint whose ``ConfigKey`` was not + explicitly set is skipped (the check's default is assumed to match what + the requirement expects). + """ + if not config_requirements: + return True, "" + + audit_config = audit_config or {} + + for constraint in config_requirements: + # Accept both dicts (API template) and objects (Pydantic model). + if isinstance(constraint, dict): + check = constraint.get("Check") + config_key = constraint.get("ConfigKey") + operator = constraint.get("Operator") + expected = constraint.get("Value") + else: + check = getattr(constraint, "Check", None) + config_key = getattr(constraint, "ConfigKey", None) + operator = getattr(constraint, "Operator", None) + expected = getattr(constraint, "Value", None) + + if config_key not in audit_config: + # Config not explicitly set → default is assumed adequate. + continue + + applied = audit_config[config_key] + if not _check_operator(applied, operator, expected): + reason = ( + f"config not valid for requirement: {check}.{config_key}=" + f"{applied!r} does not satisfy {operator} {expected!r}" + ) + return False, reason + + return True, "" + + +def get_scan_audit_config() -> dict: + """Return the scan-global applied configuration (the provider's audit_config). + + The applied config is identical across every resource and region of a scan, + so every compliance output evaluates constraints against this single mapping. + Imported lazily to avoid a circular import with the provider package and to + keep this module usable from contexts without a global provider (returns + ``{}`` if no provider is set or audit_config is unavailable). + """ + try: + from prowler.providers.common.provider import Provider + + return Provider.get_global_provider().audit_config or {} + except Exception: + return {} + + +def _requirement_id(requirement: Any) -> Optional[str]: + """Return a requirement's id across the legacy (``Id``) and universal (``id``) models.""" + return getattr(requirement, "Id", None) or getattr(requirement, "id", None) + + +def _requirement_constraints(requirement: Any) -> Optional[list]: + """Return a requirement's config constraints across both model flavours. + + Legacy ``Compliance_Requirement`` exposes ``ConfigRequirements`` (a list of + Pydantic models); ``UniversalComplianceRequirement`` exposes + ``config_requirements`` (a list of dicts). ``evaluate_config_constraints`` + handles both element types. + """ + return getattr(requirement, "ConfigRequirements", None) or getattr( + requirement, "config_requirements", None + ) + + +def build_requirement_config_status( + requirements: list, + audit_config: Optional[dict] = None, +) -> dict: + """Map every requirement id to its ``(is_compliant, reason)`` config verdict. + + Only requirements that actually declare constraints are included; callers use + ``dict.get(req_id)`` (returning ``None`` → no constraints → no override). + + Args: + requirements: the framework's requirements (legacy or universal models). + audit_config: the applied config; resolved via ``get_scan_audit_config`` + when omitted. + """ + if audit_config is None: + audit_config = get_scan_audit_config() + status = {} + for requirement in requirements: + constraints = _requirement_constraints(requirement) + if constraints: + status[_requirement_id(requirement)] = evaluate_config_constraints( + constraints, audit_config + ) + return status + + +def resolve_requirement_config_status( + requirement: Any, + audit_config: dict, + cache: dict, +) -> tuple[bool, str]: + """Return a requirement's ``(is_compliant, reason)`` verdict, memoised in ``cache``. + + For table generators that iterate findings × compliances and only encounter + each requirement lazily. ``cache`` is keyed by requirement id and reused + across the whole table build. + """ + req_id = _requirement_id(requirement) + if req_id not in cache: + constraints = _requirement_constraints(requirement) + cache[req_id] = ( + evaluate_config_constraints(constraints, audit_config) + if constraints + else (True, "") + ) + return cache[req_id] + + +def apply_config_status( + status: str, + status_extended: str, + config_status: Optional[tuple], +) -> tuple[str, str]: + """Override a finding's ``(status, status_extended)`` when its config is invalid. + + A requirement whose configurable checks ran with a config too loose to trust + is forced to ``FAIL`` regardless of the finding's own status, with the reason + prepended to ``status_extended``. ``config_status`` is the ``(ok, reason)`` + tuple from ``build_requirement_config_status`` (``None`` → no constraints). + """ + if not config_status or config_status[0]: + return status, status_extended + return ( + "FAIL", + f"{CONFIG_NOT_VALID_PREFIX} {config_status[1]}. {status_extended}", + ) + + +def get_effective_status( + status: str, + config_status: Optional[tuple], +) -> str: + """Return the effective status for table aggregation (``FAIL`` if config invalid).""" + if not config_status or config_status[0]: + return status + return "FAIL" diff --git a/prowler/lib/check/compliance_models.py b/prowler/lib/check/compliance_models.py index 3610893008d..9ee26c9df4a 100644 --- a/prowler/lib/check/compliance_models.py +++ b/prowler/lib/check/compliance_models.py @@ -3,7 +3,7 @@ import os import sys from enum import Enum -from typing import Optional, Union +from typing import Literal, Optional, Union from pydantic.v1 import BaseModel, Field, ValidationError, root_validator @@ -284,6 +284,34 @@ class CSA_CCM_Requirement_Attribute(BaseModel): # Base Compliance Model +class Compliance_Requirement_ConfigConstraint(BaseModel): + """A constraint a requirement places on a configurable check's config. + + Declares that the configurable check ``Check`` must have run with + ``ConfigKey`` satisfying ``Operator`` ``Value`` for the requirement's + result to be trusted. Example: ``max_unused_access_keys_days <= 45``. + + Operators: + - ``lte``/``gte``/``eq``: scalar comparisons (e.g. a max-age or min-retention + threshold, or a boolean toggle). + - ``in``: the applied scalar must be one of ``Value`` (a list). + - ``subset``: the applied list must be a subset of ``Value`` — for allowlist + configs (e.g. ``recommended_minimal_tls_versions``); widening the allowlist + with a weaker value (e.g. TLS ``1.0``) breaks the constraint. + - ``superset``: the applied list must be a superset of ``Value`` — for + denylist configs (e.g. ``insecure_key_algorithms``); removing a forbidden + value from the denylist breaks the constraint. + """ + + Check: str + ConfigKey: str + Operator: Literal["lte", "gte", "eq", "in", "subset", "superset"] + # ``bool`` must precede ``int`` so pydantic v1 keeps booleans (e.g. a + # ``mute_non_default_regions == false`` constraint) instead of coercing + # them to 0/1. + Value: Union[bool, int, float, str, list] + + # TODO: move this to compliance folder class Compliance_Requirement(BaseModel): """Compliance_Requirement holds the base model for every requirement within a compliance framework""" @@ -308,6 +336,7 @@ class Compliance_Requirement(BaseModel): ] ] Checks: list[str] + ConfigRequirements: Optional[list[Compliance_Requirement_ConfigConstraint]] = None class Compliance(BaseModel): @@ -680,6 +709,7 @@ class UniversalComplianceRequirement(BaseModel): name: Optional[str] = None attributes: dict = Field(default_factory=dict) checks: dict[str, list[str]] = Field(default_factory=dict) + config_requirements: Optional[list[dict]] = None tactics: Optional[list] = None sub_techniques: Optional[list] = None platforms: Optional[list] = None @@ -892,6 +922,11 @@ def adapt_legacy_to_universal(legacy: Compliance) -> ComplianceFramework: attrs = req.Attributes[0].dict() else: attrs = {} + config_requirements = ( + [c.dict() for c in req.ConfigRequirements] + if getattr(req, "ConfigRequirements", None) + else None + ) universal_requirements.append( UniversalComplianceRequirement( id=req.Id, @@ -899,6 +934,7 @@ def adapt_legacy_to_universal(legacy: Compliance) -> ComplianceFramework: name=req.Name, attributes=attrs, checks=req_checks, + config_requirements=config_requirements, ) ) diff --git a/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight.py b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight.py index 75481fb6aac..8afb6365451 100644 --- a/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight.py +++ b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_asd_essential_eight_table( @@ -22,12 +27,24 @@ def get_asd_essential_eight_table( pass_count = [] fail_count = [] muted_count = [] + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance for compliance in check_compliances: if compliance.Framework == "ASD-Essential-Eight": for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL. + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for attribute in requirement.Attributes: section = attribute.Section if section not in sections: @@ -41,10 +58,10 @@ def get_asd_essential_eight_table( muted_count.append(index) sections[section]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) sections[section]["FAIL"] += 1 - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) sections[section]["PASS"] += 1 diff --git a/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws.py b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws.py index c264c81505c..e326ae12822 100644 --- a/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws.py +++ b/prowler/lib/outputs/compliance/asd_essential_eight/asd_essential_eight_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.asd_essential_eight.models import ( ASDEssentialEightAWSModel, @@ -36,10 +40,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = ASDEssentialEightAWSModel( Provider=finding.provider, @@ -63,8 +79,8 @@ def transform( Requirements_Attributes_AuditProcedure=attribute.AuditProcedure, Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py b/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py index 4a157b03240..9f440e23fb6 100644 --- a/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py +++ b/prowler/lib/outputs/compliance/aws_well_architected/aws_well_architected.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.aws_well_architected.models import ( AWSWellArchitectedModel, @@ -36,10 +40,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AWSWellArchitectedModel( Provider=finding.provider, @@ -58,8 +73,8 @@ def transform( Requirements_Attributes_AssessmentMethod=attribute.AssessmentMethod, Requirements_Attributes_Description=attribute.Description, Requirements_Attributes_ImplementationGuidanceUrl=attribute.ImplementationGuidanceUrl, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/c5/c5.py b/prowler/lib/outputs/compliance/c5/c5.py index 32eb7e0f5a1..1417c50e393 100644 --- a/prowler/lib/outputs/compliance/c5/c5.py +++ b/prowler/lib/outputs/compliance/c5/c5.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_c5_table( @@ -22,12 +27,24 @@ def get_c5_table( fail_count = [] muted_count = [] sections = {} + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance for compliance in check_compliances: if compliance.Framework == "C5": for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL. + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for attribute in requirement.Attributes: section = attribute.Section @@ -39,10 +56,10 @@ def get_c5_table( muted_count.append(index) sections[section]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) sections[section]["FAIL"] += 1 - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) sections[section]["PASS"] += 1 diff --git a/prowler/lib/outputs/compliance/c5/c5_aws.py b/prowler/lib/outputs/compliance/c5/c5_aws.py index 5f13b77d7ac..d62566b6d1d 100644 --- a/prowler/lib/outputs/compliance/c5/c5_aws.py +++ b/prowler/lib/outputs/compliance/c5/c5_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.c5.models import AWSC5Model from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AWSC5Model( Provider=finding.provider, @@ -52,8 +68,8 @@ def transform( Requirements_Attributes_Type=attribute.Type, Requirements_Attributes_AboutCriteria=attribute.AboutCriteria, Requirements_Attributes_ComplementaryCriteria=attribute.ComplementaryCriteria, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/c5/c5_azure.py b/prowler/lib/outputs/compliance/c5/c5_azure.py index 1b3a9e6b90f..976d37e9be9 100644 --- a/prowler/lib/outputs/compliance/c5/c5_azure.py +++ b/prowler/lib/outputs/compliance/c5/c5_azure.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.c5.models import AzureC5Model from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AzureC5Model( Provider=finding.provider, @@ -52,8 +68,8 @@ def transform( Requirements_Attributes_Type=attribute.Type, Requirements_Attributes_AboutCriteria=attribute.AboutCriteria, Requirements_Attributes_ComplementaryCriteria=attribute.ComplementaryCriteria, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/c5/c5_gcp.py b/prowler/lib/outputs/compliance/c5/c5_gcp.py index 84e66a141f8..ae69df8dfe9 100644 --- a/prowler/lib/outputs/compliance/c5/c5_gcp.py +++ b/prowler/lib/outputs/compliance/c5/c5_gcp.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.c5.models import GCPC5Model from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = GCPC5Model( Provider=finding.provider, @@ -52,8 +68,8 @@ def transform( Requirements_Attributes_Type=attribute.Type, Requirements_Attributes_AboutCriteria=attribute.AboutCriteria, Requirements_Attributes_ComplementaryCriteria=attribute.ComplementaryCriteria, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/ccc/ccc.py b/prowler/lib/outputs/compliance/ccc/ccc.py index 99a6c91cd92..a829aa70d70 100644 --- a/prowler/lib/outputs/compliance/ccc/ccc.py +++ b/prowler/lib/outputs/compliance/ccc/ccc.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_ccc_table( @@ -22,12 +27,24 @@ def get_ccc_table( fail_count = [] muted_count = [] sections = {} + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance for compliance in check_compliances: if compliance.Framework == "CCC": for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL. + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for attribute in requirement.Attributes: section = attribute.Section @@ -39,10 +56,10 @@ def get_ccc_table( muted_count.append(index) sections[section]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) sections[section]["FAIL"] += 1 - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) sections[section]["PASS"] += 1 diff --git a/prowler/lib/outputs/compliance/ccc/ccc_aws.py b/prowler/lib/outputs/compliance/ccc/ccc_aws.py index 11144255740..82f5a58eab9 100644 --- a/prowler/lib/outputs/compliance/ccc/ccc_aws.py +++ b/prowler/lib/outputs/compliance/ccc/ccc_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.ccc.models import CCC_AWSModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = CCC_AWSModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_Recommendation=attribute.Recommendation, Requirements_Attributes_SectionThreatMappings=attribute.SectionThreatMappings, Requirements_Attributes_SectionGuidelineMappings=attribute.SectionGuidelineMappings, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/ccc/ccc_azure.py b/prowler/lib/outputs/compliance/ccc/ccc_azure.py index e0cdf323308..450c2b335ed 100644 --- a/prowler/lib/outputs/compliance/ccc/ccc_azure.py +++ b/prowler/lib/outputs/compliance/ccc/ccc_azure.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.ccc.models import CCC_AzureModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = CCC_AzureModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_Recommendation=attribute.Recommendation, Requirements_Attributes_SectionThreatMappings=attribute.SectionThreatMappings, Requirements_Attributes_SectionGuidelineMappings=attribute.SectionGuidelineMappings, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/ccc/ccc_gcp.py b/prowler/lib/outputs/compliance/ccc/ccc_gcp.py index 326a15e5c35..19ba65227d0 100644 --- a/prowler/lib/outputs/compliance/ccc/ccc_gcp.py +++ b/prowler/lib/outputs/compliance/ccc/ccc_gcp.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.ccc.models import CCC_GCPModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = CCC_GCPModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_Recommendation=attribute.Recommendation, Requirements_Attributes_SectionThreatMappings=attribute.SectionThreatMappings, Requirements_Attributes_SectionGuidelineMappings=attribute.SectionGuidelineMappings, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis.py b/prowler/lib/outputs/compliance/cis/cis.py index 7f161f34c28..eb92978451f 100644 --- a/prowler/lib/outputs/compliance/cis/cis.py +++ b/prowler/lib/outputs/compliance/cis/cis.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_cis_table( @@ -23,6 +28,10 @@ def get_cis_table( pass_count = [] fail_count = [] muted_count = [] + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance @@ -30,6 +39,14 @@ def get_cis_table( version_in_name = compliance_framework.split("_")[1] if compliance.Framework == "CIS" and version_in_name in compliance.Version: for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL. + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for attribute in requirement.Attributes: section = attribute.Section # Check if Section exists @@ -45,19 +62,19 @@ def get_cis_table( muted_count.append(index) sections[section]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) if "Level 1" in attribute.Profile: if not finding.muted: - if finding.status == "FAIL": + if effective_status == "FAIL": sections[section]["Level 1"]["FAIL"] += 1 else: sections[section]["Level 1"]["PASS"] += 1 elif "Level 2" in attribute.Profile: if not finding.muted: - if finding.status == "FAIL": + if effective_status == "FAIL": sections[section]["Level 2"]["FAIL"] += 1 else: sections[section]["Level 2"]["PASS"] += 1 diff --git a/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py b/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py index a1880d3af98..80023215179 100644 --- a/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py +++ b/prowler/lib/outputs/compliance/cis/cis_alibabacloud.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import AlibabaCloudCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AlibabaCloudCISModel( Provider=finding.provider, @@ -59,8 +74,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_DefaultValue=attribute.DefaultValue, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_aws.py b/prowler/lib/outputs/compliance/cis/cis_aws.py index ac0250150c1..f3b62056298 100644 --- a/prowler/lib/outputs/compliance/cis/cis_aws.py +++ b/prowler/lib/outputs/compliance/cis/cis_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import AWSCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,24 @@ def transform( Returns: - None """ + # The applied config is scan-global (the provider's audit_config), so + # evaluate each requirement's config constraints once against it. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: force FAIL regardless of the + # finding's own status. + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AWSCISModel( Provider=finding.provider, @@ -59,8 +77,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_DefaultValue=attribute.DefaultValue, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_azure.py b/prowler/lib/outputs/compliance/cis/cis_azure.py index b1e09ca453d..0ce742f864b 100644 --- a/prowler/lib/outputs/compliance/cis/cis_azure.py +++ b/prowler/lib/outputs/compliance/cis/cis_azure.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import AzureCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AzureCISModel( Provider=finding.provider, @@ -59,8 +74,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_DefaultValue=attribute.DefaultValue, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_gcp.py b/prowler/lib/outputs/compliance/cis/cis_gcp.py index b2889333beb..f6a3453c556 100644 --- a/prowler/lib/outputs/compliance/cis/cis_gcp.py +++ b/prowler/lib/outputs/compliance/cis/cis_gcp.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import GCPCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = GCPCISModel( Provider=finding.provider, @@ -58,8 +73,8 @@ def transform( Requirements_Attributes_AuditProcedure=attribute.AuditProcedure, Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_github.py b/prowler/lib/outputs/compliance/cis/cis_github.py index 2ce5b644801..fad959a7e4c 100644 --- a/prowler/lib/outputs/compliance/cis/cis_github.py +++ b/prowler/lib/outputs/compliance/cis/cis_github.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import GithubCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = GithubCISModel( Provider=finding.provider, @@ -58,8 +73,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_References=attribute.References, Requirements_Attributes_DefaultValue=attribute.DefaultValue, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py b/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py index d74375bf855..a0b841f40ca 100644 --- a/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py +++ b/prowler/lib/outputs/compliance/cis/cis_googleworkspace.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import GoogleWorkspaceCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = GoogleWorkspaceCISModel( Provider=finding.provider, @@ -58,8 +73,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_DefaultValue=attribute.DefaultValue, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_kubernetes.py b/prowler/lib/outputs/compliance/cis/cis_kubernetes.py index 9607123e443..0a559225d00 100644 --- a/prowler/lib/outputs/compliance/cis/cis_kubernetes.py +++ b/prowler/lib/outputs/compliance/cis/cis_kubernetes.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import KubernetesCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = KubernetesCISModel( Provider=finding.provider, @@ -59,8 +74,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_References=attribute.References, Requirements_Attributes_DefaultValue=attribute.DefaultValue, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_m365.py b/prowler/lib/outputs/compliance/cis/cis_m365.py index 1a001669463..4be14f04103 100644 --- a/prowler/lib/outputs/compliance/cis/cis_m365.py +++ b/prowler/lib/outputs/compliance/cis/cis_m365.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import M365CISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = M365CISModel( Provider=finding.provider, @@ -59,8 +74,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_DefaultValue=attribute.DefaultValue, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py b/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py index 117c3ca0047..d67d3c99409 100644 --- a/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py +++ b/prowler/lib/outputs/compliance/cis/cis_oraclecloud.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cis.models import OracleCloudCISModel from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = OracleCloudCISModel( Provider=finding.provider, @@ -59,8 +74,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_DefaultValue=attribute.DefaultValue, Requirements_Attributes_References=attribute.References, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py b/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py index 9f1336e8326..9254d411352 100644 --- a/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py +++ b/prowler/lib/outputs/compliance/cisa_scuba/cisa_scuba_googleworkspace.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.cisa_scuba.models import ( GoogleWorkspaceCISASCuBAModel, @@ -36,10 +40,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = GoogleWorkspaceCISASCuBAModel( Provider=finding.provider, @@ -52,8 +67,8 @@ def transform( Requirements_Attributes_SubSection=attribute.SubSection, Requirements_Attributes_Service=attribute.Service, Requirements_Attributes_Type=attribute.Type, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/ens/ens.py b/prowler/lib/outputs/compliance/ens/ens.py index a414a0206da..8747731dc0f 100644 --- a/prowler/lib/outputs/compliance/ens/ens.py +++ b/prowler/lib/outputs/compliance/ens/ens.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_ens_table( @@ -26,12 +31,24 @@ def get_ens_table( pass_count = [] fail_count = [] muted_count = [] + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance for compliance in check_compliances: if compliance.Framework == "ENS": for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL. + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for attribute in requirement.Attributes: marco_categoria = f"{attribute.Marco}/{attribute.Categoria}" # Check if Marco/Categoria exists @@ -49,7 +66,7 @@ def get_ens_table( muted_count.append(index) marcos[marco_categoria]["Muted"] += 1 else: - if finding.status == "FAIL": + if effective_status == "FAIL": if ( attribute.Tipo != "recomendacion" and index not in fail_count @@ -58,7 +75,7 @@ def get_ens_table( marcos[marco_categoria][ "Estado" ] = f"{Fore.RED}NO CUMPLE{Style.RESET_ALL}" - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) if attribute.Nivel == "opcional": marcos[marco_categoria]["Opcional"] += 1 diff --git a/prowler/lib/outputs/compliance/ens/ens_aws.py b/prowler/lib/outputs/compliance/ens/ens_aws.py index 40d185294b4..ed416b3533b 100644 --- a/prowler/lib/outputs/compliance/ens/ens_aws.py +++ b/prowler/lib/outputs/compliance/ens/ens_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.ens.models import AWSENSModel @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AWSENSModel( Provider=finding.provider, @@ -60,8 +76,8 @@ def transform( Requirements_Attributes_Dependencias=",".join( attribute.Dependencias ), - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/ens/ens_azure.py b/prowler/lib/outputs/compliance/ens/ens_azure.py index 20d727fed0a..a78debb539f 100644 --- a/prowler/lib/outputs/compliance/ens/ens_azure.py +++ b/prowler/lib/outputs/compliance/ens/ens_azure.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.ens.models import AzureENSModel @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AzureENSModel( Provider=finding.provider, @@ -60,8 +76,8 @@ def transform( Requirements_Attributes_Dependencias=",".join( attribute.Dependencias ), - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/ens/ens_gcp.py b/prowler/lib/outputs/compliance/ens/ens_gcp.py index 8a2baaca664..893b1411420 100644 --- a/prowler/lib/outputs/compliance/ens/ens_gcp.py +++ b/prowler/lib/outputs/compliance/ens/ens_gcp.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.ens.models import GCPENSModel @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = GCPENSModel( Provider=finding.provider, @@ -60,8 +76,8 @@ def transform( Requirements_Attributes_Dependencias=",".join( attribute.Dependencias ), - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/generic/generic.py b/prowler/lib/outputs/compliance/generic/generic.py index b774f09577a..25d640dddbe 100644 --- a/prowler/lib/outputs/compliance/generic/generic.py +++ b/prowler/lib/outputs/compliance/generic/generic.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.generic.models import GenericComplianceModel @@ -35,11 +39,27 @@ def transform( - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks ran + # with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + def compliance_row(requirement, attribute, finding=None): # Read attribute fields defensively: GenericCompliance is the # last-resort renderer for any framework, and provider-specific # schemas (e.g. CIS, ENS, ISO27001) do not declare the universal # Section/SubSection/SubGroup/Service/Type/Comment fields. + status, status_extended = ( + apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) + if finding + else ("MANUAL", "Manual check") + ) return GenericComplianceModel( Provider=(finding.provider if finding else compliance.Provider.lower()), Description=compliance.Description, @@ -56,8 +76,8 @@ def compliance_row(requirement, attribute, finding=None): Requirements_Attributes_Service=getattr(attribute, "Service", None), Requirements_Attributes_Type=getattr(attribute, "Type", None), Requirements_Attributes_Comment=getattr(attribute, "Comment", None), - Status=finding.status if finding else "MANUAL", - StatusExtended=(finding.status_extended if finding else "Manual check"), + Status=status, + StatusExtended=status_extended, ResourceId=finding.resource_uid if finding else "manual_check", ResourceName=finding.resource_name if finding else "Manual check", CheckId=finding.check_id if finding else "manual", diff --git a/prowler/lib/outputs/compliance/generic/generic_table.py b/prowler/lib/outputs/compliance/generic/generic_table.py index 9136cf19e2f..30cf04360c5 100644 --- a/prowler/lib/outputs/compliance/generic/generic_table.py +++ b/prowler/lib/outputs/compliance/generic/generic_table.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_generic_compliance_table( @@ -15,6 +20,10 @@ def get_generic_compliance_table( pass_count = [] fail_count = [] muted_count = [] + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance @@ -25,13 +34,28 @@ def get_generic_compliance_table( and compliance.Version in compliance_framework.upper() and compliance.Provider.upper() in compliance_framework.upper() ): + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL if any of + # the requirements it maps to has an invalid config. + effective_status = finding.status + for requirement in compliance.Requirements: + if finding.check_id in requirement.Checks: + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + if ( + get_effective_status(finding.status, config_status) + == "FAIL" + ): + effective_status = "FAIL" + break if finding.muted: if index not in muted_count: muted_count.append(index) else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) if ( len(fail_count) + len(pass_count) + len(muted_count) > 1 diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py b/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py index 282f27a218e..462fa749085 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.iso27001.models import AWSISO27001Model @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AWSISO27001Model( Provider=finding.provider, @@ -52,8 +67,8 @@ def transform( Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, Requirements_Attributes_Check_Summary=attribute.Check_Summary, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, CheckId=finding.check_id, Muted=finding.muted, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py b/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py index 0112bbd7e16..07c53673c96 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_azure.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.iso27001.models import AzureISO27001Model @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AzureISO27001Model( Provider=finding.provider, @@ -52,8 +67,8 @@ def transform( Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, Requirements_Attributes_Check_Summary=attribute.Check_Summary, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, CheckId=finding.check_id, Muted=finding.muted, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py b/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py index f60a30e67e4..433b0e81344 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_gcp.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.iso27001.models import GCPISO27001Model @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = GCPISO27001Model( Provider=finding.provider, @@ -52,8 +67,8 @@ def transform( Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, Requirements_Attributes_Check_Summary=attribute.Check_Summary, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, CheckId=finding.check_id, Muted=finding.muted, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py b/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py index 59fd593ce69..e0f54a8f02e 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_kubernetes.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.iso27001.models import KubernetesISO27001Model @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = KubernetesISO27001Model( Provider=finding.provider, @@ -52,8 +67,8 @@ def transform( Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, Requirements_Attributes_Check_Summary=attribute.Check_Summary, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, CheckId=finding.check_id, Muted=finding.muted, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py b/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py index cf6712a4a6a..5ea78838033 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_m365.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.iso27001.models import M365ISO27001Model @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = M365ISO27001Model( Provider=finding.provider, @@ -52,8 +67,8 @@ def transform( Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, Requirements_Attributes_Check_Summary=attribute.Check_Summary, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, CheckId=finding.check_id, Muted=finding.muted, diff --git a/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py b/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py index 2ad5a0bda45..6599ca1340c 100644 --- a/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py +++ b/prowler/lib/outputs/compliance/iso27001/iso27001_nhn.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.iso27001.models import NHNISO27001Model @@ -34,10 +38,21 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = NHNISO27001Model( Provider=finding.provider, @@ -52,8 +67,8 @@ def transform( Requirements_Attributes_Objetive_ID=attribute.Objetive_ID, Requirements_Attributes_Objetive_Name=attribute.Objetive_Name, Requirements_Attributes_Check_Summary=attribute.Check_Summary, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, CheckId=finding.check_id, Muted=finding.muted, diff --git a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py index 93b925a5ff3..a83f2db7939 100644 --- a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py +++ b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_kisa_ismsp_table( @@ -23,6 +28,10 @@ def get_kisa_ismsp_table( pass_count = [] fail_count = [] muted_count = [] + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance @@ -32,6 +41,14 @@ def get_kisa_ismsp_table( and compliance.Version in compliance_framework ): for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL. + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for attribute in requirement.Attributes: section = attribute.Section # Check if Section exists @@ -48,10 +65,10 @@ def get_kisa_ismsp_table( muted_count.append(index) sections[section]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) sections[section]["Status"]["FAIL"] += 1 - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) sections[section]["Status"]["PASS"] += 1 diff --git a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py index f927c2d4b11..dcde83c059d 100644 --- a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py +++ b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.kisa_ismsp.models import AWSKISAISMSPModel @@ -34,10 +38,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = AWSKISAISMSPModel( Provider=finding.provider, @@ -55,8 +71,8 @@ def transform( Requirements_Attributes_RelatedRegulations=attribute.RelatedRegulations, Requirements_Attributes_AuditEvidence=attribute.AuditEvidence, Requirements_Attributes_NonComplianceCases=attribute.NonComplianceCases, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py index bab3e4e31aa..81474cdda14 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) def get_mitre_attack_table( @@ -22,6 +27,10 @@ def get_mitre_attack_table( pass_count = [] fail_count = [] muted_count = [] + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance @@ -31,6 +40,14 @@ def get_mitre_attack_table( and compliance.Version in compliance_framework ): for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL. + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for tactic in requirement.Tactics: if tactic not in tactics: tactics[tactic] = {"FAIL": 0, "PASS": 0, "Muted": 0} @@ -39,11 +56,11 @@ def get_mitre_attack_table( muted_count.append(index) tactics[tactic]["Muted"] += 1 else: - if finding.status == "FAIL": + if effective_status == "FAIL": if index not in fail_count: fail_count.append(index) tactics[tactic]["FAIL"] += 1 - elif finding.status == "PASS": + elif effective_status == "PASS": if index not in pass_count: pass_count.append(index) tactics[tactic]["PASS"] += 1 diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py index 33a7aca74bb..8530fae9c80 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.mitre_attack.models import AWSMitreAttackModel @@ -35,10 +39,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) compliance_row = AWSMitreAttackModel( Provider=finding.provider, Description=compliance.Description, @@ -66,8 +82,8 @@ def transform( Requirements_Attributes_Comments=", ".join( attribute.Comment for attribute in requirement.Attributes ), - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py index 0254aad86e8..6b06806bf16 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_azure.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.mitre_attack.models import AzureMitreAttackModel @@ -35,10 +39,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) compliance_row = AzureMitreAttackModel( Provider=finding.provider, Description=compliance.Description, @@ -67,8 +83,8 @@ def transform( Requirements_Attributes_Comments=", ".join( attribute.Comment for attribute in requirement.Attributes ), - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py index 46342734941..b317e7c7f44 100644 --- a/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py +++ b/prowler/lib/outputs/compliance/mitre_attack/mitre_attack_gcp.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.mitre_attack.models import GCPMitreAttackModel @@ -35,10 +39,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) compliance_row = GCPMitreAttackModel( Provider=finding.provider, Description=compliance.Description, @@ -66,8 +82,8 @@ def transform( Requirements_Attributes_Comments=", ".join( attribute.Comment for attribute in requirement.Attributes ), - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore.py index cfcd4a006e5..0321cb126ff 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance @@ -30,12 +35,25 @@ def get_prowler_threatscore_table( score_per_pillar = {} max_score_per_pillar = {} counted_findings_per_pillar = {} + # The applied config is scan-global (the provider's audit_config). Evaluate + # each requirement's config constraints once against it (memoised by Id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check = bulk_checks_metadata[finding.check_metadata.CheckID] check_compliances = check.Compliance for compliance in check_compliances: if compliance.Framework == "ProwlerThreatScore": for requirement in compliance.Requirements: + # A requirement whose configurable checks ran with an invalid + # config can't be trusted: treat the finding as FAIL (it stops + # contributing to the pillar/generic score and counts as FAIL). + config_status = resolve_requirement_config_status( + requirement, audit_config, config_status_cache + ) + effective_status = get_effective_status( + finding.status, config_status + ) for attribute in requirement.Attributes: pillar = attribute.Section @@ -54,7 +72,7 @@ def get_prowler_threatscore_table( index not in counted_findings_per_pillar[pillar] and not finding.muted ): - if finding.status == "PASS": + if effective_status == "PASS": score_per_pillar[pillar] += ( attribute.LevelOfRisk * attribute.Weight ) @@ -71,16 +89,16 @@ def get_prowler_threatscore_table( muted_count.append(index) pillars[pillar]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) pillars[pillar]["FAIL"] += 1 - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) pillars[pillar]["PASS"] += 1 # Generic score if index not in counted_findings_generic and not finding.muted: - if finding.status == "PASS": + if effective_status == "PASS": generic_score += ( attribute.LevelOfRisk * attribute.Weight ) diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py index 510c098dff4..e11fa9c278d 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_alibaba.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.prowler_threatscore.models import ( @@ -36,10 +40,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreAlibabaModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk, Requirements_Attributes_Weight=attribute.Weight, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py index ae992f8a75c..42215f02671 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_aws.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.prowler_threatscore.models import ( @@ -36,10 +40,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreAWSModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk, Requirements_Attributes_Weight=attribute.Weight, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py index dd0a3b9a56e..a50008a7b48 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_azure.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.prowler_threatscore.models import ( @@ -36,10 +40,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreAzureModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk, Requirements_Attributes_Weight=attribute.Weight, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py index c3bad98ade7..72d49b1da6a 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_gcp.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.prowler_threatscore.models import ( @@ -36,10 +40,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreGCPModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk, Requirements_Attributes_Weight=attribute.Weight, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py index 51f88348f02..f2e80c5028b 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_kubernetes.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.prowler_threatscore.models import ( @@ -36,10 +40,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreKubernetesModel( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk, Requirements_Attributes_Weight=attribute.Weight, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py index d0b2ad635c4..a6bafa99cef 100644 --- a/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py +++ b/prowler/lib/outputs/compliance/prowler_threatscore/prowler_threatscore_m365.py @@ -1,4 +1,8 @@ from prowler.config.config import timestamp +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import Compliance from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput from prowler.lib.outputs.compliance.prowler_threatscore.models import ( @@ -36,10 +40,22 @@ def transform( Returns: - None """ + # Evaluate each requirement's config constraints once against the + # scan-global applied config; a requirement whose configurable checks + # ran with a config too loose to trust is forced to FAIL. + requirement_config_status = build_requirement_config_status( + compliance.Requirements + ) + for finding in findings: for requirement in compliance.Requirements: # Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift). if finding.check_id in requirement.Checks: + row_status, row_status_extended = apply_config_status( + finding.status, + finding.status_extended, + requirement_config_status.get(requirement.Id), + ) for attribute in requirement.Attributes: compliance_row = ProwlerThreatScoreM365Model( Provider=finding.provider, @@ -56,8 +72,8 @@ def transform( Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, Requirements_Attributes_LevelOfRisk=attribute.LevelOfRisk, Requirements_Attributes_Weight=attribute.Weight, - Status=finding.status, - StatusExtended=finding.status_extended, + Status=row_status, + StatusExtended=row_status_extended, ResourceId=finding.resource_uid, ResourceName=finding.resource_name, CheckId=finding.check_id, diff --git a/prowler/lib/outputs/compliance/universal/ocsf_compliance.py b/prowler/lib/outputs/compliance/universal/ocsf_compliance.py index 2886f7e4d2f..fed5f13457a 100644 --- a/prowler/lib/outputs/compliance/universal/ocsf_compliance.py +++ b/prowler/lib/outputs/compliance/universal/ocsf_compliance.py @@ -19,6 +19,10 @@ from py_ocsf_models.objects.resource_details import ResourceDetails from prowler.config.config import prowler_version +from prowler.lib.check.compliance_config_eval import ( + apply_config_status, + build_requirement_config_status, +) from prowler.lib.check.compliance_models import ComplianceFramework from prowler.lib.logger import logger from prowler.lib.outputs.utils import unroll_dict_to_list @@ -181,11 +185,22 @@ def _transform( for check_id in all_checks: check_req_map.setdefault(check_id, []).append(req) + # The applied config is scan-global (the provider's audit_config). A + # requirement whose config constraints aren't satisfied is FAILed even + # when its findings PASS, mirroring the CSV/CLI behaviour. + requirement_config_status = build_requirement_config_status( + framework.requirements + ) + for finding in findings: if finding.check_id in check_req_map: for req in check_req_map[finding.check_id]: cf = self._build_compliance_finding( - finding, framework, req, compliance_name + finding, + framework, + req, + compliance_name, + requirement_config_status.get(req.id, (True, "")), ) if cf: self._data.append(cf) @@ -240,10 +255,17 @@ def _build_compliance_finding( framework: ComplianceFramework, requirement, compliance_name: str, + config_status: tuple = (True, ""), ) -> ComplianceFinding: try: + # If the requirement's config isn't valid, the requirement FAILs even + # though the check (and finding) passed. The Check keeps the real + # finding status; the Compliance status reflects the requirement. + effective_status, message = apply_config_status( + finding.status, finding.status_extended, config_status + ) compliance_status = PROWLER_TO_COMPLIANCE_STATUS.get( - finding.status, ComplianceStatusID.Unknown + effective_status, ComplianceStatusID.Unknown ) check_status = PROWLER_TO_COMPLIANCE_STATUS.get( finding.status, ComplianceStatusID.Unknown @@ -293,7 +315,7 @@ def _build_compliance_finding( else None ), ), - message=finding.status_extended, + message=message, metadata=Metadata( event_code=finding.check_id, product=Product( diff --git a/prowler/lib/outputs/compliance/universal/universal_table.py b/prowler/lib/outputs/compliance/universal/universal_table.py index e838c5e9cfc..73b0d4d512c 100644 --- a/prowler/lib/outputs/compliance/universal/universal_table.py +++ b/prowler/lib/outputs/compliance/universal/universal_table.py @@ -2,6 +2,11 @@ from tabulate import tabulate from prowler.config.config import orange_color +from prowler.lib.check.compliance_config_eval import ( + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) from prowler.lib.check.compliance_models import ComplianceFramework @@ -166,6 +171,10 @@ def _render_grouped( pass_count = [] fail_count = [] muted_count = [] + # A requirement whose configurable checks ran with an invalid config can't be + # trusted: treat the finding as FAIL (config is scan-global, memoised by id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check_id = finding.check_metadata.CheckID @@ -173,6 +182,12 @@ def _render_grouped( continue for req in check_map[check_id]: + effective_status = get_effective_status( + finding.status, + resolve_requirement_config_status( + req, audit_config, config_status_cache + ), + ) for group_key in _get_group_key(req, group_by): if group_key not in groups: groups[group_key] = {"FAIL": 0, "PASS": 0, "Muted": 0} @@ -182,10 +197,10 @@ def _render_grouped( muted_count.append(index) groups[group_key]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) groups[group_key]["FAIL"] += 1 - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) groups[group_key]["PASS"] += 1 @@ -261,6 +276,10 @@ def _render_split( pass_count = [] fail_count = [] muted_count = [] + # A requirement whose configurable checks ran with an invalid config can't be + # trusted: treat the finding as FAIL (config is scan-global, memoised by id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check_id = finding.check_metadata.CheckID @@ -268,6 +287,12 @@ def _render_split( continue for req in check_map[check_id]: + effective_status = get_effective_status( + finding.status, + resolve_requirement_config_status( + req, audit_config, config_status_cache + ), + ) for group_key in _get_group_key(req, group_by): if group_key not in groups: groups[group_key] = { @@ -282,15 +307,15 @@ def _render_split( muted_count.append(index) groups[group_key]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) for sv in split_values: if sv in str(split_val): if not finding.muted: - if finding.status == "FAIL": + if effective_status == "FAIL": groups[group_key][sv]["FAIL"] += 1 else: groups[group_key][sv]["PASS"] += 1 @@ -374,6 +399,10 @@ def _render_scored( generic_score = 0 max_generic_score = 0 counted_generic = [] + # A requirement whose configurable checks ran with an invalid config can't be + # trusted: treat the finding as FAIL (config is scan-global, memoised by id). + audit_config = get_scan_audit_config() + config_status_cache = {} for index, finding in enumerate(findings): check_id = finding.check_metadata.CheckID @@ -381,6 +410,12 @@ def _render_scored( continue for req in check_map[check_id]: + effective_status = get_effective_status( + finding.status, + resolve_requirement_config_status( + req, audit_config, config_status_cache + ), + ) for group_key in _get_group_key(req, group_by): attrs = req.attributes risk = attrs.get(risk_field, 0) @@ -393,7 +428,7 @@ def _render_scored( counted_per_group[group_key] = [] if index not in counted_per_group[group_key] and not finding.muted: - if finding.status == "PASS": + if effective_status == "PASS": score_per_group[group_key] += risk * weight max_score_per_group[group_key] += risk * weight counted_per_group[group_key].append(index) @@ -403,15 +438,15 @@ def _render_scored( muted_count.append(index) groups[group_key]["Muted"] += 1 else: - if finding.status == "FAIL" and index not in fail_count: + if effective_status == "FAIL" and index not in fail_count: fail_count.append(index) groups[group_key]["FAIL"] += 1 - elif finding.status == "PASS" and index not in pass_count: + elif effective_status == "PASS" and index not in pass_count: pass_count.append(index) groups[group_key]["PASS"] += 1 if index not in counted_generic and not finding.muted: - if finding.status == "PASS": + if effective_status == "PASS": generic_score += risk * weight max_generic_score += risk * weight counted_generic.append(index) diff --git a/tests/lib/check/compliance_config_constraint_model_test.py b/tests/lib/check/compliance_config_constraint_model_test.py new file mode 100644 index 00000000000..30bfdd9faef --- /dev/null +++ b/tests/lib/check/compliance_config_constraint_model_test.py @@ -0,0 +1,113 @@ +"""Validation coverage for the ConfigRequirements schema. + +``Compliance_Requirement_ConfigConstraint`` is the model behind every +``ConfigRequirements`` entry in the compliance framework JSONs. These tests pin +the operator vocabulary, the value-typing rules (notably that booleans are not +coerced to integers), and that constraints survive the legacy → universal +adaptation used by the App backend and the OCSF/table outputs. +""" + +import json +import pathlib + +import pytest +from pydantic.v1 import ValidationError + +from prowler.lib.check.compliance_models import ( + Compliance, + Compliance_Requirement_ConfigConstraint, + adapt_legacy_to_universal, +) + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] +_CIS_6_0 = _REPO_ROOT / "prowler" / "compliance" / "aws" / "cis_6.0_aws.json" + + +class Test_Compliance_Requirement_ConfigConstraint: + @pytest.mark.parametrize( + "operator,value", + [ + ("lte", 45), + ("gte", 365), + ("eq", False), + ("in", [1, 2, 3]), + ("subset", ["1.2", "1.3"]), + ("superset", ["RSA-1024", "P-192"]), + ], + ) + def test_valid_operators(self, operator, value): + c = Compliance_Requirement_ConfigConstraint( + Check="some_check", ConfigKey="some_key", Operator=operator, Value=value + ) + assert c.Operator == operator + assert c.Value == value + + def test_invalid_operator_rejected(self): + with pytest.raises(ValidationError): + Compliance_Requirement_ConfigConstraint( + Check="c", ConfigKey="k", Operator="between", Value=1 + ) + + def test_boolean_value_not_coerced_to_int(self): + # ``mute_non_default_regions == false`` must stay a bool, not become 0. + c = Compliance_Requirement_ConfigConstraint( + Check="securityhub_enabled", + ConfigKey="mute_non_default_regions", + Operator="eq", + Value=False, + ) + assert c.Value is False + assert isinstance(c.Value, bool) + + def test_list_value_preserved_for_set_operators(self): + c = Compliance_Requirement_ConfigConstraint( + Check="c", ConfigKey="k", Operator="subset", Value=["1.2", "1.3"] + ) + assert isinstance(c.Value, list) + assert c.Value == ["1.2", "1.3"] + + def test_missing_required_fields_rejected(self): + with pytest.raises(ValidationError): + Compliance_Requirement_ConfigConstraint(Check="c", ConfigKey="k") + + +class Test_ConfigRequirements_On_Compliance: + def test_requirements_without_constraints_default_to_none(self): + compliance = Compliance(**json.load(open(_CIS_6_0))) + # Requirement without configurable checks → ConfigRequirements is None. + no_constraint = [r for r in compliance.Requirements if not r.ConfigRequirements] + assert no_constraint + assert no_constraint[0].ConfigRequirements is None + + def test_requirement_with_constraints_parses(self): + compliance = Compliance(**json.load(open(_CIS_6_0))) + with_constraint = [r for r in compliance.Requirements if r.ConfigRequirements] + assert with_constraint, "cis_6.0_aws should declare ConfigRequirements" + constraint = with_constraint[0].ConfigRequirements[0] + assert isinstance(constraint, Compliance_Requirement_ConfigConstraint) + assert constraint.Check + assert constraint.Operator in {"lte", "gte", "eq", "in", "subset", "superset"} + + +class Test_Adapt_Legacy_To_Universal: + def test_config_requirements_carried_to_universal(self): + legacy = Compliance(**json.load(open(_CIS_6_0))) + universal = adapt_legacy_to_universal(legacy) + + legacy_with = {r.Id for r in legacy.Requirements if r.ConfigRequirements} + universal_with = {r.id for r in universal.requirements if r.config_requirements} + assert legacy_with == universal_with + assert universal_with, "expected at least one requirement with constraints" + + # The constraint payload survives as a list of dicts with the same keys. + sample = next(r for r in universal.requirements if r.config_requirements) + entry = sample.config_requirements[0] + assert isinstance(entry, dict) + assert set(entry) == {"Check", "ConfigKey", "Operator", "Value"} + + def test_requirements_without_constraints_are_none_in_universal(self): + legacy = Compliance(**json.load(open(_CIS_6_0))) + universal = adapt_legacy_to_universal(legacy) + without = [r for r in universal.requirements if not r.config_requirements] + assert without + assert without[0].config_requirements is None diff --git a/tests/lib/check/compliance_config_eval_test.py b/tests/lib/check/compliance_config_eval_test.py new file mode 100644 index 00000000000..8a1edc2d0ea --- /dev/null +++ b/tests/lib/check/compliance_config_eval_test.py @@ -0,0 +1,240 @@ +from types import SimpleNamespace + +from prowler.lib.check.compliance_config_eval import ( + CONFIG_NOT_VALID_PREFIX, + apply_config_status, + build_requirement_config_status, + evaluate_config_constraints, + get_effective_status, + get_scan_audit_config, + resolve_requirement_config_status, +) + +CONSTRAINTS = [ + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45, + } +] + + +class Test_evaluate_config_constraints: + def test_no_constraints_is_compliant(self): + assert evaluate_config_constraints(None, {}) == (True, "") + assert evaluate_config_constraints([], {"x": 1}) == (True, "") + + def test_config_absent_assumes_default_ok(self): + # Key not explicitly set → default assumed adequate. + is_ok, reason = evaluate_config_constraints(CONSTRAINTS, {}) + assert is_ok is True + assert reason == "" + + def test_none_audit_config_is_compliant(self): + assert evaluate_config_constraints(CONSTRAINTS, None) == (True, "") + + def test_lte_satisfied(self): + assert evaluate_config_constraints( + CONSTRAINTS, {"max_unused_access_keys_days": 45} + ) == (True, "") + + def test_lte_violated(self): + is_ok, reason = evaluate_config_constraints( + CONSTRAINTS, {"max_unused_access_keys_days": 120} + ) + assert is_ok is False + assert "max_unused_access_keys_days" in reason + assert "120" in reason + + def test_gte_operator(self): + c = [{"Check": "c", "ConfigKey": "k", "Operator": "gte", "Value": 10}] + assert evaluate_config_constraints(c, {"k": 10})[0] is True + assert evaluate_config_constraints(c, {"k": 9})[0] is False + + def test_eq_operator(self): + c = [{"Check": "c", "ConfigKey": "k", "Operator": "eq", "Value": "HIGH"}] + assert evaluate_config_constraints(c, {"k": "HIGH"})[0] is True + assert evaluate_config_constraints(c, {"k": "LOW"})[0] is False + + def test_in_operator(self): + c = [{"Check": "c", "ConfigKey": "k", "Operator": "in", "Value": [1, 2, 3]}] + assert evaluate_config_constraints(c, {"k": 2})[0] is True + assert evaluate_config_constraints(c, {"k": 9})[0] is False + + def test_subset_operator_allowlist(self): + # Allowlist config: applied list must stay within the secure baseline. + c = [ + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": ["1.2", "1.3"], + } + ] + assert ( + evaluate_config_constraints( + c, {"recommended_minimal_tls_versions": ["1.2", "1.3"]} + )[0] + is True + ) + # Stricter (subset) still passes. + assert ( + evaluate_config_constraints( + c, {"recommended_minimal_tls_versions": ["1.3"]} + )[0] + is True + ) + # Widening with a weaker value breaks it. + is_ok, reason = evaluate_config_constraints( + c, {"recommended_minimal_tls_versions": ["1.0", "1.2", "1.3"]} + ) + assert is_ok is False + assert "recommended_minimal_tls_versions" in reason + + def test_superset_operator_denylist(self): + # Denylist config: applied list must keep covering the forbidden baseline. + c = [ + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": ["RSA-1024", "P-192"], + } + ] + assert ( + evaluate_config_constraints( + c, {"insecure_key_algorithms": ["RSA-1024", "P-192"]} + )[0] + is True + ) + # Extra forbidden values are fine. + assert ( + evaluate_config_constraints( + c, {"insecure_key_algorithms": ["RSA-1024", "P-192", "P-224"]} + )[0] + is True + ) + # Removing a forbidden value breaks it. + assert ( + evaluate_config_constraints(c, {"insecure_key_algorithms": ["P-192"]})[0] + is False + ) + + def test_subset_superset_non_list_not_satisfied(self): + sub = [{"Check": "c", "ConfigKey": "k", "Operator": "subset", "Value": ["a"]}] + sup = [{"Check": "c", "ConfigKey": "k", "Operator": "superset", "Value": ["a"]}] + # A scalar applied value cannot satisfy a set constraint. + assert evaluate_config_constraints(sub, {"k": "a"})[0] is False + assert evaluate_config_constraints(sup, {"k": "a"})[0] is False + + def test_mismatched_types_not_satisfied(self): + assert ( + evaluate_config_constraints( + CONSTRAINTS, {"max_unused_access_keys_days": "x"} + )[0] + is False + ) + + def test_multiple_constraints_first_violation_reported(self): + constraints = [ + {"Check": "a", "ConfigKey": "k1", "Operator": "lte", "Value": 45}, + {"Check": "b", "ConfigKey": "k2", "Operator": "lte", "Value": 45}, + ] + is_ok, reason = evaluate_config_constraints(constraints, {"k1": 45, "k2": 90}) + assert is_ok is False + assert "b.k2" in reason + + +# A constraint forcing FAIL when the applied value is too loose. +REGION_CONSTRAINT = [ + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": False, + } +] + + +def _legacy_req(req_id, constraints=None): + """Fake legacy Compliance_Requirement (``Id`` / ``ConfigRequirements``).""" + return SimpleNamespace(Id=req_id, ConfigRequirements=constraints) + + +def _universal_req(req_id, constraints=None): + """Fake UniversalComplianceRequirement (``id`` / ``config_requirements``).""" + return SimpleNamespace(id=req_id, config_requirements=constraints) + + +class Test_build_requirement_config_status: + def test_only_requirements_with_constraints_included(self): + reqs = [_legacy_req("1", CONSTRAINTS), _legacy_req("2", None)] + status = build_requirement_config_status( + reqs, {"max_unused_access_keys_days": 120} + ) + assert set(status) == {"1"} + assert status["1"][0] is False + + def test_supports_universal_requirements(self): + reqs = [_universal_req("u1", REGION_CONSTRAINT)] + status = build_requirement_config_status( + reqs, {"mute_non_default_regions": True} + ) + assert status["u1"][0] is False + + def test_compliant_when_config_satisfied(self): + reqs = [_legacy_req("1", CONSTRAINTS)] + status = build_requirement_config_status( + reqs, {"max_unused_access_keys_days": 30} + ) + assert status["1"] == (True, "") + + +class Test_resolve_requirement_config_status: + def test_memoises_by_requirement_id(self): + cache = {} + req = _legacy_req("1", CONSTRAINTS) + first = resolve_requirement_config_status( + req, {"max_unused_access_keys_days": 120}, cache + ) + assert cache["1"] is first + assert first[0] is False + # A different audit_config is ignored once cached (intended for one build). + second = resolve_requirement_config_status(req, {}, cache) + assert second is first + + def test_requirement_without_constraints_is_ok(self): + cache = {} + req = _legacy_req("1", None) + assert resolve_requirement_config_status(req, {}, cache) == (True, "") + + +class Test_apply_config_status: + def test_none_config_status_keeps_finding(self): + assert apply_config_status("PASS", "ext", None) == ("PASS", "ext") + + def test_compliant_keeps_finding(self): + assert apply_config_status("PASS", "ext", (True, "")) == ("PASS", "ext") + + def test_invalid_config_forces_fail_and_prefixes_reason(self): + status, extended = apply_config_status("PASS", "ext", (False, "bad config")) + assert status == "FAIL" + assert extended.startswith(CONFIG_NOT_VALID_PREFIX) + assert "bad config" in extended + assert "ext" in extended + + +class Test_get_effective_status: + def test_none_and_compliant_keep_status(self): + assert get_effective_status("PASS", None) == "PASS" + assert get_effective_status("PASS", (True, "")) == "PASS" + + def test_invalid_config_forces_fail(self): + assert get_effective_status("PASS", (False, "reason")) == "FAIL" + + +class Test_get_scan_audit_config: + def test_returns_empty_without_global_provider(self): + # No global provider set in this unit-test context → safe empty mapping. + assert get_scan_audit_config() == {} diff --git a/tests/lib/check/compliance_config_requirements_data_test.py b/tests/lib/check/compliance_config_requirements_data_test.py new file mode 100644 index 00000000000..5309ceea6b6 --- /dev/null +++ b/tests/lib/check/compliance_config_requirements_data_test.py @@ -0,0 +1,161 @@ +"""Data-integrity tests for every ``ConfigRequirements`` declared in the shipped +compliance framework JSONs. + +These guard the ~700 constraints added across the frameworks against drift: +- every constraint is well-formed (valid operator, value typed for its operator), +- every constraint targets a check the requirement actually maps (no orphans), +- the region-mute invariant holds (every requirement mapping a region-scoped + check carries the ``mute_non_default_regions == false`` constraint), +- every framework still parses through its model. +""" + +import glob +import json +import pathlib + +import pytest + +from prowler.lib.check.compliance_models import Compliance, ComplianceFramework + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] +_COMPLIANCE_DIR = _REPO_ROOT / "prowler" / "compliance" + +_VALID_OPERATORS = {"lte", "gte", "eq", "in", "subset", "superset"} +# Checks whose result is untrustworthy when non-default regions are muted. +_REGION_CHECKS = { + "accessanalyzer_enabled", + "config_recorder_all_regions_enabled", + "drs_job_exist", + "guardduty_delegated_admin_enabled_all_regions", + "guardduty_is_enabled", + "securityhub_enabled", +} + +_ALL_FILES = sorted(glob.glob(str(_COMPLIANCE_DIR / "**" / "*.json"), recursive=True)) + + +def _load(path): + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def _requirements(data): + return data.get("Requirements") or data.get("requirements") or [] + + +def _req_id(req): + return req.get("Id") or req.get("id") + + +def _req_checks(req): + ch = req.get("Checks", req.get("checks")) + checks = set() + if isinstance(ch, dict): + for v in ch.values(): + checks |= set(v or []) + elif isinstance(ch, list): + checks |= set(ch) + return checks + + +def _req_constraints(req): + return req.get("ConfigRequirements") or req.get("config_requirements") or [] + + +def _iter_constraints(): + """Yield (file, req_id, checks, constraint) for every constraint shipped.""" + for path in _ALL_FILES: + data = _load(path) + for req in _requirements(data): + checks = _req_checks(req) + for c in _req_constraints(req): + yield pathlib.Path(path).name, _req_id(req), checks, c + + +_ALL_CONSTRAINTS = list(_iter_constraints()) + + +def test_there_are_constraints_to_validate(): + # Guards against the iteration silently finding nothing (e.g. path change). + assert len(_ALL_CONSTRAINTS) > 100 + + +@pytest.mark.parametrize( + "fname,req_id,checks,constraint", + _ALL_CONSTRAINTS, + ids=[f"{f}:{r}:{c['Check']}" for f, r, _, c in _ALL_CONSTRAINTS], +) +class Test_Constraint_Wellformed: + def test_has_required_keys(self, fname, req_id, checks, constraint): + assert set(constraint) == { + "Check", + "ConfigKey", + "Operator", + "Value", + }, f"{fname}:{req_id} malformed constraint {constraint}" + + def test_operator_valid(self, fname, req_id, checks, constraint): + assert constraint["Operator"] in _VALID_OPERATORS + + def test_check_is_mapped_by_requirement(self, fname, req_id, checks, constraint): + # No orphan constraints: the target check must be one the requirement runs. + assert constraint["Check"] in checks, ( + f"{fname}:{req_id} constraint targets {constraint['Check']} " + f"which the requirement does not map" + ) + + def test_value_type_matches_operator(self, fname, req_id, checks, constraint): + op, val = constraint["Operator"], constraint["Value"] + if op in ("subset", "superset", "in"): + assert isinstance(val, list), f"{fname}:{req_id} {op} needs a list value" + elif op in ("lte", "gte"): + # Numeric threshold; bool is not a valid threshold even though it is + # an int subclass. + assert isinstance(val, (int, float)) and not isinstance( + val, bool + ), f"{fname}:{req_id} {op} needs a numeric value, got {val!r}" + elif op == "eq": + assert isinstance( + val, (bool, int, float, str) + ), f"{fname}:{req_id} eq needs a scalar value" + + +class Test_Region_Mute_Invariant: + """Every requirement mapping a region-scoped check must carry the + ``mute_non_default_regions == false`` constraint for it.""" + + def test_region_checks_always_constrained(self): + gaps = [] + for path in _ALL_FILES: + data = _load(path) + for req in _requirements(data): + checks = _req_checks(req) + constrained = { + c["Check"] + for c in _req_constraints(req) + if c["ConfigKey"] == "mute_non_default_regions" + } + for region_check in checks & _REGION_CHECKS: + if region_check not in constrained: + gaps.append( + f"{pathlib.Path(path).name}:{_req_id(req)}:{region_check}" + ) + assert not gaps, f"region-mute constraint missing for: {gaps}" + + def test_region_mute_constraints_use_eq_false(self): + for fname, req_id, _checks, c in _ALL_CONSTRAINTS: + if c["ConfigKey"] == "mute_non_default_regions": + assert ( + c["Operator"] == "eq" and c["Value"] is False + ), f"{fname}:{req_id} region-mute must be eq false" + + +@pytest.mark.parametrize( + "path", _ALL_FILES, ids=[pathlib.Path(p).name for p in _ALL_FILES] +) +def test_every_framework_parses_with_constraints(path): + data = _load(path) + if "Requirements" in data: + Compliance(**data) + else: + ComplianceFramework.parse_obj(data) diff --git a/tests/lib/outputs/compliance/cis/cis_aws_config_requirements_test.py b/tests/lib/outputs/compliance/cis/cis_aws_config_requirements_test.py new file mode 100644 index 00000000000..56c5f51674f --- /dev/null +++ b/tests/lib/outputs/compliance/cis/cis_aws_config_requirements_test.py @@ -0,0 +1,89 @@ +"""Integration coverage for requirement-level config validation in the CIS AWS +CSV output. Requirement CIS 6.0 AWS 2.11 maps two configurable checks; when the +scan config is looser than the requirement demands, the requirement row must be +FAIL even if the underlying finding is PASS. The applied config is read from the +active provider's ``audit_config``.""" + +import json +import pathlib +from types import SimpleNamespace +from unittest.mock import patch + +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.cis.cis_aws import AWSCIS + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[5] +_CIS_6_0 = _REPO_ROOT / "prowler" / "compliance" / "aws" / "cis_6.0_aws.json" + + +def _load_cis_60() -> Compliance: + return Compliance(**json.load(open(_CIS_6_0))) + + +def _finding(check_id: str, status: str): + return SimpleNamespace( + provider="aws", + account_uid="123456789012", + region="us-east-1", + check_id=check_id, + status=status, + status_extended=f"{check_id} {status}", + resource_uid="arn:aws:iam::123456789012:user/bob", + resource_name="bob", + muted=False, + ) + + +def _rows_for(requirement_id, findings, audit_config): + with patch( + "prowler.providers.common.provider.Provider.get_global_provider" + ) as mock_gp: + mock_gp.return_value.audit_config = audit_config + out = AWSCIS(findings=findings, compliance=_load_cis_60(), file_path=None) + return [r for r in out._data if r.Requirements_Id == requirement_id] + + +class Test_CIS_AWS_Config_Requirements: + def test_loose_config_forces_requirement_fail(self): + findings = [_finding("iam_user_accesskey_unused", "PASS")] + rows = _rows_for("2.11", findings, {"max_unused_access_keys_days": 120}) + assert rows, "expected a row for requirement 2.11" + assert all(r.Status == "FAIL" for r in rows) + assert all("CONFIG NOT VALID" in r.StatusExtended for r in rows) + + def test_valid_config_keeps_finding_status(self): + findings = [_finding("iam_user_accesskey_unused", "PASS")] + rows = _rows_for("2.11", findings, {"max_unused_access_keys_days": 45}) + assert rows + assert all(r.Status == "PASS" for r in rows) + assert all("CONFIG NOT VALID" not in r.StatusExtended for r in rows) + + def test_absent_config_assumes_default_ok(self): + findings = [_finding("iam_user_accesskey_unused", "PASS")] + rows = _rows_for("2.11", findings, {}) + assert rows + assert all(r.Status == "PASS" for r in rows) + + def test_other_requirements_unaffected(self): + # A finding for a check without ConfigRequirements keeps its status even + # when the config is loose for a different requirement. + findings = [_finding("iam_rotate_access_key_90_days", "PASS")] + rows = _rows_for("2.13", findings, {"max_unused_access_keys_days": 120}) + assert rows + assert all(r.Status == "PASS" for r in rows) + + def test_region_mute_constraint_forces_fail(self): + # Requirement 5.16 maps securityhub_enabled with a + # mute_non_default_regions == false constraint: muting non-default + # regions makes the PASS untrustworthy, so the row must be FAIL. + findings = [_finding("securityhub_enabled", "PASS")] + rows = _rows_for("5.16", findings, {"mute_non_default_regions": True}) + assert rows, "expected a row for requirement 5.16" + assert all(r.Status == "FAIL" for r in rows) + assert all("CONFIG NOT VALID" in r.StatusExtended for r in rows) + + def test_region_mute_constraint_default_passes(self): + findings = [_finding("securityhub_enabled", "PASS")] + rows = _rows_for("5.16", findings, {"mute_non_default_regions": False}) + assert rows + assert all(r.Status == "PASS" for r in rows) diff --git a/tests/lib/outputs/compliance/cis/cis_azure_config_requirements_test.py b/tests/lib/outputs/compliance/cis/cis_azure_config_requirements_test.py new file mode 100644 index 00000000000..99c6268c43a --- /dev/null +++ b/tests/lib/outputs/compliance/cis/cis_azure_config_requirements_test.py @@ -0,0 +1,75 @@ +"""Integration coverage for the ``subset`` set-operator in a CSV output. + +CIS Azure 5.0 requirement 9.1.3 maps ``storage_smb_channel_encryption_with_secure_algorithm`` +with a ``recommended_smb_channel_encryption_algorithms subset ["AES-256-GCM"]`` +constraint: widening the allowlist with a weaker algorithm makes the PASS +untrustworthy, so the requirement row must be FAIL. Exercises the shared override +path through a per-provider CSV class (not just OCSF).""" + +import json +import pathlib +from types import SimpleNamespace +from unittest.mock import patch + +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[5] +_CIS_5_0_AZURE = _REPO_ROOT / "prowler" / "compliance" / "azure" / "cis_5.0_azure.json" +_REQUIREMENT_ID = "9.1.3" +_CHECK = "storage_smb_channel_encryption_with_secure_algorithm" + + +def _load(): + return Compliance(**json.load(open(_CIS_5_0_AZURE))) + + +def _finding(check_id, status): + return SimpleNamespace( + provider="azure", + account_uid="00000000-0000-0000-0000-000000000000", + region="eastus", + check_id=check_id, + status=status, + status_extended=f"{check_id} {status}", + resource_uid="/subscriptions/x/storageAccounts/sa", + resource_name="sa", + muted=False, + ) + + +def _rows_for(audit_config): + findings = [_finding(_CHECK, "PASS")] + with patch( + "prowler.providers.common.provider.Provider.get_global_provider" + ) as mock_gp: + mock_gp.return_value.audit_config = audit_config + out = AzureCIS(findings=findings, compliance=_load(), file_path=None) + return [r for r in out._data if r.Requirements_Id == _REQUIREMENT_ID] + + +class Test_CIS_Azure_Subset_Constraint: + def test_widened_allowlist_forces_fail(self): + rows = _rows_for( + { + "recommended_smb_channel_encryption_algorithms": [ + "AES-128-CCM", + "AES-256-GCM", + ] + } + ) + assert rows, f"expected a row for requirement {_REQUIREMENT_ID}" + assert all(r.Status == "FAIL" for r in rows) + assert all("CONFIG NOT VALID" in r.StatusExtended for r in rows) + + def test_secure_allowlist_keeps_pass(self): + rows = _rows_for( + {"recommended_smb_channel_encryption_algorithms": ["AES-256-GCM"]} + ) + assert rows + assert all(r.Status == "PASS" for r in rows) + + def test_absent_config_keeps_pass(self): + rows = _rows_for({}) + assert rows + assert all(r.Status == "PASS" for r in rows) diff --git a/tests/lib/outputs/compliance/ens/ens_aws_config_requirements_test.py b/tests/lib/outputs/compliance/ens/ens_aws_config_requirements_test.py new file mode 100644 index 00000000000..336af73df23 --- /dev/null +++ b/tests/lib/outputs/compliance/ens/ens_aws_config_requirements_test.py @@ -0,0 +1,61 @@ +"""Integration coverage proving the shared requirement-level config validation +is applied beyond CIS. ENS RD2022 AWS requirement ``op.exp.1.aws.cfg.1`` maps +``config_recorder_all_regions_enabled`` with a ``mute_non_default_regions == +false`` constraint; muting non-default regions makes a PASS untrustworthy, so +the requirement row must be FAIL even when the finding PASSes. The applied +config is read from the active provider's ``audit_config``.""" + +import json +import pathlib +from types import SimpleNamespace +from unittest.mock import patch + +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS + +_REPO_ROOT = pathlib.Path(__file__).resolve().parents[5] +_ENS = _REPO_ROOT / "prowler" / "compliance" / "aws" / "ens_rd2022_aws.json" +_REQUIREMENT_ID = "op.exp.1.aws.cfg.1" + + +def _load_ens() -> Compliance: + return Compliance(**json.load(open(_ENS))) + + +def _finding(check_id: str, status: str): + return SimpleNamespace( + provider="aws", + account_uid="123456789012", + region="us-east-1", + check_id=check_id, + status=status, + status_extended=f"{check_id} {status}", + resource_uid="arn:aws:config:us-east-1:123456789012:recorder/default", + resource_name="default", + muted=False, + ) + + +def _rows_for(requirement_id, findings, audit_config): + with patch( + "prowler.providers.common.provider.Provider.get_global_provider" + ) as mock_gp: + mock_gp.return_value.audit_config = audit_config + out = AWSENS(findings=findings, compliance=_load_ens(), file_path=None) + return [r for r in out._data if r.Requirements_Id == requirement_id] + + +class Test_ENS_AWS_Config_Requirements: + def test_region_mute_constraint_forces_fail(self): + findings = [_finding("config_recorder_all_regions_enabled", "PASS")] + rows = _rows_for(_REQUIREMENT_ID, findings, {"mute_non_default_regions": True}) + assert rows, f"expected a row for requirement {_REQUIREMENT_ID}" + assert all(r.Status == "FAIL" for r in rows) + assert all("CONFIG NOT VALID" in r.StatusExtended for r in rows) + + def test_default_config_keeps_finding_status(self): + findings = [_finding("config_recorder_all_regions_enabled", "PASS")] + rows = _rows_for(_REQUIREMENT_ID, findings, {}) + assert rows + assert all(r.Status == "PASS" for r in rows) + assert all("CONFIG NOT VALID" not in r.StatusExtended for r in rows) diff --git a/tests/lib/outputs/compliance/universal/ocsf_compliance_config_requirements_test.py b/tests/lib/outputs/compliance/universal/ocsf_compliance_config_requirements_test.py new file mode 100644 index 00000000000..18e706077c0 --- /dev/null +++ b/tests/lib/outputs/compliance/universal/ocsf_compliance_config_requirements_test.py @@ -0,0 +1,191 @@ +"""Integration coverage for ConfigRequirements in the OCSF compliance output. + +OCSF is the universal output path every framework renders through, so it is the +natural place to exercise the requirement-level config override end to end across +all operators. When a requirement's configurable check ran with a config too +loose to trust, the Compliance status must be FAIL (even on a PASS finding) and +the message must carry the ``[CONFIG NOT VALID]`` prefix. The Check status keeps +the real finding status. +""" + +from datetime import datetime, timezone +from types import SimpleNamespace +from unittest.mock import patch + +import pytest +from py_ocsf_models.objects.compliance_status import StatusID as ComplianceStatusID + +from prowler.lib.check.compliance_models import ( + ComplianceFramework, + OutputsConfig, + TableConfig, + UniversalComplianceRequirement, +) +from prowler.lib.outputs.compliance.universal.ocsf_compliance import ( + OCSFComplianceOutput, +) + +_MODULE = "prowler.providers.common.provider.Provider.get_global_provider" + + +def _finding(check_id, status="PASS", provider="aws"): + finding = SimpleNamespace() + finding.provider = provider + finding.account_uid = "123456789012" + finding.account_name = "test-account" + finding.account_email = "" + finding.account_organization_uid = "org-123" + finding.account_organization_name = "test-org" + finding.account_tags = {"env": "test"} + finding.region = "us-east-1" + finding.status = status + finding.status_extended = f"{check_id} is {status}" + finding.resource_uid = f"arn:aws:iam::123456789012:{check_id}" + finding.resource_name = check_id + finding.resource_details = "details" + finding.resource_metadata = {} + finding.resource_tags = {"Name": "test"} + finding.partition = "aws" + finding.muted = False + finding.check_id = check_id + finding.uid = "test-finding-uid" + finding.timestamp = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc) + finding.prowler_version = "5.0.0" + finding.compliance = {} + finding.metadata = SimpleNamespace( + Provider=provider, + CheckID=check_id, + CheckTitle=f"Title for {check_id}", + CheckType=["test-type"], + Description=f"Description for {check_id}", + Severity="medium", + ServiceName="iam", + ResourceType="aws-iam-role", + Risk="risk", + RelatedUrl="https://example.com", + Remediation=SimpleNamespace( + Recommendation=SimpleNamespace(Text="Fix", Url="https://fix.com"), + ), + DependsOn=[], + RelatedTo=[], + Categories=["test"], + Notes="", + AdditionalURLs=[], + ) + return finding + + +def _framework(check_id, constraint): + req = UniversalComplianceRequirement( + id="REQ-1", + description="Requirement REQ-1", + attributes={}, + checks={"aws": [check_id]}, + config_requirements=[constraint], + ) + return ComplianceFramework( + framework="TestFW", + name="Test Framework", + provider="AWS", + version="1.0", + description="Test framework", + requirements=[req], + attributes_metadata=None, + outputs=OutputsConfig(table_config=TableConfig(group_by="Section")), + ) + + +def _run(check_id, constraint, audit_config): + fw = _framework(check_id, constraint) + findings = [_finding(check_id, "PASS")] + with patch(_MODULE) as mock_gp: + mock_gp.return_value.audit_config = audit_config + out = OCSFComplianceOutput(findings=findings, framework=fw, provider="aws") + return out.data[0] + + +# (check, constraint, violating_config, valid_config) +_CASES = [ + ( + "securityhub_enabled", + { + "Check": "securityhub_enabled", + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": False, + }, + {"mute_non_default_regions": True}, + {"mute_non_default_regions": False}, + ), + ( + "iam_user_accesskey_unused", + { + "Check": "iam_user_accesskey_unused", + "ConfigKey": "max_unused_access_keys_days", + "Operator": "lte", + "Value": 45, + }, + {"max_unused_access_keys_days": 120}, + {"max_unused_access_keys_days": 30}, + ), + ( + "cloudwatch_log_group_retention_policy_specific_days_enabled", + { + "Check": "cloudwatch_log_group_retention_policy_specific_days_enabled", + "ConfigKey": "log_group_retention_days", + "Operator": "gte", + "Value": 365, + }, + {"log_group_retention_days": 90}, + {"log_group_retention_days": 365}, + ), + ( + "sqlserver_recommended_minimal_tls_version", + { + "Check": "sqlserver_recommended_minimal_tls_version", + "ConfigKey": "recommended_minimal_tls_versions", + "Operator": "subset", + "Value": ["1.2", "1.3"], + }, + {"recommended_minimal_tls_versions": ["1.0", "1.2", "1.3"]}, + {"recommended_minimal_tls_versions": ["1.3"]}, + ), + ( + "acm_certificates_with_secure_key_algorithms", + { + "Check": "acm_certificates_with_secure_key_algorithms", + "ConfigKey": "insecure_key_algorithms", + "Operator": "superset", + "Value": ["RSA-1024", "P-192"], + }, + {"insecure_key_algorithms": ["P-192"]}, + {"insecure_key_algorithms": ["RSA-1024", "P-192"]}, + ), +] + + +class Test_OCSF_Config_Requirements: + @pytest.mark.parametrize( + "check,constraint,bad,ok", + _CASES, + ids=[c[1]["Operator"] for c in _CASES], + ) + def test_violating_config_fails_requirement(self, check, constraint, bad, ok): + cf = _run(check, constraint, bad) + assert cf.compliance.status_id == ComplianceStatusID.Fail + assert "[CONFIG NOT VALID]" in cf.message + + @pytest.mark.parametrize( + "check,constraint,bad,ok", + _CASES, + ids=[c[1]["Operator"] for c in _CASES], + ) + def test_valid_config_keeps_pass(self, check, constraint, bad, ok): + cf = _run(check, constraint, ok) + assert cf.compliance.status_id == ComplianceStatusID.Pass + assert "[CONFIG NOT VALID]" not in cf.message + + def test_absent_config_assumes_default_ok(self): + check, constraint, _bad, _ok = _CASES[0] + cf = _run(check, constraint, {}) + assert cf.compliance.status_id == ComplianceStatusID.Pass diff --git a/tests/lib/outputs/compliance/universal/universal_table_config_requirements_test.py b/tests/lib/outputs/compliance/universal/universal_table_config_requirements_test.py new file mode 100644 index 00000000000..a684a79e6a0 --- /dev/null +++ b/tests/lib/outputs/compliance/universal/universal_table_config_requirements_test.py @@ -0,0 +1,90 @@ +"""Integration coverage for ConfigRequirements in the console table generators. + +The table generators aggregate pass/fail counts, so a requirement whose config +is too loose must count its (otherwise PASS) finding as FAIL. Driven through the +universal table renderer, which backs the table output for every framework using +the shared ``get_effective_status`` helper.""" + +from types import SimpleNamespace +from unittest.mock import patch + +from prowler.lib.check.compliance_models import ( + ComplianceFramework, + OutputsConfig, + TableConfig, + UniversalComplianceRequirement, +) +from prowler.lib.outputs.compliance.universal.universal_table import get_universal_table + +_MODULE = "prowler.providers.common.provider.Provider.get_global_provider" +_CHECK = "securityhub_enabled" + + +def _finding(status="PASS"): + return SimpleNamespace( + check_metadata=SimpleNamespace(CheckID=_CHECK), status=status, muted=False + ) + + +# The overview table is only printed when there is more than one finding, so the +# tests use two PASS findings (both mapping the constrained check). +_FINDINGS = [_finding("PASS"), _finding("PASS")] + + +def _framework(): + req = UniversalComplianceRequirement( + id="1.1", + description="region check", + attributes={"Section": "Monitoring"}, + checks={"aws": [_CHECK]}, + config_requirements=[ + { + "Check": _CHECK, + "ConfigKey": "mute_non_default_regions", + "Operator": "eq", + "Value": False, + } + ], + ) + return ComplianceFramework( + framework="TestFW", + name="Test Framework", + provider="AWS", + version="1.0", + description="Test", + requirements=[req], + outputs=OutputsConfig(table_config=TableConfig(group_by="Section")), + ) + + +def _render(audit_config, capsys): + with patch(_MODULE) as mock_gp: + mock_gp.return_value.audit_config = audit_config + get_universal_table( + findings=_FINDINGS, + bulk_checks_metadata={}, + compliance_framework_name="testfw_1.0_aws", + output_filename="out", + output_directory="/tmp", + compliance_overview=False, + framework=_framework(), + provider="aws", + ) + return capsys.readouterr().out + + +class Test_Universal_Table_Config_Requirements: + def test_violating_config_counts_pass_finding_as_fail(self, capsys): + out = _render({"mute_non_default_regions": True}, capsys) + assert "FAIL(2)" in out + assert "PASS(2)" not in out + + def test_valid_config_keeps_pass_count(self, capsys): + out = _render({"mute_non_default_regions": False}, capsys) + assert "PASS(2)" in out + assert "FAIL(2)" not in out + + def test_absent_config_keeps_pass_count(self, capsys): + out = _render({}, capsys) + assert "PASS(2)" in out + assert "FAIL(2)" not in out From 7f249360c220e9d15beb23b5923d8d4ee1c3855c Mon Sep 17 00:00:00 2001 From: pedrooot Date: Tue, 23 Jun 2026 12:14:27 +0200 Subject: [PATCH 7/7] chore(changelog): update with latest changes --- prowler/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 3cab5e423ce..d9b5ce21856 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -52,6 +52,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `acmpca_certificate_authority_pqc_key_algorithm` check and new `acmpca` service for AWS provider to verify AWS Private CA certificate authorities use a post-quantum (ML-DSA) key algorithm [(#11318)](https://github.com/prowler-cloud/prowler/pull/11318) - `rolesanywhere_trust_anchor_pqc_pki` check and new `rolesanywhere` service for AWS provider to verify IAM Roles Anywhere trust anchors are backed by a post-quantum (ML-DSA) PKI [(#11319)](https://github.com/prowler-cloud/prowler/pull/11319) - Kubernetes core checks for container CPU limits, CPU requests, memory limits, memory requests, fixed image tags, liveness probes, and readiness probes [(#11373)](https://github.com/prowler-cloud/prowler/pull/11373) +- Per-requirement configuration validation for compliance frameworks via `ConfigRequirements`, so a requirement is reported as FAIL when its configurable checks ran with a configuration too loose to satisfy it (applied across all compliance outputs: CSV, OCSF, and console tables) [(#11667)](https://github.com/prowler-cloud/prowler/pull/11667) ### 🔄 Changed